Skip to content

Commit

Permalink
Feat/draft donation (#1308)
Browse files Browse the repository at this point in the history
* Added draftDonation resolver and entity

* Added few tests
Incomplete matchDraftDoantions

* Implemented draft donation matching logic

* Prepared draft donation matching worker draft

* Added support for draft donation matching worker run

* Updated delete expired draft donations
Integrated delete expired draft donation to draft donation matcher job

* Added enum creation to migration file

* Added sequence to migration file

* Reorder sql qury run

* Fixed id colum creation issue in migration file

* Removed settings from test.env

* rename draft worker

---------

Co-authored-by: Carlos <[email protected]>
  • Loading branch information
aminlatifi and CarlosQ96 authored Feb 13, 2024
1 parent 9128bb8 commit f5d4319
Show file tree
Hide file tree
Showing 33 changed files with 1,629 additions and 154 deletions.
6 changes: 6 additions & 0 deletions config/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,9 @@ UPDATE_RECURRING_DONATIONS_STREAM=0 0 * * *

MPETH_GRAPHQL_PRICES_URL=
MPETH_GRAPHQL_PRICES_URL=

# Draft donation match expiration hours, they will be deleted after to lessen dabase size
ENABLE_DRAFT_DONATION=true
DRAFT_DONATION_MATCH_EXPIRATION_HOURS=24
MATCH_DRAFT_DONATION_CRONJOB_EXPRESSION = '0 */5 * * * *';

5 changes: 4 additions & 1 deletion config/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,7 @@ DISABLE_NOTIFICATION_CENTER=false
DONATION_SAVE_BACKUP_CRONJOB_EXPRESSION=
ENABLE_IMPORT_DONATION_BACKUP=false
DONATION_SAVE_BACKUP_API_URL=
DONATION_SAVE_BACKUP_API_SECRET=
DONATION_SAVE_BACKUP_API_SECRET=

DRAFT_DONATION_MATCH_EXPIRATION_HOURS=24
ENABLE_DRAFT_DONATION=true
83 changes: 83 additions & 0 deletions migration/1707738577647-addDraftDonationTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddDraftDonationTable1707738577647 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'draft_donation_chaintype_enum') THEN
CREATE TYPE public.draft_donation_chaintype_enum AS ENUM
('EVM', 'SOLANA');
END IF;
ALTER TYPE public.draft_donation_chaintype_enum
OWNER TO postgres;
END$$;`);

await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'draft_donation_status_enum') THEN
CREATE TYPE public.draft_donation_status_enum AS ENUM
('pending', 'matched', 'failed');
END IF;
ALTER TYPE public.draft_donation_status_enum
OWNER TO postgres;
END$$;
`);

await queryRunner.query(`
CREATE TABLE IF NOT EXISTS public.draft_donation
(
"id" SERIAL NOT NULL,
"networkId" integer NOT NULL,
"chainType" draft_donation_chaintype_enum NOT NULL DEFAULT 'EVM'::draft_donation_chaintype_enum,
status draft_donation_status_enum NOT NULL DEFAULT 'pending'::draft_donation_status_enum,
"toWalletAddress" character varying COLLATE pg_catalog."default" NOT NULL,
"fromWalletAddress" character varying COLLATE pg_catalog."default" NOT NULL,
"tokenAddress" character varying COLLATE pg_catalog."default",
currency character varying COLLATE pg_catalog."default" NOT NULL,
anonymous boolean,
amount real NOT NULL,
"projectId" integer,
"userId" integer,
"createdAt" timestamp without time zone NOT NULL DEFAULT now(),
"referrerId" character varying COLLATE pg_catalog."default",
"expectedCallData" character varying COLLATE pg_catalog."default",
"errorMessage" character varying COLLATE pg_catalog."default",
CONSTRAINT "PK_4f2eb58b84fb470edcd483c78af" PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.draft_donation
OWNER to postgres;
CREATE INDEX IF NOT EXISTS "IDX_287bf9818fca5b436122847223"
ON public.draft_donation USING btree
("userId" ASC NULLS LAST)
TABLESPACE pg_default
WHERE status = 'pending'::draft_donation_status_enum;
CREATE UNIQUE INDEX IF NOT EXISTS "IDX_af180374473ea402e7595196a6"
ON public.draft_donation USING btree
("fromWalletAddress" COLLATE pg_catalog."default" ASC NULLS LAST, "toWalletAddress" COLLATE pg_catalog."default" ASC NULLS LAST, "networkId" ASC NULLS LAST, amount ASC NULLS LAST, currency COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default
WHERE status = 'pending'::draft_donation_status_enum;
CREATE INDEX IF NOT EXISTS "IDX_029453ee31e092317f7f96ee3b"
ON public.draft_donation USING btree
("createdAt" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "IDX_ff4b8666a0090d059f00c59216"
ON public.draft_donation USING btree
(status ASC NULLS LAST)
TABLESPACE pg_default
WHERE status = 'pending'::draft_donation_status_enum;
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"test:projectAddressRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/projectAddressRepository.test.ts",
"test:anchorContractAddressRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/anchorContractAddressRepository.test.ts",
"test:recurringDonationRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/recurringDonationRepository.test.ts",
"test:dbCronRepository": "NODE_ENV=test mocha -t 90000 ./test/pre-test-scripts.ts ./src/repositories/dbCronRepository.test.ts",
"test:dbCronRepository": "NODE_ENV=test mocha -t 90000 ./test/pre-test-scripts.ts ./src/repositories/dbCronRepository.test.ts",
"test:powerBoostingResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/powerBoostingResolver.test.ts",
"test:userProjectPowerResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/userProjectPowerResolver.test.ts",
"test:projectPowerResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/projectPowerResolver.test.ts",
Expand All @@ -170,6 +170,7 @@
"test:recurringDonationResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/recurringDonationResolver.test.ts",
"test:fillSnapshotBalance": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/fillSnapshotBalances.test.ts",
"test:donationService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/qfRoundHistoryRepository.test.ts ./src/services/donationService.test.ts",
"test:draftDonationService": "NODE_ENV=test mocha -t 99999 ./test/pre-test-scripts.ts src/services/chains/evm/draftDonationService.test.ts src/repositories/draftDonationRepository.test.ts src/workers/draftDonationMatchWorker.test.ts",
"test:userService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/userService.test.ts",
"test:lostDonations": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/importLostDonationsJob.test.ts",
"test:reactionsService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/reactionsService.test.ts",
Expand Down
3 changes: 2 additions & 1 deletion src/entities/donation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ export const DONATION_STATUS = {
FAILED: 'failed',
};

export const DONATION_EXTERNAL_SOURCES = {
export const DONATION_ORIGINS = {
IDRISS_TWITTER: 'Idriss',
DRAFT_DONATION_MATCHING: 'DraftDonationMatching',
};

export const DONATION_TYPES = {
Expand Down
110 changes: 110 additions & 0 deletions src/entities/draftDonation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Field, ID, Int, ObjectType } from 'type-graphql';
import {
PrimaryGeneratedColumn,
Column,
Entity,
BaseEntity,
Index,
CreateDateColumn,
} from 'typeorm';
import { ChainType } from '../types/network';

export const DRAFT_DONATION_STATUS = {
PENDING: 'pending',
MATCHED: 'matched',
FAILED: 'failed',
};

@Entity()
@ObjectType()
// To mark the draft donation as matched, when the donation is created in DonationResolver
@Index(
['fromWalletAddress', 'toWalletAddress', 'networkId', 'amount', 'currency'],
{
where: `status = '${DRAFT_DONATION_STATUS.PENDING}'`,
unique: true,
},
)
export class DraftDonation extends BaseEntity {
@Field(type => ID)
@PrimaryGeneratedColumn()
id: number;

@Field()
@Column({ nullable: false })
networkId: number;

// // TODO: support safeTransactionId
// @Field()
// @Column({ nullable: true })
// safeTransactionId?: string;

@Field(type => String)
@Column({
type: 'enum',
enum: ChainType,
default: ChainType.EVM,
})
chainType: ChainType;

@Field()
@Column({
type: 'enum',
enum: DRAFT_DONATION_STATUS,
default: DRAFT_DONATION_STATUS.PENDING,
})
@Index({ where: `status = '${DRAFT_DONATION_STATUS.PENDING}'` })
status: string;

@Field()
@Column()
toWalletAddress: string;

@Field()
@Column()
fromWalletAddress: string;

@Field({ nullable: true })
@Column({ nullable: true })
tokenAddress: string;

@Field()
@Column()
currency: string;

@Field({ nullable: true })
@Column({ nullable: true })
anonymous: boolean;

@Field()
@Column({ type: 'real' })
amount: number;

@Field()
@Column({ nullable: true })
projectId: number;

@Field()
@Column({ nullable: true })
@Index({ where: `status = '${DRAFT_DONATION_STATUS.PENDING}'` })
userId: number;

@Index()
@Field(type => Date)
@CreateDateColumn()
createdAt: Date;

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

// Expected call data used only for matching ERC20 transfers
// Is calculated and saved once during the matching time, and will be used in next iterations
@Field(type => String, { nullable: true })
@Column({ nullable: true })
expectedCallData?: string;

@Field({ nullable: true })
@Column({ nullable: true })
errorMessage?: string;
}
2 changes: 2 additions & 0 deletions src/entities/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { QfRoundHistory } from './qfRoundHistory';
import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView';
import { AnchorContractAddress } from './anchorContractAddress';
import { RecurringDonation } from './recurringDonation';
import { DraftDonation } from './draftDonation';

export const getEntities = (): DataSourceOptions['entities'] => {
return [
Expand All @@ -59,6 +60,7 @@ export const getEntities = (): DataSourceOptions['entities'] => {
FeaturedUpdate,
Reaction,
Donation,
DraftDonation,
Token,
Wallet,
ProjectStatus,
Expand Down
5 changes: 3 additions & 2 deletions src/orm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { redis, redisConfig } from './redis';
export class AppDataSource {
private static datasource: DataSource;

static async initialize() {
static async initialize(_overrideDrop?: boolean) {
if (!AppDataSource.datasource) {
const dropSchema = config.get('DROP_DATABASE') === 'true';
const dropSchema =
_overrideDrop ?? config.get('DROP_DATABASE') === 'true';
const synchronize = (config.get('ENVIRONMENT') as string) === 'test';
const entities = getEntities();
AppDataSource.datasource = new DataSource({
Expand Down
84 changes: 84 additions & 0 deletions src/repositories/draftDonationRepository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Create a draft donation

import { expect } from 'chai';
import { generateRandomEtheriumAddress } from '../../test/testUtils';
import {
DRAFT_DONATION_STATUS,
DraftDonation,
} from '../entities/draftDonation';
import {
delecteExpiredDraftDonations,
markDraftDonationStatusMatched,
} from './draftDonationRepository';

// Mark the draft donation as matched
describe('draftDonationRepository', () => {
beforeEach(async () => {
await DraftDonation.clear();
});

it('should mark a draft donation as matched', async () => {
// Setup
const draftDonation = await DraftDonation.create({
networkId: 1,
status: DRAFT_DONATION_STATUS.PENDING,
toWalletAddress: generateRandomEtheriumAddress(),
fromWalletAddress: generateRandomEtheriumAddress(),
tokenAddress: generateRandomEtheriumAddress(),
currency: 'GIV',
anonymous: false,
amount: 0.01,
});

await draftDonation.save();

await markDraftDonationStatusMatched({
fromWalletAddress: draftDonation.fromWalletAddress,
toWalletAddress: draftDonation.toWalletAddress,
networkId: draftDonation.networkId,
currency: draftDonation.currency,
amount: draftDonation.amount,
});

const updatedDraftDonation = await DraftDonation.findOne({
where: {
id: draftDonation.id,
},
});

expect(updatedDraftDonation?.status).equal(DRAFT_DONATION_STATUS.MATCHED);
});

it('should clear expired draft donations', async () => {
// create a draft donation with createdAt two hours ago, and one with createdAt one hour ago
await DraftDonation.create({
networkId: 1,
status: DRAFT_DONATION_STATUS.PENDING,
toWalletAddress: generateRandomEtheriumAddress(),
fromWalletAddress: generateRandomEtheriumAddress(),
tokenAddress: generateRandomEtheriumAddress(),
currency: 'GIV',
anonymous: false,
amount: 1,
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
}).save();

await DraftDonation.create({
networkId: 1,
status: DRAFT_DONATION_STATUS.PENDING,
toWalletAddress: generateRandomEtheriumAddress(),
fromWalletAddress: generateRandomEtheriumAddress(),
tokenAddress: generateRandomEtheriumAddress(),
currency: 'GIV',
anonymous: false,
amount: 1,
createdAt: new Date(Date.now() - 1 * 60 * 60 * 1000),
}).save();

await delecteExpiredDraftDonations(1.5);

const count = await DraftDonation.createQueryBuilder().getCount();

expect(count).equal(1);
});
});
Loading

0 comments on commit f5d4319

Please sign in to comment.