From 1d342263d10dce30ab433505786914a15920caba Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Wed, 15 Jan 2025 16:09:22 +0530 Subject: [PATCH 1/3] address feedback from team - remove finalize from db - add TODO to fetch finalise from DB - metricName is not unique - add metricIdentifier to Metric and use this in ballot - recreate migration --- src/controllers/metricController.ts | 1 + src/controllers/poolController.ts | 51 +------- src/controllers/voteController.ts | 3 +- src/entity/Metric.ts | 7 +- src/entity/Pool.ts | 3 - src/entity/Vote.ts | 3 +- src/errors.ts | 6 + ...tion.ts => 1736937474029-InitMigration.ts} | 8 +- src/routes/metricRoutes.ts | 5 + src/routes/poolRoutes.ts | 26 ---- src/routes/voteRoutes.ts | 17 ++- src/service/PoolService.ts | 13 -- src/utils/calculate.ts | 120 ++++++++++-------- 13 files changed, 101 insertions(+), 162 deletions(-) rename src/migration/{1736788844815-InitMigration.ts => 1736937474029-InitMigration.ts} (79%) diff --git a/src/controllers/metricController.ts b/src/controllers/metricController.ts index 56a1d87..7875398 100644 --- a/src/controllers/metricController.ts +++ b/src/controllers/metricController.ts @@ -11,6 +11,7 @@ const logger = createLogger(); const isMetric = (obj: any): obj is Metric => { return ( typeof obj === 'object' && + typeof obj.identifier === 'string' && typeof obj.name === 'string' && typeof obj.description === 'string' && (obj.orientation === MetricOrientation.Increase || diff --git a/src/controllers/poolController.ts b/src/controllers/poolController.ts index 55e9d56..5a26f6a 100644 --- a/src/controllers/poolController.ts +++ b/src/controllers/poolController.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express'; import poolService from '@/service/PoolService'; import applicationService from '@/service/ApplicationService'; -import { catchError, isPoolManager, validateRequest } from '@/utils'; +import { catchError, validateRequest } from '@/utils'; import { createLogger } from '@/logger'; import { indexerClient, @@ -13,11 +13,9 @@ import { IsNullError, NotFoundError, ServerError, - UnauthorizedError, } from '@/errors'; import { EligibilityType } from '@/entity/EligibilityCriteria'; import { calculate } from '@/utils/calculate'; -import { type Hex } from 'viem'; const logger = createLogger(); @@ -34,12 +32,6 @@ interface ChainIdAlloPoolIdRequest { alloPoolId: string; } -interface FinalizePoolRequest { - chainId: number; - alloPoolId: string; - signature: Hex; -} - /** * Creates a pool * @@ -204,44 +196,3 @@ export const calculateDistribution = async (req, res): Promise => { 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 => { - const { chainId, alloPoolId, signature } = req.body as FinalizePoolRequest; - - if ( - !(await isPoolManager( - { chainId, alloPoolId }, - signature, - chainId, - alloPoolId - )) - ) { - res.status(401).json({ message: 'Unauthorized' }); - throw new UnauthorizedError('Unauthorized'); - } - - 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' }); -}; diff --git a/src/controllers/voteController.ts b/src/controllers/voteController.ts index fda22d1..ecd9b4b 100644 --- a/src/controllers/voteController.ts +++ b/src/controllers/voteController.ts @@ -34,8 +34,7 @@ export const submitVote = async ( (Array.isArray(ballot) && ballot.every( item => - typeof item.metricName === 'string' && - (item.metricId === undefined || typeof item.metricId === 'number') && + typeof item.metricIdentifier === 'string' && typeof item.voteShare === 'number' )) ) { diff --git a/src/entity/Metric.ts b/src/entity/Metric.ts index bde1680..4c688a5 100644 --- a/src/entity/Metric.ts +++ b/src/entity/Metric.ts @@ -6,11 +6,16 @@ export enum MetricOrientation { } @Entity() -@Unique(['name']) +@Unique(['identifier']) export class Metric { @PrimaryGeneratedColumn() id: number; + @Column({ + unique: true, + }) + identifier: string; + @Column() name: string; diff --git a/src/entity/Pool.ts b/src/entity/Pool.ts index fb42220..3e62a11 100644 --- a/src/entity/Pool.ts +++ b/src/entity/Pool.ts @@ -44,9 +44,6 @@ export class Pool { @OneToMany(() => Vote, vote => vote.pool) votes: Vote[]; - @Column({ default: false }) - finalized: boolean; - @Column('simple-json', { nullable: true }) distribution: Distribution[]; } diff --git a/src/entity/Vote.ts b/src/entity/Vote.ts index 23e90ed..3c2d191 100644 --- a/src/entity/Vote.ts +++ b/src/entity/Vote.ts @@ -8,8 +8,7 @@ import { import { Pool } from './Pool'; export interface Ballot { - metricName: string; - metricId?: number; + metricIdentifier: string; voteShare: number; // Percentage of the total vote allocated to this metric } diff --git a/src/errors.ts b/src/errors.ts index 65fe727..c5b049e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -45,3 +45,9 @@ export class UnauthorizedError extends BaseError { super(message, 401); } } + +export class ActionNotAllowedError extends BaseError { + constructor(message: string) { + super(message, 403); + } +} diff --git a/src/migration/1736788844815-InitMigration.ts b/src/migration/1736937474029-InitMigration.ts similarity index 79% rename from src/migration/1736788844815-InitMigration.ts rename to src/migration/1736937474029-InitMigration.ts index 9be5b9a..7fef14c 100644 --- a/src/migration/1736788844815-InitMigration.ts +++ b/src/migration/1736937474029-InitMigration.ts @@ -1,15 +1,15 @@ import { type MigrationInterface, type QueryRunner } from "typeorm"; -export class InitMigration1736788844815 implements MigrationInterface { - name = 'InitMigration1736788844815' +export class InitMigration1736937474029 implements MigrationInterface { + name = 'InitMigration1736937474029' public async up(queryRunner: QueryRunner): Promise { 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"))`); await queryRunner.query(`CREATE TYPE "public"."metric_orientation_enum" AS ENUM('increase', 'decrease')`); - await queryRunner.query(`CREATE TABLE "metric" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "description" character varying NOT NULL, "orientation" "public"."metric_orientation_enum" NOT NULL, "enabled" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_54e5ac9404e6102f0c661a5bf06" UNIQUE ("name"), CONSTRAINT "PK_7d24c075ea2926dd32bd1c534ce" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "metric" ("id" SERIAL NOT NULL, "identifier" character varying NOT NULL, "name" character varying NOT NULL, "description" character varying NOT NULL, "orientation" "public"."metric_orientation_enum" NOT NULL, "enabled" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_1136bb423acf02b4e7e5c909d0c" UNIQUE ("identifier"), CONSTRAINT "UQ_1136bb423acf02b4e7e5c909d0c" UNIQUE ("identifier"), CONSTRAINT "PK_7d24c075ea2926dd32bd1c534ce" PRIMARY KEY ("id"))`); await queryRunner.query(`CREATE TYPE "public"."eligibility_criteria_eligibilitytype_enum" AS ENUM('linear')`); await queryRunner.query(`CREATE TABLE "eligibility_criteria" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloPoolId" character varying NOT NULL, "eligibilityType" "public"."eligibility_criteria_eligibilitytype_enum" NOT NULL, "data" json NOT NULL, "poolId" integer NOT NULL, CONSTRAINT "UQ_6120ad406cc5a00db622f3b0c97" UNIQUE ("poolId"), CONSTRAINT "PK_231ea7a8a87bb6092eb3af1c5a8" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "pool" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloPoolId" character varying NOT NULL, "finalized" boolean NOT NULL DEFAULT false, "distribution" text, CONSTRAINT "UQ_72fcaa655b2b7348f4feaf25ea3" UNIQUE ("chainId", "alloPoolId"), CONSTRAINT "PK_db1bfe411e1516c01120b85f8fe" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "pool" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloPoolId" character varying NOT NULL, "distribution" text, CONSTRAINT "UQ_72fcaa655b2b7348f4feaf25ea3" UNIQUE ("chainId", "alloPoolId"), CONSTRAINT "PK_db1bfe411e1516c01120b85f8fe" PRIMARY KEY ("id"))`); await queryRunner.query(`CREATE TABLE "vote" ("id" SERIAL NOT NULL, "voter" character varying(42) NOT NULL, "alloPoolId" character varying NOT NULL, "chainId" integer NOT NULL, "ballot" text NOT NULL, "poolId" integer NOT NULL, CONSTRAINT "UQ_3940f20660f872bfe5386def7f1" UNIQUE ("poolId", "voter"), CONSTRAINT "PK_2d5932d46afe39c8176f9d4be72" PRIMARY KEY ("id"))`); await queryRunner.query(`ALTER TABLE "application" ADD CONSTRAINT "FK_a2d1c7a2c6ee681b42112d41284" FOREIGN KEY ("poolId") REFERENCES "pool"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE "vote" ADD CONSTRAINT "FK_86b9c0ae3057aa451170728b2bb" FOREIGN KEY ("poolId") REFERENCES "pool"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); diff --git a/src/routes/metricRoutes.ts b/src/routes/metricRoutes.ts index 644bd62..854b27c 100644 --- a/src/routes/metricRoutes.ts +++ b/src/routes/metricRoutes.ts @@ -19,6 +19,10 @@ const router = Router(); * items: * type: object * properties: + * identifier: + * type: string + * description: Identifier of the metric + * example: "userEngagement" * name: * type: string * description: Name of the metric @@ -37,6 +41,7 @@ const router = Router(); * description: Whether the metric is active * example: true * required: + * - identifier * - name * - description * - orientation diff --git a/src/routes/poolRoutes.ts b/src/routes/poolRoutes.ts index 8cbcf2b..23a0c2b 100644 --- a/src/routes/poolRoutes.ts +++ b/src/routes/poolRoutes.ts @@ -2,7 +2,6 @@ import { createPool, syncPool, calculateDistribution, - finalizeDistribution, } from '@/controllers/poolController'; import { Router } from 'express'; @@ -118,29 +117,4 @@ router.post('/sync', syncPool); */ router.post('/calculate', calculateDistribution); -/** - * @swagger - * /pool/finalize: - * post: - * tags: - * - pool - * summary: Finalizes the distribution of a pool based on chainId and alloPoolId - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * alloPoolId: - * type: string - * description: The ID of the pool to finalize - * example: "609" # Example of poolId - * chainId: - * type: number - * description: The chain ID associated with the pool - * example: 42161 # Example of chainId (Arbitrum) - */ -router.post('/finalize', finalizeDistribution); - export default router; diff --git a/src/routes/voteRoutes.ts b/src/routes/voteRoutes.ts index 9ba0da7..8974fd5 100644 --- a/src/routes/voteRoutes.ts +++ b/src/routes/voteRoutes.ts @@ -21,6 +21,10 @@ const router = Router(); * type: string * description: Address of the voter * example: "0x1234567890abcdef1234567890abcdef12345678" + * signature: + * type: string + * description: Signature of the voter + * example: "0xdeadbeef" * alloPoolId: * type: string * description: The ID of the pool (from Allo) to which the vote is submitted @@ -35,19 +39,20 @@ const router = Router(); * items: * type: object * properties: - * metricId: - * type: number - * description: ID of the metric + * metricIdentifier: + * type: string + * description: Metric identifier * voteShare: * type: number * description: Vote share percentage allocated to the metric * example: - * - metricId: 1 - * voteShare: 50 - * - metricId: 2 + * - metricId: "twitterAge" + * voteShare: 150 + * - metricId: "githubFollowers" * voteShare: 50 * required: * - voter + * - signature * - alloPoolId * - chainId * - ballot diff --git a/src/service/PoolService.ts b/src/service/PoolService.ts index bd02d92..e508b82 100644 --- a/src/service/PoolService.ts +++ b/src/service/PoolService.ts @@ -84,19 +84,6 @@ class PoolService { pool.distribution = distribution; return await this.savePool(pool); } - - async finalizePoolDistribution( - alloPoolId: string, - chainId: number - ): Promise { - const pool = await this.getPoolByChainIdAndAlloPoolId(chainId, alloPoolId); - if (pool == null) { - throw new NotFoundError('Pool not found'); - } - - pool.finalized = true; - return await this.savePool(pool); - } } const poolService = new PoolService(); diff --git a/src/utils/calculate.ts b/src/utils/calculate.ts index 798ed84..5196425 100644 --- a/src/utils/calculate.ts +++ b/src/utils/calculate.ts @@ -1,13 +1,13 @@ import { type Metric } from '@/entity/Metric'; import { type Distribution } from '@/entity/Pool'; import { type Vote } from '@/entity/Vote'; -import { NotFoundError } from '@/errors'; +import { ActionNotAllowedError, NotFoundError } from '@/errors'; import { indexerClient, Status } from '@/ext/indexer'; import poolService from '@/service/PoolService'; interface MetricFetcherResponse { alloApplicationId: string; - metricName: string; + metricIdentifier: string; metricScore: number; } @@ -16,37 +16,37 @@ const getHardcodedVotes = (): Array> => { return [ { ballot: [ - { metricName: 'twitterAccountAge', metricId: 1, voteShare: 50 }, - { metricName: 'gasFees', metricId: 2, voteShare: 30 }, - { metricName: 'userEngagement', metricId: 3, voteShare: 20 }, + { metricIdentifier: 'twitterAccountAge', voteShare: 50 }, + { metricIdentifier: 'gasFees', voteShare: 30 }, + { metricIdentifier: 'userEngagement', voteShare: 20 }, ], }, { ballot: [ - { metricName: 'twitterAccountAge', metricId: 1, voteShare: 40 }, - { metricName: 'gasFees', metricId: 2, voteShare: 50 }, - { metricName: 'userEngagement', metricId: 3, voteShare: 10 }, + { metricIdentifier: 'twitterAccountAge', voteShare: 40 }, + { metricIdentifier: 'gasFees', voteShare: 50 }, + { metricIdentifier: 'userEngagement', voteShare: 10 }, ], }, { ballot: [ - { metricName: 'twitterAccountAge', metricId: 1, voteShare: 60 }, - { metricName: 'gasFees', metricId: 2, voteShare: 20 }, - { metricName: 'userEngagement', metricId: 3, voteShare: 20 }, + { metricIdentifier: 'twitterAccountAge', voteShare: 60 }, + { metricIdentifier: 'gasFees', voteShare: 20 }, + { metricIdentifier: 'userEngagement', voteShare: 20 }, ], }, { ballot: [ - { metricName: 'twitterAccountAge', metricId: 1, voteShare: 30 }, - { metricName: 'gasFees', metricId: 2, voteShare: 60 }, - { metricName: 'userEngagement', metricId: 3, voteShare: 10 }, + { metricIdentifier: 'twitterAccountAge', voteShare: 30 }, + { metricIdentifier: 'gasFees', voteShare: 60 }, + { metricIdentifier: 'userEngagement', voteShare: 10 }, ], }, { ballot: [ - { metricName: 'twitterAccountAge', metricId: 1, voteShare: 20 }, - { metricName: 'gasFees', metricId: 2, voteShare: 30 }, - { metricName: 'userEngagement', metricId: 3, voteShare: 50 }, + { metricIdentifier: 'twitterAccountAge', voteShare: 20 }, + { metricIdentifier: 'gasFees', voteShare: 30 }, + { metricIdentifier: 'userEngagement', voteShare: 50 }, ], }, ]; @@ -55,16 +55,21 @@ const getHardcodedVotes = (): Array> => { const getApprovedAlloApplicationIds = async ( alloPoolId: string, chainId: number -): Promise => { +): Promise<[boolean, string[]]> => { const indexerPoolData = await indexerClient.getRoundWithApplications({ chainId, roundId: alloPoolId, }); - return ( + + // TODO: Implement this from indexer + const isFinalised = false; + + return [ + isFinalised, indexerPoolData?.applications .filter(application => application.status === Status.APPROVED) - .map(application => application.id) ?? [] - ); + .map(application => application.id) ?? [], + ]; }; // TODO: Implement the gr8LucasMetricFetcher function to fetch metrics from the external endpoint @@ -76,57 +81,57 @@ const gr8LucasMetricFetcher = async ( return [ { alloApplicationId: 'app1', - metricName: 'twitterAccountAge', + metricIdentifier: 'twitterAccountAge', metricScore: 2, }, - { alloApplicationId: 'app1', metricName: 'gasFees', metricScore: 30 }, + { alloApplicationId: 'app1', metricIdentifier: 'gasFees', metricScore: 30 }, { alloApplicationId: 'app1', - metricName: 'userEngagement', + metricIdentifier: 'userEngagement', metricScore: 0.5, }, { alloApplicationId: 'app2', - metricName: 'twitterAccountAge', + metricIdentifier: 'twitterAccountAge', metricScore: 1, }, - { alloApplicationId: 'app2', metricName: 'gasFees', metricScore: 20 }, + { alloApplicationId: 'app2', metricIdentifier: 'gasFees', metricScore: 20 }, { alloApplicationId: 'app2', - metricName: 'userEngagement', + metricIdentifier: 'userEngagement', metricScore: 0.7, }, { alloApplicationId: 'app3', - metricName: 'twitterAccountAge', + metricIdentifier: 'twitterAccountAge', metricScore: 3, }, - { alloApplicationId: 'app3', metricName: 'gasFees', metricScore: 40 }, + { alloApplicationId: 'app3', metricIdentifier: 'gasFees', metricScore: 40 }, { alloApplicationId: 'app3', - metricName: 'userEngagement', + metricIdentifier: 'userEngagement', metricScore: 0.4, }, { alloApplicationId: 'app4', - metricName: 'twitterAccountAge', + metricIdentifier: 'twitterAccountAge', metricScore: 0.5, }, - { alloApplicationId: 'app4', metricName: 'gasFees', metricScore: 10 }, + { alloApplicationId: 'app4', metricIdentifier: 'gasFees', metricScore: 10 }, { alloApplicationId: 'app4', - metricName: 'userEngagement', + metricIdentifier: 'userEngagement', metricScore: 0.6, }, { alloApplicationId: 'app5', - metricName: 'twitterAccountAge', + metricIdentifier: 'twitterAccountAge', metricScore: 4, }, - { alloApplicationId: 'app5', metricName: 'gasFees', metricScore: 50 }, + { alloApplicationId: 'app5', metricIdentifier: 'gasFees', metricScore: 50 }, { alloApplicationId: 'app5', - metricName: 'userEngagement', + metricIdentifier: 'userEngagement', metricScore: 0.2, }, ]; @@ -142,8 +147,11 @@ const fetchVotes = async ( }; // Function to determine if a metric is increasing or decreasing -const isMetricIncreasing = (metrics: Metric[], metricName: string): boolean => { - const metric = metrics.find(metric => metric.name === metricName); +const isMetricIncreasing = ( + metrics: Metric[], + metricIdentifier: string +): boolean => { + const metric = metrics.find(metric => metric.name === metricIdentifier); if (metric == null) { throw new NotFoundError(`Metric not found`); } @@ -184,10 +192,12 @@ export const calculate = async ( if (unAccountedBallots != null) votes.push(unAccountedBallots); // Fetch approved allo application ids - const approvedAlloApplicationIds = await getApprovedAlloApplicationIds( - alloPoolId, - chainId - ); + const [isFinalised, approvedAlloApplicationIds] = + await getApprovedAlloApplicationIds(alloPoolId, chainId); + + if (isFinalised) { + throw new ActionNotAllowedError('Pool is finalised'); + } // Fetch metrics from the external endpoint const fetchedApplicaionMetricScores = await gr8LucasMetricFetcher( @@ -203,19 +213,19 @@ export const calculate = async ( // Precompute min and max values for each metric const metricBounds: Record = {}; - applicationToMetricsScores.forEach(({ metricName, metricScore }) => { - if (metricBounds[metricName] == null) { - metricBounds[metricName] = { + applicationToMetricsScores.forEach(({ metricIdentifier, metricScore }) => { + if (metricBounds[metricIdentifier] == null) { + metricBounds[metricIdentifier] = { minValue: metricScore, maxValue: metricScore, }; } else { - metricBounds[metricName].minValue = Math.min( - metricBounds[metricName].minValue, + metricBounds[metricIdentifier].minValue = Math.min( + metricBounds[metricIdentifier].minValue, metricScore ); - metricBounds[metricName].maxValue = Math.max( - metricBounds[metricName].maxValue, + metricBounds[metricIdentifier].maxValue = Math.max( + metricBounds[metricIdentifier].maxValue, metricScore ); } @@ -227,26 +237,26 @@ export const calculate = async ( for (const metricScore of applicationToMetricsScores) { const { alloApplicationId, - metricName, + metricIdentifier, metricScore: rawScore, } = metricScore; // Get metric details from the pool const metricDetails = pool.metrics.find( - metric => metric.name === metricName + metric => metric.name === metricIdentifier ); if (metricDetails == null) { - throw new NotFoundError(`Metric "${metricName}" not found in pool`); + throw new NotFoundError(`Metric "${metricIdentifier}" not found in pool`); } - const { maxValue } = metricBounds[metricName]; - const isIncreasing = isMetricIncreasing(pool.metrics, metricName); + const { maxValue } = metricBounds[metricIdentifier]; + const isIncreasing = isMetricIncreasing(pool.metrics, metricIdentifier); const normalizedScore = normalizeScore(rawScore, maxValue, isIncreasing); // Get vote share for the metric const totalVoteShare = votes.reduce((sum, vote) => { const ballotItem = vote.ballot?.find( - item => item.metricName === metricName + item => item.metricIdentifier === metricIdentifier ); return ballotItem != null ? sum + ballotItem.voteShare : sum; }, 0); From fbfcb5da0c23d48049cf8908bb5bc2f3d86341bd Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Wed, 15 Jan 2025 19:27:55 +0530 Subject: [PATCH 2/3] update --- src/utils/calculate.ts | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/utils/calculate.ts b/src/utils/calculate.ts index 5196425..5a790cf 100644 --- a/src/utils/calculate.ts +++ b/src/utils/calculate.ts @@ -80,60 +80,49 @@ const gr8LucasMetricFetcher = async ( // Hardcoded object for now return [ { - alloApplicationId: 'app1', + alloApplicationId: '1', metricIdentifier: 'twitterAccountAge', metricScore: 2, }, - { alloApplicationId: 'app1', metricIdentifier: 'gasFees', metricScore: 30 }, + { alloApplicationId: '1', metricIdentifier: 'gasFees', metricScore: 30 }, { - alloApplicationId: 'app1', + alloApplicationId: '1', metricIdentifier: 'userEngagement', metricScore: 0.5, }, { - alloApplicationId: 'app2', + alloApplicationId: '2', metricIdentifier: 'twitterAccountAge', metricScore: 1, }, - { alloApplicationId: 'app2', metricIdentifier: 'gasFees', metricScore: 20 }, + { alloApplicationId: '2', metricIdentifier: 'gasFees', metricScore: 20 }, { - alloApplicationId: 'app2', + alloApplicationId: '2', metricIdentifier: 'userEngagement', metricScore: 0.7, }, { - alloApplicationId: 'app3', + alloApplicationId: '2', metricIdentifier: 'twitterAccountAge', metricScore: 3, }, - { alloApplicationId: 'app3', metricIdentifier: 'gasFees', metricScore: 40 }, + { alloApplicationId: '3', metricIdentifier: 'gasFees', metricScore: 40 }, { - alloApplicationId: 'app3', + alloApplicationId: '3', metricIdentifier: 'userEngagement', metricScore: 0.4, }, { - alloApplicationId: 'app4', + alloApplicationId: '4', metricIdentifier: 'twitterAccountAge', metricScore: 0.5, }, - { alloApplicationId: 'app4', metricIdentifier: 'gasFees', metricScore: 10 }, + { alloApplicationId: '4', metricIdentifier: 'gasFees', metricScore: 10 }, { - alloApplicationId: 'app4', + alloApplicationId: '4', metricIdentifier: 'userEngagement', metricScore: 0.6, }, - { - alloApplicationId: 'app5', - metricIdentifier: 'twitterAccountAge', - metricScore: 4, - }, - { alloApplicationId: 'app5', metricIdentifier: 'gasFees', metricScore: 50 }, - { - alloApplicationId: 'app5', - metricIdentifier: 'userEngagement', - metricScore: 0.2, - }, ]; }; From 19cba87a23e22fc3fdadac329010573014747d9f Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Wed, 15 Jan 2025 22:55:13 +0530 Subject: [PATCH 3/3] wip: fix query --- src/controllers/applicationController.ts | 7 +- src/controllers/metricController.ts | 37 ++++++-- src/controllers/poolController.ts | 72 +++++++++++----- src/controllers/voteController.ts | 20 +++-- src/entity/EligibilityCriteria.ts | 5 +- src/entity/Metric.ts | 3 +- src/entity/Pool.ts | 6 +- ...tion.ts => 1736971489047-InitMigration.ts} | 18 ++-- src/routes/applicationRoutes.ts | 4 +- src/routes/index.ts | 2 +- src/routes/metricRoutes.ts | 44 +++++++++- src/routes/poolRoutes.ts | 84 ++++++++++++++++--- src/service/EligibilityCriteriaService.ts | 41 ++++++++- src/service/MetricService.ts | 25 +++++- src/service/PoolService.ts | 33 ++++---- src/service/VoteService.ts | 6 +- src/utils/calculate.ts | 22 ++--- 17 files changed, 318 insertions(+), 111 deletions(-) rename src/migration/{1736937474029-InitMigration.ts => 1736971489047-InitMigration.ts} (82%) diff --git a/src/controllers/applicationController.ts b/src/controllers/applicationController.ts index 0190cf5..da214ad 100644 --- a/src/controllers/applicationController.ts +++ b/src/controllers/applicationController.ts @@ -5,12 +5,11 @@ import { catchError, validateRequest } from '@/utils'; import { IsNullError, NotFoundError } from '@/errors'; import { createLogger } from '@/logger'; import { indexerClient } from '@/ext/indexer'; +import { type PoolIdChainId } from './types'; const logger = createLogger(); -interface CreateApplicationRequest { - chainId: number; - alloPoolId: string; +interface CreateApplicationRequest extends PoolIdChainId { alloApplicationId: string; } @@ -70,7 +69,7 @@ export const createApplication = async ( ); // Handle errors - if (error !== null || application === null) { + if (error !== undefined || application === null) { logger.error(`Failed to create application: ${error?.message}`); res .status(500) diff --git a/src/controllers/metricController.ts b/src/controllers/metricController.ts index 7875398..55a3498 100644 --- a/src/controllers/metricController.ts +++ b/src/controllers/metricController.ts @@ -16,7 +16,7 @@ const isMetric = (obj: any): obj is Metric => { typeof obj.description === 'string' && (obj.orientation === MetricOrientation.Increase || obj.orientation === MetricOrientation.Decrease) && - typeof obj.active === 'boolean' + typeof obj.enabled === 'boolean' ); }; @@ -33,7 +33,7 @@ export const addMetrics = async ( // TODO: ensure caller is admin - const data = req.body; + const data = req.body as Metric[]; // Combined validation to check if req.body is Metric[] if (!isValidMetricsData(data)) { @@ -43,11 +43,9 @@ export const addMetrics = async ( const metricsData: Metric[] = data; - const [error, metrics] = await catchError( - metricService.saveMetrics(metricsData) - ); + const [error] = await catchError(metricService.saveMetrics(metricsData)); - if (error != null || metrics == null) { + if (error !== undefined) { logger.error(`Failed to save metrics: ${error?.message}`); res .status(500) @@ -55,6 +53,31 @@ export const addMetrics = async ( throw new IsNullError('Error saving metrics'); } - logger.info('successfully saved metrics', metrics); + logger.info('successfully saved metrics'); res.status(201).json({ message: 'Metrics saved successfully.' }); }; + +export const updateMetric = async ( + req: Request, + res: Response +): Promise => { + validateRequest(req, res); + + const identifier = req.params.identifier; + const metric = req.body as Partial; + + const [error, metrics] = await catchError( + metricService.updateMetric(identifier, metric) + ); + + if (error !== undefined) { + logger.error(`Failed to update metric: ${error?.message}`); + res + .status(500) + .json({ message: 'Error updating metric', error: error?.message }); + throw new IsNullError('Error updating metric'); + } + + logger.info('successfully updated metric', metrics); + res.status(200).json({ message: 'Metric updated successfully.' }); +}; diff --git a/src/controllers/poolController.ts b/src/controllers/poolController.ts index 5a26f6a..5998d78 100644 --- a/src/controllers/poolController.ts +++ b/src/controllers/poolController.ts @@ -16,20 +16,20 @@ import { } from '@/errors'; import { EligibilityType } from '@/entity/EligibilityCriteria'; import { calculate } from '@/utils/calculate'; +import eligibilityCriteriaService from '@/service/EligibilityCriteriaService'; +import { type PoolIdChainId } from './types'; const logger = createLogger(); -interface CreatePoolRequest { - chainId: number; - alloPoolId: string; - metricsIds: number[]; +interface CreatePoolRequest extends PoolIdChainId { + metricIdentifiers: string[]; eligibilityType: EligibilityType; eligibilityData: object; } -interface ChainIdAlloPoolIdRequest { - chainId: number; - alloPoolId: string; +interface EligibilityCriteriaRequest extends PoolIdChainId { + eligibilityType: EligibilityType; + data: object; } /** @@ -45,9 +45,13 @@ export const createPool = async ( // Validate the incoming request validateRequest(req, res); - // Extract chainId and alloPoolId from the request body - const { chainId, alloPoolId, eligibilityType, eligibilityData, metricsIds } = - req.body as CreatePoolRequest; + const { + chainId, + alloPoolId, + eligibilityType, + eligibilityData, + metricIdentifiers, + } = req.body as CreatePoolRequest; logger.info( `Received create pool request for chainId: ${chainId}, alloPoolId: ${alloPoolId}` @@ -67,25 +71,24 @@ export const createPool = async ( }) ); - if (errorFetching !== null || indexerPoolData === null) { + if (errorFetching !== undefined || indexerPoolData === null) { res.status(404).json({ message: 'Pool not found on indexer' }); throw new NotFoundError('Pool not found on indexer'); } - // Get or create the pool // Create the pool with the fetched data - const [error, pool] = await catchError( + const [error] = await catchError( poolService.createNewPool( chainId, alloPoolId, eligibilityType, eligibilityData, - metricsIds + metricIdentifiers ) ); // Handle errors during the create operation - if (error != null || pool == null) { + if (error !== undefined) { logger.error(`Failed to create pool: ${error?.message}`); res .status(500) @@ -93,7 +96,7 @@ export const createPool = async ( throw new IsNullError(`Error creating pool`); } - logger.info('successfully created pool', pool); + logger.info('successfully created pool'); res.status(200).json({ message: 'pool created successfully' }); }; @@ -108,7 +111,7 @@ export const syncPool = async (req: Request, res: Response): Promise => { validateRequest(req, res); // Extract chainId and alloPoolId from the request body - const { chainId, alloPoolId } = req.body as ChainIdAlloPoolIdRequest; + const { chainId, alloPoolId } = req.body as PoolIdChainId; // Log the receipt of the update request logger.info( @@ -124,7 +127,7 @@ export const syncPool = async (req: Request, res: Response): Promise => { ); // Handle errors or missing data from the indexer - if (errorFetching != null || indexerPoolData == null) { + if (errorFetching !== undefined || indexerPoolData == null) { logger.warn( `No pool found for chainId: ${chainId}, alloPoolId: ${alloPoolId}` ); @@ -167,7 +170,7 @@ const updateApplications = async ( * @param res - Express response object */ export const calculateDistribution = async (req, res): Promise => { - const { chainId, alloPoolId } = req.body as ChainIdAlloPoolIdRequest; + const { chainId, alloPoolId } = req.body as PoolIdChainId; const [errorFetching, distribution] = await catchError( calculate(chainId, alloPoolId) @@ -196,3 +199,34 @@ export const calculateDistribution = async (req, res): Promise => { res.status(200).json({ message: 'Distribution updated successfully' }); }; + +export const updateEligibilityCriteria = async (req, res): Promise => { + const { eligibilityType, alloPoolId, chainId, data } = + req.body as EligibilityCriteriaRequest; + + // Log the receipt of the update request + logger.info( + `Received update eligibility criteria request for chainId: ${chainId}, alloPoolId: ${alloPoolId}` + ); + + const [error] = await catchError( + eligibilityCriteriaService.saveEligibilityCriteria({ + chainId, + alloPoolId, + eligibilityType, + data, + }) + ); + + if (error !== undefined) { + logger.error(`Failed to update eligibility criteria: ${error?.message}`); + res.status(500).json({ + message: 'Error updating eligibility criteria', + error: error?.message, + }); + } + + res.status(200).json({ + message: 'Eligibility criteria updated successfully', + }); +}; diff --git a/src/controllers/voteController.ts b/src/controllers/voteController.ts index ecd9b4b..b911f10 100644 --- a/src/controllers/voteController.ts +++ b/src/controllers/voteController.ts @@ -6,12 +6,19 @@ import { BadRequestError, ServerError, UnauthorizedError } from '@/errors'; import { createLogger } from '@/logger'; import { calculate } from '@/utils/calculate'; import { type Pool } from '@/entity/Pool'; -import { type Vote } from '@/entity/Vote'; +import { type Ballot, type Vote } from '@/entity/Vote'; import eligibilityCriteriaService from '@/service/EligibilityCriteriaService'; import { type Hex } from 'viem'; import { env } from 'process'; +import { type PoolIdChainId } from './types'; const logger = createLogger(); +interface SubmitVoteRequest extends PoolIdChainId { + voter: string; + ballot: Ballot[]; + signature: Hex; +} + /** * Submits a vote for a given pool * @@ -25,7 +32,8 @@ export const submitVote = async ( // Validate the incoming request validateRequest(req, res); - const { voter, alloPoolId, chainId, ballot, signature } = req.body; + const { voter, alloPoolId, chainId, ballot, signature } = + req.body as SubmitVoteRequest; if ( typeof voter !== 'string' || typeof alloPoolId !== 'string' || @@ -55,7 +63,7 @@ export const submitVote = async ( if ( !(await checkVoterEligibility( { alloPoolId, chainId }, - signature as Hex, + signature, pool, voter )) @@ -64,7 +72,7 @@ export const submitVote = async ( throw new BadRequestError('Not Authorzied'); } - const [error, result] = await catchError( + const [error] = await catchError( voteService.saveVote({ voter, alloPoolId, @@ -75,7 +83,7 @@ export const submitVote = async ( }) ); - if (error !== null || result === null) { + if (error !== null) { res .status(500) .json({ message: 'Error submitting vote', error: error?.message }); @@ -85,7 +93,7 @@ export const submitVote = async ( // Trigger the distribution without waiting void calculate(chainId, alloPoolId); - logger.info('Vote submitted successfully', result); + logger.info('Vote submitted successfully'); res.status(201).json({ message: 'Vote submitted successfully' }); }; diff --git a/src/entity/EligibilityCriteria.ts b/src/entity/EligibilityCriteria.ts index dce21b7..f30e9dd 100644 --- a/src/entity/EligibilityCriteria.ts +++ b/src/entity/EligibilityCriteria.ts @@ -13,7 +13,7 @@ export enum EligibilityType { } @Entity() -@Unique(['poolId']) +@Unique(['chainId', 'alloPoolId']) export class EligibilityCriteria { @PrimaryGeneratedColumn() id: number; @@ -37,7 +37,4 @@ export class EligibilityCriteria { @Column('json') data: any; - - @Column() // Explicitly define the foreign key column for pool - poolId: number; } diff --git a/src/entity/Metric.ts b/src/entity/Metric.ts index 4c688a5..86ba9a0 100644 --- a/src/entity/Metric.ts +++ b/src/entity/Metric.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; export enum MetricOrientation { Increase = 'increase', @@ -6,7 +6,6 @@ export enum MetricOrientation { } @Entity() -@Unique(['identifier']) export class Metric { @PrimaryGeneratedColumn() id: number; diff --git a/src/entity/Pool.ts b/src/entity/Pool.ts index 3e62a11..67e5580 100644 --- a/src/entity/Pool.ts +++ b/src/entity/Pool.ts @@ -5,10 +5,8 @@ import { OneToMany, Unique, OneToOne, - ManyToMany, } from 'typeorm'; import { Application } from '@/entity/Application'; -import { Metric } from './Metric'; import { EligibilityCriteria } from './EligibilityCriteria'; import { Vote } from './Vote'; @@ -38,8 +36,8 @@ export class Pool { @OneToMany(() => Application, application => application.pool) applications: Application[]; - @ManyToMany(() => Metric, { eager: true }) // Unidirectional relation - metrics: Metric[]; + @Column('simple-array', { nullable: true }) + metricIdentifiers: string[]; @OneToMany(() => Vote, vote => vote.pool) votes: Vote[]; diff --git a/src/migration/1736937474029-InitMigration.ts b/src/migration/1736971489047-InitMigration.ts similarity index 82% rename from src/migration/1736937474029-InitMigration.ts rename to src/migration/1736971489047-InitMigration.ts index 7fef14c..fd947df 100644 --- a/src/migration/1736937474029-InitMigration.ts +++ b/src/migration/1736971489047-InitMigration.ts @@ -1,16 +1,16 @@ import { type MigrationInterface, type QueryRunner } from "typeorm"; -export class InitMigration1736937474029 implements MigrationInterface { - name = 'InitMigration1736937474029' +export class InitMigration1736971489047 implements MigrationInterface { + name = 'InitMigration1736971489047' public async up(queryRunner: QueryRunner): Promise { 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"))`); - await queryRunner.query(`CREATE TYPE "public"."metric_orientation_enum" AS ENUM('increase', 'decrease')`); - await queryRunner.query(`CREATE TABLE "metric" ("id" SERIAL NOT NULL, "identifier" character varying NOT NULL, "name" character varying NOT NULL, "description" character varying NOT NULL, "orientation" "public"."metric_orientation_enum" NOT NULL, "enabled" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_1136bb423acf02b4e7e5c909d0c" UNIQUE ("identifier"), CONSTRAINT "UQ_1136bb423acf02b4e7e5c909d0c" UNIQUE ("identifier"), CONSTRAINT "PK_7d24c075ea2926dd32bd1c534ce" PRIMARY KEY ("id"))`); await queryRunner.query(`CREATE TYPE "public"."eligibility_criteria_eligibilitytype_enum" AS ENUM('linear')`); - await queryRunner.query(`CREATE TABLE "eligibility_criteria" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloPoolId" character varying NOT NULL, "eligibilityType" "public"."eligibility_criteria_eligibilitytype_enum" NOT NULL, "data" json NOT NULL, "poolId" integer NOT NULL, CONSTRAINT "UQ_6120ad406cc5a00db622f3b0c97" UNIQUE ("poolId"), CONSTRAINT "PK_231ea7a8a87bb6092eb3af1c5a8" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "pool" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloPoolId" character varying NOT NULL, "distribution" text, CONSTRAINT "UQ_72fcaa655b2b7348f4feaf25ea3" UNIQUE ("chainId", "alloPoolId"), CONSTRAINT "PK_db1bfe411e1516c01120b85f8fe" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "eligibility_criteria" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloPoolId" character varying NOT NULL, "eligibilityType" "public"."eligibility_criteria_eligibilitytype_enum" NOT NULL, "data" json NOT NULL, CONSTRAINT "UQ_cab3614863337cf5dba521b9b84" UNIQUE ("chainId", "alloPoolId"), CONSTRAINT "PK_231ea7a8a87bb6092eb3af1c5a8" PRIMARY KEY ("id"))`); await queryRunner.query(`CREATE TABLE "vote" ("id" SERIAL NOT NULL, "voter" character varying(42) NOT NULL, "alloPoolId" character varying NOT NULL, "chainId" integer NOT NULL, "ballot" text NOT NULL, "poolId" integer NOT NULL, CONSTRAINT "UQ_3940f20660f872bfe5386def7f1" UNIQUE ("poolId", "voter"), CONSTRAINT "PK_2d5932d46afe39c8176f9d4be72" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "pool" ("id" SERIAL NOT NULL, "chainId" integer NOT NULL, "alloPoolId" character varying NOT NULL, "metricIdentifiers" text, "distribution" text, CONSTRAINT "UQ_72fcaa655b2b7348f4feaf25ea3" UNIQUE ("chainId", "alloPoolId"), CONSTRAINT "PK_db1bfe411e1516c01120b85f8fe" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."metric_orientation_enum" AS ENUM('increase', 'decrease')`); + await queryRunner.query(`CREATE TABLE "metric" ("id" SERIAL NOT NULL, "identifier" character varying NOT NULL, "name" character varying NOT NULL, "description" character varying NOT NULL, "orientation" "public"."metric_orientation_enum" NOT NULL, "enabled" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_1136bb423acf02b4e7e5c909d0c" UNIQUE ("identifier"), CONSTRAINT "PK_7d24c075ea2926dd32bd1c534ce" PRIMARY KEY ("id"))`); await queryRunner.query(`ALTER TABLE "application" ADD CONSTRAINT "FK_a2d1c7a2c6ee681b42112d41284" FOREIGN KEY ("poolId") REFERENCES "pool"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE "vote" ADD CONSTRAINT "FK_86b9c0ae3057aa451170728b2bb" FOREIGN KEY ("poolId") REFERENCES "pool"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); } @@ -18,12 +18,12 @@ export class InitMigration1736937474029 implements MigrationInterface { public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "vote" DROP CONSTRAINT "FK_86b9c0ae3057aa451170728b2bb"`); await queryRunner.query(`ALTER TABLE "application" DROP CONSTRAINT "FK_a2d1c7a2c6ee681b42112d41284"`); - await queryRunner.query(`DROP TABLE "vote"`); + await queryRunner.query(`DROP TABLE "metric"`); + await queryRunner.query(`DROP TYPE "public"."metric_orientation_enum"`); await queryRunner.query(`DROP TABLE "pool"`); + await queryRunner.query(`DROP TABLE "vote"`); await queryRunner.query(`DROP TABLE "eligibility_criteria"`); await queryRunner.query(`DROP TYPE "public"."eligibility_criteria_eligibilitytype_enum"`); - await queryRunner.query(`DROP TABLE "metric"`); - await queryRunner.query(`DROP TYPE "public"."metric_orientation_enum"`); await queryRunner.query(`DROP TABLE "application"`); } diff --git a/src/routes/applicationRoutes.ts b/src/routes/applicationRoutes.ts index 303cef8..cf8184c 100644 --- a/src/routes/applicationRoutes.ts +++ b/src/routes/applicationRoutes.ts @@ -19,11 +19,11 @@ const router = Router(); * alloPoolId: * type: string * description: The ID of the pool to link the application to - * example: "609" # Example of poolId + * example: "11155111" # Example of poolId * chainId: * type: number * description: The chain ID associated with the pool - * example: 42161 # Example of chainId (Arbitrum) + * example: 11155111 # Example of chainId (Sepolia) * alloApplicationId: * type: string * description: The ID of the application to create diff --git a/src/routes/index.ts b/src/routes/index.ts index 1e1b0a4..aa3f6d0 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -7,7 +7,7 @@ import voteRoutes from '@/routes/voteRoutes'; const router = Router(); router.use('/application', applicationRoutes); -router.use('/pools', poolRoutes); +router.use('/pool', poolRoutes); router.use('/metrics', metricRoutes); router.use('/vote', voteRoutes); diff --git a/src/routes/metricRoutes.ts b/src/routes/metricRoutes.ts index 854b27c..6b51168 100644 --- a/src/routes/metricRoutes.ts +++ b/src/routes/metricRoutes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { addMetrics } from '@/controllers/metricController'; +import { addMetrics, updateMetric } from '@/controllers/metricController'; const router = Router(); @@ -36,16 +36,16 @@ const router = Router(); * enum: [increase, decrease] * description: Priority of the metric * example: "increase" - * active: + * enabled: * type: boolean - * description: Whether the metric is active + * description: Whether the metric is enabled * example: true * required: * - identifier * - name * - description * - orientation - * - active + * - enabled * responses: * 201: * description: Metrics added successfully @@ -56,4 +56,40 @@ const router = Router(); */ router.post('/', addMetrics); +/** + * @swagger + * /metrics/{identifier}: + * put: + * tags: + * - metrics + * summary: Updates a metric + * parameters: + * - in: path + * name: identifier + * required: true + * description: The identifier of the metric to update + * schema: + * type: string + * example: "userEngagement" + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * enabled: + * type: boolean + * description: Whether the metric is enabled + * example: true + * responses: + * 200: + * description: Metric updated successfully + * 400: + * description: Invalid input + * 500: + * description: Internal server error + */ +router.put('/:identifier', updateMetric); + export default router; diff --git a/src/routes/poolRoutes.ts b/src/routes/poolRoutes.ts index 23a0c2b..87641f2 100644 --- a/src/routes/poolRoutes.ts +++ b/src/routes/poolRoutes.ts @@ -2,6 +2,7 @@ import { createPool, syncPool, calculateDistribution, + updateEligibilityCriteria, } from '@/controllers/poolController'; import { Router } from 'express'; @@ -24,14 +25,29 @@ const router = Router(); * alloPoolId: * type: string * description: The ID of the pool to create - * example: "609" # Example of poolId + * example: "615" # Example of poolId * chainId: * type: number * description: The chain ID associated with the pool - * example: 42161 # Example of chainId (Arbitrum) + * example: 11155111 # Example of chainId (Sepolia) + * eligibilityType: + * type: string + * description: The type of eligibility to check + * example: "linear" # Example of eligibilityType + * eligibilityData: + * type: object + * description: The data for the eligibility criteria + * example: { "voters": ["0xB8cEF765721A6da910f14Be93e7684e9a3714123", "0x5645bF145C3f1E974D0D7FB91bf3c68592ab5012"] } # Example of data + * metricIdentifiers: + * type: array + * description: The identifiers of the metrics to associate with the pool + * example: ["userEngagement"] # Example of metricsIds * required: * - alloPoolId * - chainId + * - eligibilityType + * - eligibilityData + * - metricIdentifiers * responses: * 201: * description: Pool created successfully @@ -42,15 +58,18 @@ const router = Router(); * examples: * application/json: * - value: - * alloPoolId: "609" - * chainId: "42161" + * alloPoolId: "615" # Example of poolId + * chainId: "11155111" # Example of chainId (Sepolia) + * eligibilityType: "linear" + * eligibilityData: { "voters": ["0xB8cEF765721A6da910f14Be93e7684e9a3714123", "0x5645bF145C3f1E974D0D7FB91bf3c68592ab5012"] } + * metricIdentifiers: ["userEngagement"] */ router.post('/', createPool); /** * @swagger * /pool/sync: - * post: + * put: * tags: * - pool * summary: Syncs a pools applications with the given alloPoolId and chainId @@ -64,11 +83,11 @@ router.post('/', createPool); * alloPoolId: * type: string * description: The ID of the pool to sync - * example: "609" # Example of poolId + * example: "615" # Example of poolId * chainId: * type: number * description: The chain ID associated with the pool - * example: 42161 # Example of chainId (Arbitrum) + * example: 11155111 # Example of chainId (Sepolia) * required: * - alloPoolId * - chainId @@ -80,7 +99,7 @@ router.post('/', createPool); * 500: * description: Internal server error */ -router.post('/sync', syncPool); +router.put('/sync', syncPool); /** * @swagger @@ -99,11 +118,11 @@ router.post('/sync', syncPool); * alloPoolId: * type: string * description: The ID of the pool to calculate - * example: "609" # Example of poolId + * example: "615" # Example of poolId * chainId: * type: number * description: The chain ID associated with the pool - * example: 42161 # Example of chainId (Arbitrum) + * example: 11155111 # Example of chainId (Sepolia) * required: * - alloPoolId * - chainId @@ -117,4 +136,49 @@ router.post('/sync', syncPool); */ router.post('/calculate', calculateDistribution); +/** + * @swagger + * /pool/eligibility: + * put: + * tags: + * - pool + * summary: Update the eligibility of a pool + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * alloPoolId: + * type: string + * description: The ID of the pool + * example: "615" # Example of poolId + * chainId: + * type: number + * description: The chain ID associated with the pool + * example: 11155111 # Example of chainId (Sepolia) + * eligibilityType: + * type: string + * description: The type of eligibility to check + * example: "linear" # Example of eligibilityType + * data: + * type: object + * description: The data for the eligibility criteria + * example: { "voters": ["0xB8cEF765721A6da910f14Be93e7684e9a3714123", "0x5645bF145C3f1E974D0D7FB91bf3c68592ab5012"] } # Example of data + * required: + * - alloPoolId + * - chainId + * - eligibilityType + * - data + * responses: + * 200: + * description: Eligibility checked successfully + * 400: + * description: Invalid eligibilityType, poolId, or chainId format + * 500: + * description: Internal server error + */ +router.put('/eligibility', updateEligibilityCriteria); + export default router; diff --git a/src/service/EligibilityCriteriaService.ts b/src/service/EligibilityCriteriaService.ts index 3bea92b..84bee5a 100644 --- a/src/service/EligibilityCriteriaService.ts +++ b/src/service/EligibilityCriteriaService.ts @@ -2,8 +2,9 @@ import { type EligibilityCriteria, EligibilityType, } from '@/entity/EligibilityCriteria'; -import { NotFoundError } from '@/errors'; +import { BadRequestError, NotFoundError, ServerError } from '@/errors'; import { eligibilityCriteriaRepository } from '@/repository'; +import { isHex } from 'viem'; interface LinearEligibilityTypeData { voters: string[]; @@ -13,7 +14,30 @@ class EligibilityCriteriaService { async saveEligibilityCriteria( eligibilityCriteria: Partial ): Promise { - return await eligibilityCriteriaRepository.save(eligibilityCriteria); + if ( + eligibilityCriteria.chainId == null || + eligibilityCriteria.alloPoolId == null + ) { + throw new BadRequestError('Chain ID and Allo Pool ID are required'); + } + + validateEligibilityCriteriaData(eligibilityCriteria); + + const result = await eligibilityCriteriaRepository.upsert( + eligibilityCriteria, + { + conflictPaths: ['chainId', 'alloPoolId'], + } + ); + + const id = result.identifiers[0].id; + const criteria = await eligibilityCriteriaRepository.findOne({ + where: { id }, + }); + if (criteria === null) { + throw new ServerError('Unable to fetch saved eligibility criteria'); + } + return criteria; } async getEligibilityCriteriaByChainIdAndAlloPoolId( @@ -49,5 +73,18 @@ class EligibilityCriteriaService { } } +const validateEligibilityCriteriaData = ( + eligibilityCriteria: Partial +): void => { + if (eligibilityCriteria.eligibilityType === EligibilityType.Linear) { + const data = eligibilityCriteria.data as LinearEligibilityTypeData; + data.voters.forEach((voter: string) => { + if (!isHex(voter)) { + throw new BadRequestError('data must be an array of valid addresses'); + } + }); + } +}; + const eligibilityCriteriaService = new EligibilityCriteriaService(); export default eligibilityCriteriaService; diff --git a/src/service/MetricService.ts b/src/service/MetricService.ts index 3c369eb..1fcd849 100644 --- a/src/service/MetricService.ts +++ b/src/service/MetricService.ts @@ -3,9 +3,16 @@ import { metricRepository } from '@/repository'; import { In } from 'typeorm'; class MetricService { - async saveMetrics(metrics: Array>): Promise { + async saveMetrics(metrics: Metric[]): Promise { const newMetrics = metricRepository.create(metrics); - return await metricRepository.save(newMetrics); + await metricRepository.save(newMetrics); + } + + async updateMetric( + identifier: string, + metric: Partial + ): Promise { + await metricRepository.update({ identifier }, metric); } async getMetricById(id: number): Promise { @@ -26,6 +33,20 @@ class MetricService { async getMetricsByNames(names: string[]): Promise { return await metricRepository.find({ where: { name: In(names) } }); } + + async getMetricsByIdentifiers(identifiers: string[]): Promise { + return await metricRepository.find({ + where: { identifier: In(identifiers) }, + }); + } + + async getEnabledMetricsByIdentifiers( + identifiers: string[] + ): Promise { + return await metricRepository.find({ + where: { identifier: In(identifiers), enabled: true }, + }); + } } const metricService = new MetricService(); diff --git a/src/service/PoolService.ts b/src/service/PoolService.ts index e508b82..289d6c6 100644 --- a/src/service/PoolService.ts +++ b/src/service/PoolService.ts @@ -19,10 +19,9 @@ class PoolService { chainId: number, alloPoolId: string ): Promise { - const pool = await poolRepository.findOne({ + return await poolRepository.findOne({ where: { chainId, alloPoolId }, }); - return pool; } async createNewPool( @@ -30,18 +29,14 @@ class PoolService { alloPoolId: string, eligibilityType: EligibilityType, eligibilityData: object, - metricsIds: number[] - ): Promise { - let eligibilityCriteria = - await eligibilityCriteriaService.getEligibilityCriteriaByChainIdAndAlloPoolId( - chainId, - alloPoolId - ); - if (eligibilityCriteria != null) { - throw new AlreadyExistsError(`Eligibility criteria already exists`); + metricIdentifiers: string[] + ): Promise { + const _pool = await this.getPoolByChainIdAndAlloPoolId(chainId, alloPoolId); + if (_pool !== null) { + throw new AlreadyExistsError(`Pool already exists`); } - eligibilityCriteria = + const eligibilityCriteria = await eligibilityCriteriaService.saveEligibilityCriteria({ chainId, alloPoolId, @@ -49,18 +44,18 @@ class PoolService { data: eligibilityData, }); - const _pool = await this.getPoolByChainIdAndAlloPoolId(chainId, alloPoolId); - if (_pool != null) { - throw new AlreadyExistsError(`Pool already exists`); - } + const metrics = + await metricService.getEnabledMetricsByIdentifiers(metricIdentifiers); - const metrics = await metricService.getMetricsByIds(metricsIds); + if (metrics.length !== metricIdentifiers.length) { + throw new NotFoundError('Metrics not found/enabled'); + } - return await this.savePool({ + await this.savePool({ chainId, alloPoolId, eligibilityCriteria, - metrics, + metricIdentifiers, }); } diff --git a/src/service/VoteService.ts b/src/service/VoteService.ts index 3301e38..c89380b 100644 --- a/src/service/VoteService.ts +++ b/src/service/VoteService.ts @@ -2,7 +2,7 @@ import { type Vote } from '@/entity/Vote'; import { voteRepository } from '@/repository'; class VoteService { - async saveVote(voteData: Partial): Promise { + async saveVote(voteData: Partial): Promise { // Check if vote already exists for the voter and pool const existingVote = await voteRepository.findOne({ where: { @@ -14,12 +14,12 @@ class VoteService { if (existingVote !== null) { // Update existing vote voteRepository.merge(existingVote, voteData); - return await voteRepository.save(existingVote); + await voteRepository.save(existingVote); } // Create new vote const newVote = voteRepository.create(voteData); - return await voteRepository.save(newVote); + await voteRepository.save(newVote); } async getVotesByChainIdAndAlloPoolId( diff --git a/src/utils/calculate.ts b/src/utils/calculate.ts index 5a790cf..1353674 100644 --- a/src/utils/calculate.ts +++ b/src/utils/calculate.ts @@ -1,8 +1,8 @@ -import { type Metric } from '@/entity/Metric'; import { type Distribution } from '@/entity/Pool'; import { type Vote } from '@/entity/Vote'; import { ActionNotAllowedError, NotFoundError } from '@/errors'; import { indexerClient, Status } from '@/ext/indexer'; +import metricService from '@/service/MetricService'; import poolService from '@/service/PoolService'; interface MetricFetcherResponse { @@ -136,15 +136,14 @@ const fetchVotes = async ( }; // Function to determine if a metric is increasing or decreasing -const isMetricIncreasing = ( - metrics: Metric[], - metricIdentifier: string -): boolean => { - const metric = metrics.find(metric => metric.name === metricIdentifier); +const isMetricIncreasing = (metricIdentifier: string): boolean => { + const metric = metricService.getEnabledMetricsByIdentifiers([ + metricIdentifier, + ]); if (metric == null) { throw new NotFoundError(`Metric not found`); } - return metric.orientation === 'increase'; + return metric[0].orientation === 'increase'; }; // Function to normalize the score @@ -178,7 +177,7 @@ export const calculate = async ( const votes = await fetchVotes(chainId, alloPoolId); // Add unAccountedBallots to the votes - if (unAccountedBallots != null) votes.push(unAccountedBallots); + if (unAccountedBallots !== undefined) votes.push(unAccountedBallots); // Fetch approved allo application ids const [isFinalised, approvedAlloApplicationIds] = @@ -231,15 +230,12 @@ export const calculate = async ( } = metricScore; // Get metric details from the pool - const metricDetails = pool.metrics.find( - metric => metric.name === metricIdentifier - ); - if (metricDetails == null) { + if (!pool.metricIdentifiers.includes(metricIdentifier)) { throw new NotFoundError(`Metric "${metricIdentifier}" not found in pool`); } const { maxValue } = metricBounds[metricIdentifier]; - const isIncreasing = isMetricIncreasing(pool.metrics, metricIdentifier); + const isIncreasing = isMetricIncreasing(metricIdentifier); const normalizedScore = normalizeScore(rawScore, maxValue, isIncreasing); // Get vote share for the metric