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

Feat/draft donation #1308

Merged
merged 17 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
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
5 changes: 3 additions & 2 deletions 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 Expand Up @@ -66,7 +67,7 @@ export class Donation extends BaseEntity {
@Column({ nullable: true })
safeTransactionId?: string;

@Field()
@Field(type => String)
@Column({
type: 'enum',
enum: ChainType,
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
Loading