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

Hotfix add matching funds as donations #1190

Merged
merged 7 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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


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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we hard coding the id to 2?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some qf rounds already in our DB and they have qfRoundHistory items in DB, they dont have new fields, so I had to put their ids hard coded here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


`);

// 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 donationDotEthAddress = '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address;
const matchingFundDonationsFromAddress =
(process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) ||
donationDotEthAddress;

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
184 changes: 176 additions & 8 deletions src/repositories/qfRoundHistoryRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@ import {
createDonationData,
createProjectData,
generateRandomEtheriumAddress,
generateRandomTxHash,
saveDonationDirectlyToDb,
saveProjectDirectlyToDb,
saveUserDirectlyToDb,
} from '../../test/testUtils';
import { QfRound } from '../entities/qfRound';
import { assert, expect } from 'chai';
import {
getProjectDonationsSqrtRootSum,
getQfRoundTotalProjectsDonationsSum,
} from './qfRoundRepository';
import { Project } from '../entities/project';
import moment from 'moment';
import {
refreshProjectDonationSummaryView,
refreshProjectEstimatedMatchingView,
} from '../services/projectViewsService';

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 @@ -326,3 +325,172 @@ 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,
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,
);
});
}
25 changes: 25 additions & 0 deletions src/repositories/qfRoundHistoryRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,28 @@ export const getQfRoundHistory = async (params: {
const { projectId, qfRoundId } = params;
return QfRoundHistory.findOne({ where: { projectId, qfRoundId } });
};

export const getQfRoundHistoriesThatDontHaveRelatedDonations =
async (): Promise<QfRoundHistory[]> => {
try {
return QfRoundHistory.createQueryBuilder('q')
.innerJoin('qf_round', 'qr', 'qr.id = q.qfRoundId')
.innerJoin('project', 'p', 'p.id = q.projectId')
.leftJoin(
'donation',
'd',
'q.distributedFundTxHash = d.transactionId AND q.projectId = d.projectId AND d.distributedFundQfRoundId IS NOT NULL',
)
.where('d.id IS NULL')
.andWhere('q.matchingFund IS NOT NULL')
.andWhere('q.matchingFund != 0')
.andWhere('q.distributedFundTxHash IS NOT NULL')
.andWhere('q.distributedFundNetwork IS NOT NULL')
.andWhere('q.matchingFundCurrency IS NOT NULL')
.andWhere('q.matchingFundAmount IS NOT NULL')
.getMany();
} catch (e) {
logger.error('getQfRoundHistoriesThatDontHaveRelatedDonations error', e);
throw e;
}
};
Loading
Loading