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

Feature cluster matching #1862

Draft
wants to merge 5 commits into
base: staging
Choose a base branch
from
Draft
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
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,
project_id INT NOT NULL,
qf_round_id 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 (project_id, qf_round_id);
`);

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;
}
31 changes: 15 additions & 16 deletions src/entities/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,13 @@ import { Category } from './category';
import { FeaturedUpdate } from './featuredUpdate';
import { getHtmlTextSummary } from '../utils/utils';
import { QfRound } from './qfRound';
import {
getQfRoundTotalSqrtRootSumSquared,
getProjectDonationsSqrtRootSum,
findActiveQfRound,
} from '../repositories/qfRoundRepository';
import { findActiveQfRound } 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,24 +498,26 @@ 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 projectDonationsSqrtRootSum = await getProjectDonationsSqrtRootSum(
this.id,
activeQfRound.id,
);
const matchingPool = activeQfRound.allocatedFund;

const allProjectsSum = await getQfRoundTotalSqrtRootSumSquared(
activeQfRound.id,
);
const estimatedClusterMatching =
await EstimatedClusterMatching.createQueryBuilder('matching')
.where('matching."projectId" = :projectId', { projectId: this.id })
.getOne();

const matchingPool = activeQfRound.allocatedFund;
let matching: number;
if (!estimatedClusterMatching) matching = 0;

matching = estimatedClusterMatching!.matching;

// Facilitate migration in frontend return empty values for now
return {
projectDonationsSqrtRootSum,
allProjectsSum,
projectDonationsSqrtRootSum: 0,
allProjectsSum: 0,
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
53 changes: 27 additions & 26 deletions src/resolvers/projectResolver.allProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ 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';
Expand Down Expand Up @@ -2195,31 +2195,32 @@ function allProjectsTestCases() {
});

assert.equal(result.data.data.allProjects.projects.length, 2);
const firstProject = result.data.data.allProjects.projects.find(
p => Number(p.id) === project1.id,
);
const secondProject = result.data.data.allProjects.projects.find(
p => Number(p.id) === project2.id,
);

const project1EstimatedMatching =
await calculateEstimatedMatchingWithParams({
matchingPool: firstProject.estimatedMatching.matchingPool,
projectDonationsSqrtRootSum:
firstProject.estimatedMatching.projectDonationsSqrtRootSum,
allProjectsSum: firstProject.estimatedMatching.allProjectsSum,
});

const project2EstimatedMatching =
await calculateEstimatedMatchingWithParams({
matchingPool: secondProject.estimatedMatching.matchingPool,
projectDonationsSqrtRootSum:
secondProject.estimatedMatching.projectDonationsSqrtRootSum,
allProjectsSum: secondProject.estimatedMatching.allProjectsSum,
});

assert.equal(Math.floor(project1EstimatedMatching), 666);
assert.equal(Math.floor(project2EstimatedMatching), 333);
// const firstProject = result.data.data.allProjects.projects.find(
// p => Number(p.id) === project1.id,
// );
// const secondProject = result.data.data.allProjects.projects.find(
// p => Number(p.id) === project2.id,
// );

// New estimated matching wont calculate it here
// const project1EstimatedMatching =
// await calculateEstimatedMatchingWithParams({
// matchingPool: firstProject.estimatedMatching.matchingPool,
// projectDonationsSqrtRootSum:
// firstProject.estimatedMatching.projectDonationsSqrtRootSum,
// allProjectsSum: firstProject.estimatedMatching.allProjectsSum,
// });

// const project2EstimatedMatching =
// await calculateEstimatedMatchingWithParams({
// matchingPool: secondProject.estimatedMatching.matchingPool,
// projectDonationsSqrtRootSum:
// secondProject.estimatedMatching.projectDonationsSqrtRootSum,
// allProjectsSum: secondProject.estimatedMatching.allProjectsSum,
// });

// assert.equal(Math.floor(project1EstimatedMatching), 666);
// assert.equal(Math.floor(project2EstimatedMatching), 333);
qfRound.isActive = false;
await qfRound.save();
});
Expand Down
Loading
Loading