Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add calculations #3

Merged
merged 4 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions src/controllers/poolController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import {
type RoundWithApplications as IndexerRoundWithApplications,
} from '@/ext/indexer';

import { BadRequestError, IsNullError, NotFoundError } from '@/errors';
import {
BadRequestError,
IsNullError,
NotFoundError,
ServerError,
} from '@/errors';
import { EligibilityType } from '@/entity/EligibilityCriteria';
import { calculate } from '@/utils/calculate';

const logger = createLogger();

Expand All @@ -21,7 +27,7 @@ interface CreatePoolRequest {
eligibilityData: object;
}

interface SyncPoolRequest {
interface ChainIdAlloPoolIdRequest {
chainId: number;
alloPoolId: string;
}
Expand Down Expand Up @@ -102,7 +108,7 @@ export const syncPool = async (req: Request, res: Response): Promise<void> => {
validateRequest(req, res);

// Extract chainId and alloPoolId from the request body
const { chainId, alloPoolId } = req.body as SyncPoolRequest;
const { chainId, alloPoolId } = req.body as ChainIdAlloPoolIdRequest;

// Log the receipt of the update request
logger.info(
Expand Down Expand Up @@ -153,3 +159,69 @@ const updateApplications = async (
applicationData
);
};

/**
* Calculates the distribution of a pool based on chainId and alloPoolId
*
* @param req - Express request object
* @param res - Express response object
*/
export const calculateDistribution = async (req, res): Promise<void> => {
const { chainId, alloPoolId } = req.body as ChainIdAlloPoolIdRequest;

const [errorFetching, distribution] = await catchError(
calculate(chainId, alloPoolId)
);

if (errorFetching !== null || distribution === undefined) {
logger.error(`Failed to calculate distribution: ${errorFetching?.message}`);
res.status(500).json({
message: 'Error calculating distribution',
error: errorFetching?.message,
});
throw new ServerError(`Error calculating distribution`);
}

const [errorUpdating, updatedDistribution] = await catchError(
poolService.updateDistribution(alloPoolId, chainId, distribution)
);

if (errorUpdating !== null || updatedDistribution === null) {
logger.error(`Failed to update distribution: ${errorUpdating?.message}`);
res.status(500).json({
message: 'Error updating distribution',
error: errorUpdating?.message,
});
}

res.status(200).json({ message: 'Distribution updated successfully' });
};

/**
* Finalizes the distribution of a pool based on chainId and alloPoolId
*
* @param req - Express request object
* @param res - Express response object
*/
export const finalizeDistribution = async (
req: Request,
res: Response
): Promise<void> => {
const { chainId, alloPoolId } = req.body as ChainIdAlloPoolIdRequest;

const [errorFinalizing, finalizedDistribution] = await catchError(
poolService.finalizePoolDistribution(alloPoolId, chainId)
);

if (errorFinalizing !== null || finalizedDistribution === null) {
logger.error(
`Failed to finalize distribution: ${errorFinalizing?.message}`
);
res.status(500).json({
message: 'Error finalizing distribution',
error: errorFinalizing?.message,
});
}

res.status(200).json({ message: 'Distribution finalized successfully' });
};
167 changes: 106 additions & 61 deletions src/controllers/voteController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ import { type Request, type Response } from 'express';
import voteService from '@/service/VoteService';
import poolService from '@/service/PoolService';
import { catchError, validateRequest } from '@/utils';
import { BadRequestError } from '@/errors';
import { BadRequestError, ServerError } from '@/errors';
import { createLogger } from '@/logger';
import { calculate } from '@/utils/calculate';
import { type Pool } from '@/entity/Pool';
import { type Vote } from '@/entity/Vote';
const logger = createLogger();

// Interface for ballot items
interface BallotItem {
metricId: number;
voteShare: number;
}

/**
* Submits a vote for a given pool
*
Expand All @@ -25,65 +22,113 @@ export const submitVote = async (
// Validate the incoming request
validateRequest(req, res);

const { voter, alloPoolId, chainId, ballot } = req.body;
if (
typeof voter !== 'string' ||
typeof alloPoolId !== 'string' ||
typeof chainId !== 'number' ||
(Array.isArray(ballot) &&
ballot.every(
item =>
typeof item.metricName === 'string' &&
(item.metricId === undefined || typeof item.metricId === 'number') &&
typeof item.voteShare === 'number'
))
) {
throw new BadRequestError('Invalid request data');
}

// Get the pool using PoolService
const pool = await poolService.getPoolByChainIdAndAlloPoolId(
chainId,
alloPoolId.toString()
);

if (pool === null) {
res.status(404).json({ message: 'Pool not found' });
throw new BadRequestError('Pool not found');
}

if (!(await isVoterEligible(pool, voter))) {
res.status(401).json({ message: 'Not Authorzied' });
throw new BadRequestError('Not Authorzied');
}

const [error, result] = await catchError(
(async () => {
const { voter, alloPoolId, chainId, ballot } = req.body;

// Validate request body
if (
typeof voter !== 'string' ||
typeof alloPoolId !== 'string' ||
typeof chainId !== 'number' ||
!Array.isArray(ballot) ||
ballot.some(
(item: BallotItem) =>
typeof item.metricId !== 'number' ||
typeof item.voteShare !== 'number'
)
) {
throw new BadRequestError('Invalid request data');
}

// Get the pool using PoolService
const pool = await poolService.getPoolByChainIdAndAlloPoolId(
chainId,
alloPoolId.toString()
);

if (pool === null) {
throw new BadRequestError('Pool not found');
}

// TODO: Check if voter is eligible based on pool eligibility type

// Save the vote using VoteService
const savedVote = await voteService.saveVote({
voter,
alloPoolId,
chainId,
ballot,
pool, // Associate the pool entity
poolId: pool.id,
});

return savedVote;
})()
voteService.saveVote({
voter,
alloPoolId,
chainId,
ballot,
pool,
poolId: pool.id,
})
);

if (error !== null || result === null) {
logger.error(`Failed to submit vote: ${error?.message}`);
if (error instanceof BadRequestError) {
res.status(400).json({ message: error.message });
} else {
res
.status(500)
.json({ message: 'Error submitting vote', error: error?.message });
}
return;
res
.status(500)
.json({ message: 'Error submitting vote', error: error?.message });
throw new ServerError(`Error submitting vote`);
}

// Trigger the distribution without waiting
void calculate(chainId, alloPoolId);

logger.info('Vote submitted successfully', result);
res
.status(201)
.json({ message: 'Vote submitted successfully', data: result });
res.status(201).json({ message: 'Vote submitted successfully' });
};

const isVoterEligible = async (pool: Pool, voter: string): Promise<boolean> => {
// TODO: Check if voter is eligible based on pool eligibility type
// TODO: also validate is sender is the voter
return await Promise.resolve(true);
};

/**
* Predicts the distribution of a pool based on chainId and alloPoolId and ballot
*
* @param req - Express request object
* @param res - Express response object
*/
export const predictDistribution = async (
req: Request,
res: Response
): Promise<void> => {
const { alloPoolId, chainId, ballot } = req.body;

if (
typeof alloPoolId !== 'string' ||
typeof chainId !== 'number' ||
!Array.isArray(ballot) ||
(Array.isArray(ballot) &&
ballot.every(
item =>
typeof item.metricName === 'string' &&
(item.metricId === undefined || typeof item.metricId === 'number') &&
typeof item.voteShare === 'number'
))
) {
throw new BadRequestError('Invalid request data');
}

const unAccountedBallots: Partial<Vote> = {
ballot,
};

const [errorFetching, distribution] = await catchError(
calculate(chainId, alloPoolId, unAccountedBallots)
);

if (errorFetching !== null || distribution === undefined) {
logger.error(`Failed to calculate distribution: ${errorFetching?.message}`);
res.status(500).json({
message: 'Error calculating distribution',
error: errorFetching?.message,
});
throw new ServerError(`Error calculating distribution`);
}

logger.info('Distribution predicted successfully', distribution);
res.status(200).json(distribution);
};
38 changes: 0 additions & 38 deletions src/entity/Distribution.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/entity/Pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import { Metric } from './Metric';
import { EligibilityCriteria } from './EligibilityCriteria';
import { Vote } from './Vote';

export interface Distribution {
alloApplicationId: string;
distribution_percentage: number;
}

@Entity()
@Unique(['chainId', 'alloPoolId'])
export class Pool {
Expand All @@ -38,4 +43,10 @@ export class Pool {

@OneToMany(() => Vote, vote => vote.pool)
votes: Vote[];

@Column({ default: false })
finalized: boolean;

@Column('simple-json', { nullable: true })
distribution: Distribution[];
}
11 changes: 7 additions & 4 deletions src/entity/Vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {
} from 'typeorm';
import { Pool } from './Pool';

export interface Ballot {
metricName: string;
metricId?: number;
voteShare: number; // Percentage of the total vote allocated to this metric
}

@Entity()
@Unique(['poolId', 'voter'])
export class Vote {
Expand All @@ -23,10 +29,7 @@ export class Vote {
chainId: number;

@Column('simple-json')
ballot: Array<{
metricId: number;
voteShare: number; // Percentage of the total vote allocated to this metric
}>;
ballot: Ballot[];

@ManyToOne(() => Pool, pool => pool.votes)
pool: Pool;
Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ export class IsNullError extends BaseError {
super(message, 500);
}
}

export class ServerError extends BaseError {
constructor(message: string) {
super(message, 500);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type MigrationInterface, type QueryRunner } from "typeorm";

export class InitMigration1736446973041 implements MigrationInterface {
name = 'InitMigration1736446973041'
export class InitMigration1736569153513 implements MigrationInterface {
name = 'InitMigration1736569153513'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "application" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloApplicationId" character varying NOT NULL, "poolId" integer NOT NULL, CONSTRAINT "UQ_8849159f2a2681f6be67ef84efb" UNIQUE ("alloApplicationId", "poolId"), CONSTRAINT "PK_569e0c3e863ebdf5f2408ee1670" PRIMARY KEY ("id"))`);
Expand Down
Loading
Loading