Skip to content

Commit

Permalink
Feature cluster matching (#1862)
Browse files Browse the repository at this point in the history
* add cluster matching entity

* add cluster matching adapters

* finish cocm adapter

* improve error handling for cluster matching

* comment broken contract tests by missing eth_getCode Method

* add feedback to handle qf cases

* add cluster matching job to bootstrap file

* fix coderabbit feedback PR

* termine worker if an exception is raised
  • Loading branch information
CarlosQ96 committed Jan 9, 2025
1 parent 09186cd commit b558266
Show file tree
Hide file tree
Showing 17 changed files with 411 additions and 21 deletions.
34 changes: 34 additions & 0 deletions migration/1728554628004-AddEstimatedClusterMatching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddEstimatedClusterMatching1728554628004
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE estimated_cluster_matching (
id SERIAL PRIMARY KEY,
"projectId" INT NOT NULL,
"qfRoundId" INT NOT NULL,
matching DOUBLE PRECISION NOT NULL
);
`);

// Create indexes on the new table
await queryRunner.query(`
CREATE INDEX estimated_cluster_matching_project_id_qfround_id
ON estimated_cluster_matching ("projectId", "qfRoundId");
`);

await queryRunner.query(`
CREATE INDEX estimated_cluster_matching_matching
ON estimated_cluster_matching (matching);
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Revert changes if necessary by dropping the table and restoring the view
await queryRunner.query(`
DROP TABLE IF EXISTS estimated_cluster_matching;
`);
}
}
17 changes: 17 additions & 0 deletions src/adapters/adaptersFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { DonationSaveBackupMockAdapter } from './donationSaveBackup/DonationSave
import { SuperFluidAdapter } from './superFluid/superFluidAdapter';
import { SuperFluidMockAdapter } from './superFluid/superFluidMockAdapter';
import { SuperFluidAdapterInterface } from './superFluid/superFluidAdapterInterface';
import { CocmAdapter } from './cocmAdapter/cocmAdapter';
import { CocmMockAdapter } from './cocmAdapter/cocmMockAdapter';
import { CocmAdapterInterface } from './cocmAdapter/cocmAdapterInterface';

const discordAdapter = new DiscordAdapter();
const googleAdapter = new GoogleAdapter();
Expand Down Expand Up @@ -147,3 +150,17 @@ export const getSuperFluidAdapter = (): SuperFluidAdapterInterface => {
return superFluidMockAdapter;
}
};

const clusterMatchingAdapter = new CocmAdapter();
const clusterMatchingMockAdapter = new CocmMockAdapter();

export const getClusterMatchingAdapter = (): CocmAdapterInterface => {
switch (process.env.CLUSTER_MATCHING_ADAPTER) {
case 'clusterMatching':
return clusterMatchingAdapter;
case 'mock':
return clusterMatchingMockAdapter;
default:
return clusterMatchingMockAdapter;
}
};
46 changes: 46 additions & 0 deletions src/adapters/cocmAdapter/cocmAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import axios from 'axios';
import {
CocmAdapterInterface,
EstimatedMatchingInput,
ProjectsEstimatedMatchings,
} from './cocmAdapterInterface';
import { logger } from '../../utils/logger';
import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages';

export class CocmAdapter implements CocmAdapterInterface {
private ClusterMatchingURL;

constructor() {
this.ClusterMatchingURL =
process.env.CLUSTER_MATCHING_API_URL || 'localhost';
}

async fetchEstimatedClusterMatchings(
matchingDataInput: EstimatedMatchingInput,
): Promise<ProjectsEstimatedMatchings> {
try {
const result = await axios.post(
this.ClusterMatchingURL,
matchingDataInput,
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
);
if (result?.data?.error !== null) {
logger.error('clusterMatchingApi error', result.data.error);
throw new Error(
i18n.__(translationErrorMessagesKeys.CLUSTER_MATCHING_API_ERROR),
);
}
return result.data;
} catch (e) {
logger.error('clusterMatchingApi error', e);
throw new Error(
i18n.__(translationErrorMessagesKeys.CLUSTER_MATCHING_API_ERROR),
);
}
}
}
49 changes: 49 additions & 0 deletions src/adapters/cocmAdapter/cocmAdapterInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Example Data
// {
// "matching_data": [
// {
// "matching_amount": 83.25,
// "matching_percent": 50.0,
// "project_name": "Test1",
// "strategy": "COCM"
// },
// {
// "matching_amount": 83.25,
// "matching_percent": 50.0,
// "project_name": "Test3",
// "strategy": "COCM"
// }
// ]
// }

export interface ProjectsEstimatedMatchings {
matching_data: {
matching_amount: number;
matching_percent: number;
project_name: string;
strategy: string;
}[];
}

export interface EstimatedMatchingInput {
votes_data: [
{
voter: string;
payoutAddress: string;
amountUSD: number;
project_name: string;
score: number;
},
];
strategy: string;
min_donation_threshold_amount: number;
matching_cap_amount: number;
matching_amount: number;
passport_threshold: number;
}

export interface CocmAdapterInterface {
fetchEstimatedClusterMatchings(
matchingDataInput: EstimatedMatchingInput,
): Promise<ProjectsEstimatedMatchings>;
}
27 changes: 27 additions & 0 deletions src/adapters/cocmAdapter/cocmMockAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
CocmAdapterInterface,
ProjectsEstimatedMatchings,
} from './cocmAdapterInterface';

export class CocmMockAdapter implements CocmAdapterInterface {
async fetchEstimatedClusterMatchings(
_matchingDataInput,
): Promise<ProjectsEstimatedMatchings> {
return {
matching_data: [
{
matching_amount: 83.25,
matching_percent: 50.0,
project_name: 'Test1',
strategy: 'COCM',
},
{
matching_amount: 83.25,
matching_percent: 50.0,
project_name: 'Test3',
strategy: 'COCM',
},
],
};
}
}
2 changes: 2 additions & 0 deletions src/entities/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { ProjectSocialMedia } from './projectSocialMedia';
import { DraftRecurringDonation } from './draftRecurringDonation';
import { UserQfRoundModelScore } from './userQfRoundModelScore';
import { ProjectGivbackRankView } from './ProjectGivbackRankView';
import { EstimatedClusterMatching } from './estimatedClusterMatching';

export const getEntities = (): DataSourceOptions['entities'] => {
return [
Expand Down Expand Up @@ -86,6 +87,7 @@ export const getEntities = (): DataSourceOptions['entities'] => {
PowerSnapshot,
PowerBalanceSnapshot,
PowerBoostingSnapshot,
EstimatedClusterMatching,

// View
UserProjectPowerView,
Expand Down
41 changes: 41 additions & 0 deletions src/entities/estimatedClusterMatching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Field, ObjectType } from 'type-graphql';
import {
Column,
Index,
PrimaryGeneratedColumn,
BaseEntity,
Entity,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Project } from './project';

@Entity('estimated_cluster_matching')
@Index('estimated_cluster_matching_project_id_qfround_id', [
'projectId',
'qfRoundId',
])
@Index('estimated_cluster_matching_matching', ['matching'])
@ObjectType()
export class EstimatedClusterMatching extends BaseEntity {
@Field()
@PrimaryGeneratedColumn()
id: number; // New primary key

@Field(_type => Project)
@ManyToOne(_type => Project, project => project.projectEstimatedMatchingView)
@JoinColumn({ referencedColumnName: 'id' })
project: Project;

@Field()
@Column()
projectId: number;

@Field()
@Column()
qfRoundId: number;

@Field()
@Column('double precision')
matching: number;
}
35 changes: 29 additions & 6 deletions src/entities/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ import { FeaturedUpdate } from './featuredUpdate';
import { getHtmlTextSummary } from '../utils/utils';
import { QfRound } from './qfRound';
import {
getQfRoundTotalSqrtRootSumSquared,
getProjectDonationsSqrtRootSum,
findActiveQfRound,
getProjectDonationsSqrtRootSum,
getQfRoundTotalSqrtRootSumSquared,
} from '../repositories/qfRoundRepository';
import { EstimatedMatching } from '../types/qfTypes';
import { Campaign } from './campaign';
import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView';
import { AnchorContractAddress } from './anchorContractAddress';
import { ProjectSocialMedia } from './projectSocialMedia';
import { EstimatedClusterMatching } from './estimatedClusterMatching';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const moment = require('moment');
Expand Down Expand Up @@ -501,9 +502,10 @@ export class Project extends BaseEntity {
async estimatedMatching(): Promise<EstimatedMatching | null> {
const activeQfRound = await findActiveQfRound();
if (!activeQfRound) {
// TODO should move it to materialized view
return null;
}
const matchingPool = activeQfRound.allocatedFund;

const projectDonationsSqrtRootSum = await getProjectDonationsSqrtRootSum(
this.id,
activeQfRound.id,
Expand All @@ -513,12 +515,33 @@ export class Project extends BaseEntity {
activeQfRound.id,
);

const matchingPool = activeQfRound.allocatedFund;
const estimatedClusterMatching =
await EstimatedClusterMatching.createQueryBuilder(
'estimated_cluster_matching',
)
.where('estimated_cluster_matching."projectId" = :projectId', {
projectId: this.id,
})
.andWhere('estimated_cluster_matching."qfRoundId" = :qfRoundId', {
qfRoundId: activeQfRound.id,
})
.getOne();

let matching: number;
if (!estimatedClusterMatching) matching = 0;

if (!estimatedClusterMatching) {
matching = 0;
} else {
matching = estimatedClusterMatching.matching;
}

// Facilitate migration in frontend return empty values for now
return {
projectDonationsSqrtRootSum,
allProjectsSum,
projectDonationsSqrtRootSum: projectDonationsSqrtRootSum,
allProjectsSum: allProjectsSum,
matchingPool,
matching,
};
}

Expand Down
22 changes: 22 additions & 0 deletions src/repositories/donationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ import { ORGANIZATION_LABELS } from '../entities/organization';
import { AppDataSource } from '../orm';
import { getPowerRound } from './powerRoundRepository';

export const exportClusterMatchingDonationsFormat = async (
qfRoundId: number,
) => {
return await Donation.query(
`
SELECT
d."fromWalletAddress" AS voter,
d."toWalletAddress" AS "payoutAddress",
d."valueUsd" AS "amountUSD",
p."title" AS "project_name",
d."qfRoundUserScore" AS score
FROM
donation d
INNER JOIN
project p ON d."projectId" = p."id"
WHERE
d."qfRoundId" = $1
`,
[qfRoundId],
);
};

export const fillQfRoundDonationsUserScores = async (): Promise<void> => {
await Donation.query(`
UPDATE donation
Expand Down
4 changes: 3 additions & 1 deletion src/resolvers/projectResolver.allProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ import { InstantPowerBalance } from '../entities/instantPowerBalance';
import { saveOrUpdateInstantPowerBalances } from '../repositories/instantBoostingRepository';
import { updateInstantBoosting } from '../services/instantBoostingServices';
import { QfRound } from '../entities/qfRound';
import { calculateEstimatedMatchingWithParams } from '../utils/qfUtils';
// import { calculateEstimatedMatchingWithParams } from '../utils/qfUtils';
import { refreshProjectEstimatedMatchingView } from '../services/projectViewsService';
import { addOrUpdatePowerSnapshotBalances } from '../repositories/powerBalanceSnapshotRepository';
import { findPowerSnapshots } from '../repositories/powerSnapshotRepository';
import { ChainType } from '../types/network';
import { ORGANIZATION_LABELS } from '../entities/organization';
import { calculateEstimatedMatchingWithParams } from '../utils/qfUtils';

// search and filters
describe('all projects test cases --->', allProjectsTestCases);
Expand Down Expand Up @@ -2215,6 +2216,7 @@ function allProjectsTestCases() {
p => Number(p.id) === project2.id,
);

// New estimated matching wont calculate it here
const project1EstimatedMatching =
await calculateEstimatedMatchingWithParams({
matchingPool: firstProject.estimatedMatching.matchingPool,
Expand Down
Loading

0 comments on commit b558266

Please sign in to comment.