Skip to content

Commit

Permalink
Merge branch 'staging' of github.com:GeneralMagicio/QAcc-BE into AddR…
Browse files Browse the repository at this point in the history
…oundCaps
  • Loading branch information
ae2079 committed Sep 29, 2024
2 parents 571aca9 + 9258509 commit 9ba7f4b
Show file tree
Hide file tree
Showing 20 changed files with 298 additions and 25 deletions.
Binary file removed .DS_Store
Binary file not shown.
23 changes: 23 additions & 0 deletions migration/1727098387189-addTokenPriceToRounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddTokenPriceToRounds1727098387189 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Add `token_price` column to the `qf_round` table
await queryRunner.query(
`ALTER TABLE "qf_round" ADD "token_price" double precision DEFAULT NULL`,
);

// Add `token_price` column to the `early_access_round` table
await queryRunner.query(
`ALTER TABLE "early_access_round" ADD "token_price" double precision DEFAULT NULL`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Remove `token_price` column from both tables if needed
await queryRunner.query(`ALTER TABLE "qf_round" DROP COLUMN "token_price"`);
await queryRunner.query(
`ALTER TABLE "early_access_round" DROP COLUMN "token_price"`,
);
}
}
25 changes: 25 additions & 0 deletions migration/1727458215571-fixAddTokenPriceToRounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class FixAddTokenPriceToRounds1727458215571
implements MigrationInterface
{
name = 'FixAddTokenPriceToRounds1727458215571';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "qf_round" RENAME COLUMN "token_price" TO "tokenPrice"`,
);
await queryRunner.query(
`ALTER TABLE "early_access_round" RENAME COLUMN "token_price" TO "tokenPrice"`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "early_access_round" RENAME COLUMN "tokenPrice" TO "token_price"`,
);
await queryRunner.query(
`ALTER TABLE "qf_round" RENAME COLUMN "tokenPrice" TO "token_price"`,
);
}
}
5 changes: 4 additions & 1 deletion src/adapters/price/CoingeckoPriceAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios';
import moment from 'moment';
import {
GetTokenPriceAtDateParams,
GetTokenPriceParams,
Expand Down Expand Up @@ -46,9 +47,11 @@ export class CoingeckoPriceAdapter implements PriceAdapterInterface {
params: GetTokenPriceAtDateParams,
): Promise<number> {
try {
const formattedDate = moment(params.date).format('DD-MM-YYYY');

const result = await axios.get(
// symbol in here means coingecko id for instance
`https://api.coingecko.com/api/v3/coins/${params.symbol}/history?date=${params.date}`,
`https://api.coingecko.com/api/v3/coins/${params.symbol}/history?date=${formattedDate}`,
);

const priceUsd = result?.data?.market_data?.current_price?.usd;
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/price/PriceAdapterInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface GetTokenPriceParams {

export interface GetTokenPriceAtDateParams {
symbol: string;
date: string;
date: Date;
}

export interface PriceAdapterInterface {
Expand Down
13 changes: 13 additions & 0 deletions src/constants/qacc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import config from '../config';

export const QACC_DONATION_TOKEN_ADDRESS: string =
(config.get('QACC_DONATION_TOKEN_ADDRESS') as string) ||
'0xa2036f0538221a77a3937f1379699f44945018d0'; //https://zkevm.polygonscan.com/token/0xa2036f0538221a77a3937f1379699f44945018d0#readContract
export const QACC_DONATION_TOKEN_SYMBOL =
(config.get('QACC_DONATION_TOKEN_SYMBOL') as string) || 'MATIC';
export const QACC_DONATION_TOKEN_NAME =
(config.get('QACC_DONATION_TOKEN_NAME') as string) || 'Matic token';
export const QACC_DONATION_TOKEN_DECIMALS =
(+config.get('QACC_DONATION_TOKEN_DECIMALS') as number) || 18;
export const QACC_DONATION_TOKEN_COINGECKO_ID =
(config.get('QACC_DONATION_TOKEN_COINGECKO_ID') as string) || 'matic-network';
4 changes: 4 additions & 0 deletions src/entities/earlyAccessRound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@ export class EarlyAccessRound extends BaseEntity {
@Field(() => Date)
@UpdateDateColumn()
updatedAt: Date;

@Field({ nullable: true })
@Column({ type: 'float', nullable: true })
tokenPrice?: number;
}
4 changes: 4 additions & 0 deletions src/entities/qfRound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export class QfRound extends BaseEntity {
@Column({ default: false })
isDataAnalysisDone: boolean;

@Field({ nullable: true })
@Column({ type: 'float', nullable: true })
tokenPrice?: number;

@UpdateDateColumn()
updatedAt: Date;

Expand Down
73 changes: 73 additions & 0 deletions src/repositories/earlyAccessRoundRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
import { expect } from 'chai';
import moment from 'moment';
import sinon from 'sinon';
import { EarlyAccessRound } from '../entities/earlyAccessRound';
import {
findAllEarlyAccessRounds,
findActiveEarlyAccessRound,
fillMissingTokenPriceInEarlyAccessRounds,
} from './earlyAccessRoundRepository';
import { saveRoundDirectlyToDb } from '../../test/testUtils';
import { CoingeckoPriceAdapter } from '../adapters/price/CoingeckoPriceAdapter';
import { QACC_DONATION_TOKEN_COINGECKO_ID } from '../constants/qacc';

describe('EarlyAccessRound Repository Test Cases', () => {
let priceAdapterStub: sinon.SinonStub;

beforeEach(async () => {
// Clean up data before each test case
await EarlyAccessRound.delete({});

// Stub CoingeckoPriceAdapter to mock getTokenPriceAtDate
priceAdapterStub = sinon
.stub(CoingeckoPriceAdapter.prototype, 'getTokenPriceAtDate')
.resolves(100);

// Reset tokenPrice to undefined for test consistency
await EarlyAccessRound.update({}, { tokenPrice: undefined });
});

afterEach(async () => {
// Clean up data after each test case
await EarlyAccessRound.delete({});

// Restore the stubbed method after each test
priceAdapterStub.restore();
});

it('should save a new Early Access Round directly to the database', async () => {
Expand Down Expand Up @@ -125,4 +143,59 @@ describe('EarlyAccessRound Repository Test Cases', () => {
const activeRound = await findActiveEarlyAccessRound();
expect(activeRound).to.be.null;
});

it('should update token price for rounds with null tokenPrice', async () => {
// Create a EarlyAccessRound with null token price
const earlyAccessRound = EarlyAccessRound.create({
roundNumber: Math.floor(Math.random() * 10000),
startDate: moment().subtract(3, 'days').toDate(),
endDate: moment().add(10, 'days').toDate(),
tokenPrice: undefined,
});
await EarlyAccessRound.save(earlyAccessRound);

const updatedCount = await fillMissingTokenPriceInEarlyAccessRounds();

const updatedEarlyAcccessRound = await EarlyAccessRound.findOne({
where: { id: earlyAccessRound.id },
});

// Assert that the token price fetching method was called with the correct date
sinon.assert.calledWith(priceAdapterStub, {
symbol: QACC_DONATION_TOKEN_COINGECKO_ID,
date: earlyAccessRound.startDate,
});

expect(updatedEarlyAcccessRound?.tokenPrice).to.equal(100);
expect(updatedCount).to.equal(1);
});

it('should not update token price for rounds with existing tokenPrice', async () => {
// Create a EarlyAccessRound with an existing token price
const earlyAccessRound = EarlyAccessRound.create({
roundNumber: Math.floor(Math.random() * 10000),
startDate: moment().subtract(3, 'days').toDate(),
endDate: moment().add(10, 'days').toDate(),
tokenPrice: 50,
});
await EarlyAccessRound.save(earlyAccessRound);

const updatedCount = await fillMissingTokenPriceInEarlyAccessRounds();

const updatedEarlyAcccessRound = await EarlyAccessRound.findOne({
where: { id: earlyAccessRound.id },
});

expect(updatedEarlyAcccessRound?.tokenPrice).to.equal(50);
expect(updatedCount).to.equal(0);
});

it('should return zero if there are no rounds to update', async () => {
// Ensure no rounds with null token_price
await EarlyAccessRound.update({}, { tokenPrice: 100 });

const updatedCount = await fillMissingTokenPriceInEarlyAccessRounds();

expect(updatedCount).to.equal(0);
});
});
34 changes: 34 additions & 0 deletions src/repositories/earlyAccessRoundRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { CoingeckoPriceAdapter } from '../adapters/price/CoingeckoPriceAdapter';
import { EarlyAccessRound } from '../entities/earlyAccessRound';
import { logger } from '../utils/logger';
import { AppDataSource } from '../orm';
import { QACC_DONATION_TOKEN_COINGECKO_ID } from '../constants/qacc';

export const findAllEarlyAccessRounds = async (): Promise<
EarlyAccessRound[]
Expand Down Expand Up @@ -30,3 +33,34 @@ export const findActiveEarlyAccessRound =
throw new Error('Error fetching active Early Access round');
}
};

export const fillMissingTokenPriceInEarlyAccessRounds = async (): Promise<
void | number
> => {
const priceAdapter = new CoingeckoPriceAdapter();

// Find all EarlyAccessRound where token_price is NULL
const roundsToUpdate = await AppDataSource.getDataSource()
.getRepository(EarlyAccessRound)
.createQueryBuilder('early_AccessRound')
.where('early_AccessRound.tokenPrice IS NULL')
.andWhere('early_AccessRound.startDate < :now', { now: new Date() })
.getMany();

// Set the token price for all found rounds and save them
for (const round of roundsToUpdate) {
const tokenPrice = await priceAdapter.getTokenPriceAtDate({
symbol: QACC_DONATION_TOKEN_COINGECKO_ID,
date: round.startDate,
});

if (tokenPrice) {
round.tokenPrice = tokenPrice;
await AppDataSource.getDataSource()
.getRepository(EarlyAccessRound)
.save(round);
}
}

return roundsToUpdate.length;
};
77 changes: 77 additions & 0 deletions src/repositories/qfRoundRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert, expect } from 'chai';
import moment from 'moment';
import sinon from 'sinon';
import {
createDonationData,
createProjectData,
Expand All @@ -17,10 +18,12 @@ import {
getProjectDonationsSqrtRootSum,
getQfRoundTotalSqrtRootSumSquared,
getQfRoundStats,
fillMissingTokenPriceInQfRounds,
} from './qfRoundRepository';
import { Project } from '../entities/project';
import { refreshProjectEstimatedMatchingView } from '../services/projectViewsService';
import { getProjectQfRoundStats } from './donationRepository';
import { CoingeckoPriceAdapter } from '../adapters/price/CoingeckoPriceAdapter';

describe(
'getProjectDonationsSqrtRootSum test cases',
Expand All @@ -40,6 +43,10 @@ describe(
);
describe('findQfRoundById test cases', findQfRoundByIdTestCases);
describe('findQfRoundBySlug test cases', findQfRoundBySlugTestCases);
describe(
'fillMissingTokenPriceInQfRounds test cases',
fillMissingTokenPriceInQfRoundsTestCase,
);

function getProjectDonationsSqrRootSumTests() {
let qfRound: QfRound;
Expand Down Expand Up @@ -506,3 +513,73 @@ function findQfRoundBySlugTestCases() {
assert.isNull(result);
});
}

function fillMissingTokenPriceInQfRoundsTestCase() {
let priceAdapterStub: sinon.SinonStub;

beforeEach(async () => {
// Stub CoingeckoPriceAdapter to mock getTokenPriceAtDate
priceAdapterStub = sinon
.stub(CoingeckoPriceAdapter.prototype, 'getTokenPriceAtDate')
.resolves(100);

// Reset tokenPrice to undefined for test consistency
await QfRound.update({}, { tokenPrice: undefined });
});

afterEach(() => {
// Restore the stubbed method after each test
priceAdapterStub.restore();
});

it('should update token price for rounds with null tokenPrice', async () => {
// Create a QfRound with null token price
const qfRound = QfRound.create({
isActive: true,
name: 'test',
allocatedFund: 100,
minimumPassportScore: 8,
slug: new Date().getTime().toString(),
beginDate: moment().subtract(3, 'days').toDate(),
endDate: moment().add(10, 'days').toDate(),
tokenPrice: undefined,
});
await qfRound.save();

const updatedCount = await fillMissingTokenPriceInQfRounds();

const updatedQfRound = await QfRound.findOne({ where: { id: qfRound.id } });
expect(updatedQfRound?.tokenPrice).to.equal(100);
expect(updatedCount).to.equal(1);
});

it('should not update token price for rounds with existing tokenPrice', async () => {
// Create a QfRound with an existing token price
const qfRound = QfRound.create({
isActive: true,
name: 'test',
allocatedFund: 100,
minimumPassportScore: 8,
slug: new Date().getTime().toString(),
beginDate: moment().subtract(3, 'days').toDate(),
endDate: moment().add(10, 'days').toDate(),
tokenPrice: 50,
});
await qfRound.save();

const updatedCount = await fillMissingTokenPriceInQfRounds();

const updatedQfRound = await QfRound.findOne({ where: { id: qfRound.id } });
expect(updatedQfRound?.tokenPrice).to.equal(50);
expect(updatedCount).to.equal(0);
});

it('should return zero if there are no rounds to update', async () => {
// Ensure no rounds with null tokenPrice
await QfRound.update({}, { tokenPrice: 100 });

const updatedCount = await fillMissingTokenPriceInQfRounds();

expect(updatedCount).to.equal(0);
});
}
Loading

0 comments on commit 9ba7f4b

Please sign in to comment.