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

Add matching funds as donations #1189

Merged
merged 10 commits into from
Dec 5, 2023
3 changes: 3 additions & 0 deletions config/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,7 @@ MORDOR_ETC_TESTNET_SCAN_API_URL=https://etc-mordor.blockscout.com/api/v1
# https://chainlist.org/chain/63
MORDOR_ETC_TESTNET_NODE_HTTP_URL=https://rpc.mordor.etccooperative.org


# This is the address behind donation.eth
MATCHING_FUND_DONATIONS_FROM_ADDRESS=0x6e8873085530406995170Da467010565968C7C62
QF_ROUND_MAX_REWARD=0.2
2 changes: 2 additions & 0 deletions config/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,7 @@ MORDOR_ETC_TESTNET_NODE_HTTP_URL=https://rpc.mordor.etccooperative.org
# MORDOR ETC TESTNET Node URL
MORDOR_ETC_TESTNET_SCAN_API_URL=https://etc-mordor.blockscout.com/api/v1

# This is the address behind donation.eth
MATCHING_FUND_DONATIONS_FROM_ADDRESS=0x6e8873085530406995170Da467010565968C7C62


31 changes: 0 additions & 31 deletions migration/1698844763871-updateTotalDonationsOfQfRoundProjects.ts

This file was deleted.

69 changes: 69 additions & 0 deletions migration/1701689359018-add_fields_to_qf_round_history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class addFieldsToQfRoundHistory1701689359018
implements MigrationInterface
{
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "qf_round_history"
ADD COLUMN "matchingFundAmount" real ,
ADD COLUMN "matchingFundPriceUsd" real ,
ADD COLUMN "matchingFundCurrency" text ;
`);

await queryRunner.query(`
ALTER TABLE "donation"
ADD COLUMN "distributedFundQfRoundId" integer;

-- If you have a foreign key constraint to enforce the relationship
ALTER TABLE "donation"
ADD CONSTRAINT "FK_donation_qfRound"
FOREIGN KEY ("distributedFundQfRoundId") REFERENCES "qf_round"("id");
`);

// These rounds are in Production but I didnt set any condition for that
// because I want this part of code be executed in staging ENV

// Alpha round in production
await queryRunner.query(`
UPDATE qf_round_history
SET
"matchingFundAmount" = "matchingFund",
"matchingFundPriceUsd" = 1,
"matchingFundCurrency" = 'WXDAI'
WHERE
id = 2 AND "matchingFund" IS NOT NULL;

`);

// Optimism round in production
await queryRunner.query(`
UPDATE qf_round_history
SET
"matchingFundAmount" = "matchingFund",
"matchingFundPriceUsd" = 1,
"matchingFundCurrency" = 'DAI'
WHERE
id = 4 AND "matchingFund" IS NOT NULL;

`);
}

async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "qf_round_history"
DROP COLUMN "matchingFundAmount",
DROP COLUMN "matchingFundPriceUsd",
DROP COLUMN "matchingFundCurrency";
`);

await queryRunner.query(`
-- If you added a foreign key constraint, remove it first
ALTER TABLE "donation"
DROP CONSTRAINT IF EXISTS "FK_donation_qfRound";

ALTER TABLE "donation"
DROP COLUMN "distributedFundQfRoundId";
`);
}
}
25 changes: 25 additions & 0 deletions migration/1701756190381-create_donationeth_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

const matchingFundDonationsFromAddress =
(process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) ||
'0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address;

export class createDonationethUser1701756190381 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
INSERT INTO public."user" ("walletAddress", "name", "loginType", "role")
VALUES ('${matchingFundDonationsFromAddress}', 'Donation.eth', 'wallet', 'restricted')
ON CONFLICT ("walletAddress") DO NOTHING
`);
}

async down(queryRunner: QueryRunner): Promise<void> {
if (!matchingFundDonationsFromAddress) {
throw new Error('Wallet address is not defined in the configuration.');
}

await queryRunner.query(
`DELETE FROM public."user" WHERE "walletAddress" = '${matchingFundDonationsFromAddress}'`,
);
}
}
9 changes: 9 additions & 0 deletions src/entities/donation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ export class Donation extends BaseEntity {
@Column({ nullable: true })
qfRoundId: number;

@Index()
@Field(type => QfRound, { nullable: true })
@ManyToOne(type => QfRound, { eager: true })
distributedFundQfRound: QfRound;

@RelationId((donation: Donation) => donation.distributedFundQfRound)
@Column({ nullable: true })
distributedFundQfRoundId: number;

@Index()
@Field(type => User, { nullable: true })
@ManyToOne(type => User, { eager: true, nullable: true })
Expand Down
13 changes: 13 additions & 0 deletions src/entities/qfRoundHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,23 @@ export class QfRoundHistory extends BaseEntity {
@Column({ type: 'real', nullable: true, default: 0 })
raisedFundInUsd: number;

// usd value of matching fund
@Field(type => Float, { nullable: true })
@Column({ type: 'real', nullable: true, default: 0 })
matchingFund: number;

@Field(type => Float, { nullable: true })
@Column({ type: 'real', nullable: true })
matchingFundAmount?: number;

@Field(type => Float, { nullable: true })
@Column({ type: 'real', nullable: true })
matchingFundPriceUsd?: number;

@Field(type => String, { nullable: true })
@Column({ nullable: true })
matchingFundCurrency?: string;

@Field(type => String, { nullable: true })
@Column({ nullable: true })
distributedFundTxHash: string;
Expand Down
177 changes: 177 additions & 0 deletions src/repositories/qfRoundHistoryRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
createDonationData,
createProjectData,
generateRandomEtheriumAddress,
generateRandomTxHash,
saveDonationDirectlyToDb,
saveProjectDirectlyToDb,
saveUserDirectlyToDb,
Expand All @@ -10,12 +11,18 @@ import { QfRound } from '../entities/qfRound';
import { assert, expect } from 'chai';
import { Project } from '../entities/project';
import moment from 'moment';

import {
fillQfRoundHistory,
getQfRoundHistoriesThatDontHaveRelatedDonations,
getQfRoundHistory,
} from './qfRoundHistoryRepository';

describe('fillQfRoundHistory test cases', fillQfRoundHistoryTestCases);
describe(
'getQfRoundHistoriesThatDontHaveRelatedDonations test cases',
getQfRoundHistoriesThatDontHaveRelatedDonationsTestCases,
);

// TODO need to have more test cases for this conditions:
// when there is no donation for a project
Expand Down Expand Up @@ -319,3 +326,173 @@ function fillQfRoundHistoryTestCases() {
assert.equal(foundQfRoundHistory?.raisedFundInUsd, 10);
});
}

function getQfRoundHistoriesThatDontHaveRelatedDonationsTestCases() {
let qfRound: QfRound;
let firstProject: Project;
let secondProject: Project;
beforeEach(async () => {
await QfRound.update({}, { isActive: false });
qfRound = QfRound.create({
isActive: true,
name: 'test',
allocatedFund: 100,
slug: new Date().getTime().toString(),
minimumPassportScore: 8,
beginDate: new Date(),
endDate: moment().add(10, 'days').toDate(),
});
await qfRound.save();
firstProject = await saveProjectDirectlyToDb(createProjectData());
secondProject = await saveProjectDirectlyToDb(createProjectData());

firstProject.qfRounds = [qfRound];
secondProject.qfRounds = [qfRound];

await firstProject.save();
await secondProject.save();
});

afterEach(async () => {
qfRound.isActive = false;
await qfRound.save();
});

it('should return correct value for single project', async () => {
const inCompleteQfRoundHistories =
await getQfRoundHistoriesThatDontHaveRelatedDonations();
const usersDonations: number[][] = [
[1, 3], // 4
[2, 23], // 25
[3, 97], // 100
];

await Promise.all(
usersDonations.map(async valuesUsd => {
const user = await saveUserDirectlyToDb(
generateRandomEtheriumAddress(),
);
user.passportScore = 10;
await user.save();
return Promise.all(
valuesUsd.map(valueUsd => {
return saveDonationDirectlyToDb(
{
...createDonationData(),
valueUsd,
qfRoundId: qfRound.id,
status: 'verified',
},
user.id,
firstProject.id,
);
}),
);
}),
);

// if want to fill history round end date should be passed and be inactive
qfRound.endDate = moment().subtract(1, 'days').toDate();
qfRound.isActive = false;
await qfRound.save();

await fillQfRoundHistory();
const qfRoundHistory = await getQfRoundHistory({
projectId: firstProject.id,
qfRoundId: qfRound.id,
});
assert.isNotNull(qfRoundHistory);
qfRoundHistory!.distributedFundTxHash = generateRandomTxHash();
qfRoundHistory!.distributedFundNetwork = '100';
qfRoundHistory!.matchingFundAmount = 1000;
qfRoundHistory!.matchingFundCurrency = 'DAI';
qfRoundHistory!.matchingFund = 1000;
await qfRoundHistory?.save();

const inCompleteQfRoundHistories2 =
await getQfRoundHistoriesThatDontHaveRelatedDonations();
assert.equal(
inCompleteQfRoundHistories2.length - inCompleteQfRoundHistories.length,
1,
);
});
it('should return correct value for two projects', async () => {
const usersDonations: number[][] = [
[1, 3], // 4
[2, 23], // 25
[3, 97], // 100
];

await Promise.all(
usersDonations.map(async valuesUsd => {
const user = await saveUserDirectlyToDb(
generateRandomEtheriumAddress(),
);
user.passportScore = 10;
await user.save();
return Promise.all(
valuesUsd.map(async valueUsd => {
await saveDonationDirectlyToDb(
{
...createDonationData(),
valueUsd,
qfRoundId: qfRound.id,
status: 'verified',
},
user.id,
firstProject.id,
);
await saveDonationDirectlyToDb(
{
...createDonationData(),
valueUsd,
qfRoundId: qfRound.id,
status: 'verified',
},
user.id,
secondProject.id,
);
}),
);
}),
);

// if want to fill history round end date should be passed and be inactive
qfRound.endDate = moment().subtract(1, 'days').toDate();
qfRound.isActive = false;
await qfRound.save();
const inCompleteQfRoundHistories =
await getQfRoundHistoriesThatDontHaveRelatedDonations();

await fillQfRoundHistory();
const qfRoundHistory = await getQfRoundHistory({
projectId: firstProject.id,
qfRoundId: qfRound.id,
});
assert.isNotNull(qfRoundHistory);
qfRoundHistory!.distributedFundTxHash = generateRandomTxHash();
qfRoundHistory!.distributedFundNetwork = '100';
qfRoundHistory!.matchingFundAmount = 1000;
qfRoundHistory!.matchingFundCurrency = 'DAI';
qfRoundHistory!.matchingFund = 1000;
await qfRoundHistory?.save();

const qfRoundHistory2 = await getQfRoundHistory({
projectId: secondProject.id,
qfRoundId: qfRound.id,
});
assert.isNotNull(qfRoundHistory);
qfRoundHistory2!.distributedFundTxHash = generateRandomTxHash();
qfRoundHistory2!.distributedFundNetwork = '100';
qfRoundHistory2!.matchingFundAmount = 1000;
qfRoundHistory2!.matchingFundCurrency = 'DAI';
qfRoundHistory2!.matchingFund = 1000;
await qfRoundHistory2?.save();
const inCompleteQfRoundHistories2 =
await getQfRoundHistoriesThatDontHaveRelatedDonations();
assert.equal(
inCompleteQfRoundHistories2.length - inCompleteQfRoundHistories.length,
2,
);
});
}
Loading
Loading