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: Add eligible donations to qfround entity #1124

Merged
merged 5 commits into from
Sep 14, 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
16 changes: 16 additions & 0 deletions migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

export class AddEligibleNetworksToQfRoundEntity1694295208252
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "qf_round"
ADD COLUMN IF NOT EXIST "eligibleNetworks" integer array DEFAULT ARRAY[]::integer[]
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('qf_round', 'eligibleNetworks');
}
}
32 changes: 32 additions & 0 deletions migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import config from '../src/config';

export class AddEligibleNetworksToPreviousQfRounds1694635872128
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
const environment = config.get('ENVIRONMENT') as string;

// Define the eligible network IDs based on the conditions
const eligibleNetworks =
environment !== 'production'
? [1, 3, 5, 100, 137, 10, 420, 56, 42220, 44787] // Include testnets for staging
: [1, 137, 56, 42220, 100, 10]; // Exclude testnets for non-staging

// Convert the eligibleNetworks array to a comma-separated string
const eligibleNetworksString = eligibleNetworks.join(', ');

// Update the "qf_round" table with the new eligibleNetworks values
await queryRunner.query(`
UPDATE "qf_round"
SET eligibleNetworks = '{${eligibleNetworksString}}'
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE "qf_round"
SET eligibleNetworks = '{}'
`);
}
}
14 changes: 13 additions & 1 deletion src/entities/qfRound.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field, ID, ObjectType } from 'type-graphql';
import { Field, ID, ObjectType, Int } from 'type-graphql';
import {
PrimaryGeneratedColumn,
Column,
Expand Down Expand Up @@ -36,6 +36,10 @@ export class QfRound extends BaseEntity {
@Column()
minimumPassportScore: number;

@Field(type => [Int], { nullable: true }) // Define the new field as an array of integers
@Column('integer', { array: true, default: [] })
eligibleNetworks: number[];

@Field(type => Date)
@Column()
beginDate: Date;
Expand All @@ -52,4 +56,12 @@ export class QfRound extends BaseEntity {

@ManyToMany(type => Project, project => project.qfRounds)
projects: Project[];

// only projects with status active can be listed automatically
isEligibleNetwork(donationNetworkId: number): Boolean {
// when not specified, all are valid
if (this.eligibleNetworks.length === 0) return true;

return this.eligibleNetworks.includes(donationNetworkId);
}
}
96 changes: 96 additions & 0 deletions src/resolvers/donationResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,102 @@ function createDonationTestCases() {
qfRound.isActive = false;
await qfRound.save();
});

it('should create a donation in an active qfRound when qfround has network eligiblity on XDAI', async () => {
const project = await saveProjectDirectlyToDb(createProjectData());
const qfRound = await QfRound.create({
isActive: true,
name: new Date().toString(),
minimumPassportScore: 8,
allocatedFund: 100,
eligibleNetworks: [100], // accepts ONLY xdai to mark as part of QFround
beginDate: new Date(),
endDate: moment().add(2, 'day'),
}).save();
project.qfRounds = [qfRound];
await project.save();
const referrerId = generateRandomString();
const referrerWalletAddress =
await getChainvineAdapter().getWalletAddressFromReferrer(referrerId);

const user = await User.create({
walletAddress: generateRandomEtheriumAddress(),
loginType: 'wallet',
firstName: 'first name',
}).save();

const user2 = await User.create({
walletAddress: referrerWalletAddress,
loginType: 'wallet',
firstName: 'first name',
}).save();

const referredEvent = await firstOrCreateReferredEventByUserId(user.id);
referredEvent.startTime = new Date();
await referredEvent.save();

// should save Xdai
const accessToken = await generateTestAccessToken(user.id);
const saveDonationResponseXdai = await axios.post(
graphqlUrl,
{
query: createDonationMutation,
variables: {
projectId: project.id,
transactionNetworkId: NETWORK_IDS.XDAI,
transactionId: generateRandomTxHash(),
nonce: 1,
amount: 10,
token: 'GIV',
referrerId,
},
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
assert.isOk(saveDonationResponseXdai.data.data.createDonation);
const donation = await Donation.findOne({
where: {
id: saveDonationResponseXdai.data.data.createDonation,
},
});

assert.equal(donation?.qfRound?.id as number, qfRound.id);

// should ignore non xdai donations because its not an eligible network
const saveDonationResponseNotXdai = await axios.post(
graphqlUrl,
{
query: createDonationMutation,
variables: {
projectId: project.id,
transactionNetworkId: NETWORK_IDS.CELO,
transactionId: generateRandomTxHash(),
nonce: 1,
amount: 10,
token: 'GIV',
referrerId,
},
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
assert.isOk(saveDonationResponseNotXdai.data.data.createDonation);
const donationNotFromQF = await Donation.findOne({
where: {
id: saveDonationResponseNotXdai.data.data.createDonation,
},
});
assert.isNull(donationNotFromQF?.qfRound);
qfRound.isActive = false;
await qfRound.save();
});
it('should create a donation in an active qfRound, when project is not listed', async () => {
const project = await saveProjectDirectlyToDb(createProjectData());
const qfRound = await QfRound.create({
Expand Down
5 changes: 4 additions & 1 deletion src/resolvers/donationResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,10 @@ export class DonationResolver {
const activeQfRoundForProject = await relatedActiveQfRoundForProject(
projectId,
);
if (activeQfRoundForProject) {
if (
activeQfRoundForProject &&
activeQfRoundForProject.isEligibleNetwork(Number(transactionNetworkId))
) {
donation.qfRound = activeQfRoundForProject;
}
await donation.save();
Expand Down
20 changes: 20 additions & 0 deletions src/server/adminJs/tabs/qfRoundTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getRelatedProjectsOfQfRound,
} from '../../../repositories/qfRoundRepository';
import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface';
import { NETWORK_IDS } from '../../../provider';

export const refreshMaterializedViews = async (
response,
Expand Down Expand Up @@ -70,6 +71,25 @@ export const qfRoundTab = {
minimumPassportScore: {
isVisible: true,
},
eligibleNetworks: {
isVisible: true,
type: 'array',
availableValues: [
{ value: NETWORK_IDS.MAIN_NET, label: 'MAINNET' },
{ value: NETWORK_IDS.ROPSTEN, label: 'ROPSTEN' },
{ value: NETWORK_IDS.GOERLI, label: 'GOERLI' },
{ value: NETWORK_IDS.POLYGON, label: 'POLYGON' },
{ value: NETWORK_IDS.OPTIMISTIC, label: 'OPTIMISTIC' },
{ value: NETWORK_IDS.OPTIMISM_GOERLI, label: 'OPTIMISM GOERLI' },
{ value: NETWORK_IDS.CELO, label: 'CELO' },
{
value: NETWORK_IDS.CELO_ALFAJORES,
label: 'ALFAJORES (Test CELO)',
},
{ value: NETWORK_IDS.XDAI, label: 'XDAI' },
{ value: NETWORK_IDS.BSC, label: 'BSC' },
],
},
projects: {
type: 'mixed',
isVisible: {
Expand Down
Loading