From f7c452a25c347639ef4a6aaf9a95410057cba6a1 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 12 Dec 2023 15:53:52 +0330 Subject: [PATCH] WIP Adding recurring donations and anchorContractAddress entities related to #1142 --- .../1701979390554-addnullableTransactionId.ts | 24 +++--- ...35-create_anchor_contract_address_table.ts | 47 ++++++++++++ src/entities/anchorContractAddress.ts | 76 +++++++++++++++++++ src/entities/project.ts | 11 +++ src/entities/recurringDonation.ts | 60 +++++++++++++++ .../anchorContractAddressRepository.ts | 31 ++++++++ src/repositories/projectRepository.ts | 1 + .../anchorContractAddressResolver.ts | 63 +++++++++++++++ src/resolvers/resolvers.ts | 3 + src/utils/errorMessages.ts | 2 + 10 files changed, 308 insertions(+), 10 deletions(-) create mode 100644 migration/1702364570535-create_anchor_contract_address_table.ts create mode 100644 src/entities/anchorContractAddress.ts create mode 100644 src/entities/recurringDonation.ts create mode 100644 src/repositories/anchorContractAddressRepository.ts create mode 100644 src/resolvers/anchorContractAddressResolver.ts diff --git a/migration/1701979390554-addnullableTransactionId.ts b/migration/1701979390554-addnullableTransactionId.ts index 609910a4e..1b474dcf3 100644 --- a/migration/1701979390554-addnullableTransactionId.ts +++ b/migration/1701979390554-addnullableTransactionId.ts @@ -1,13 +1,17 @@ -import {MigrationInterface, QueryRunner} from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; -export class addnullableTransactionId1701979390554 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE public.donation ALTER COLUMN "transactionId" DROP NOT NULL`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE public.donation ALTER COLUMN "transactionId" SET NOT NULL`); - } +export class addnullableTransactionId1701979390554 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE public.donation ALTER COLUMN "transactionId" DROP NOT NULL`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE public.donation ALTER COLUMN "transactionId" SET NOT NULL`, + ); + } } diff --git a/migration/1702364570535-create_anchor_contract_address_table.ts b/migration/1702364570535-create_anchor_contract_address_table.ts new file mode 100644 index 000000000..46d478f08 --- /dev/null +++ b/migration/1702364570535-create_anchor_contract_address_table.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class createAnchorContractAddressTable1702364570535 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "anchor_contract_address" ( + "id" SERIAL PRIMARY KEY, + "networkId" INTEGER NOT NULL, + "isActive" BOOLEAN DEFAULT false, + "address" TEXT NOT NULL, + "projectId" INTEGER NULL, + "creatorId" INTEGER NULL, + "ownerId" INTEGER NULL, + "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "UQ_address_networkId_project" UNIQUE ("address", "networkId", "projectId") + ); + + CREATE INDEX "IDX_address" ON "anchor_contract_address" ("address"); + CREATE INDEX "IDX_networkId" ON "anchor_contract_address" ("networkId"); + CREATE INDEX "IDX_projectId" ON "anchor_contract_address" ("projectId"); + CREATE INDEX "IDX_creatorId" ON "anchor_contract_address" ("creatorId"); + CREATE INDEX "IDX_ownerId" ON "anchor_contract_address" ("ownerId"); + + `); + + await queryRunner.query(` + ALTER TABLE "anchor_contract_address" + ADD CONSTRAINT "FK_anchor_contract_address_project" + FOREIGN KEY ("projectId") REFERENCES "project"("id") + ON DELETE SET NULL; + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "anchor_contract_address" + DROP CONSTRAINT "FK_anchor_contract_address_project"; + `); + + await queryRunner.query(` + DROP TABLE "anchor_contract_address"; + `); + } +} diff --git a/src/entities/anchorContractAddress.ts b/src/entities/anchorContractAddress.ts new file mode 100644 index 000000000..ec7e0319a --- /dev/null +++ b/src/entities/anchorContractAddress.ts @@ -0,0 +1,76 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, + RelationId, + Unique, + UpdateDateColumn, +} from 'typeorm'; +import { Field, ID, ObjectType } from 'type-graphql'; +import { Project } from './project'; +import { User } from './user'; + +@Entity() +@ObjectType() +@Unique(['address', 'networkId', 'project']) +export class AnchorContractAddress extends BaseEntity { + @Field(type => ID) + @PrimaryGeneratedColumn() + readonly id: number; + + @Field() + @Column('boolean', { default: false }) + isActive: boolean; + + @Field() + @Column() + networkId: number; + + @Index() + @Field() + @Column() + address: string; + + @Index() + @Field(type => Project) + @ManyToOne(type => Project) + project: Project; + + @RelationId((relatedAddress: AnchorContractAddress) => relatedAddress.project) + @Column({ nullable: true }) + projectId: number; + + @Index() + @Field(type => User, { nullable: true }) + @ManyToOne(type => User, { eager: true, nullable: true }) + creator: User; + + @RelationId( + (anchorContractAddress: AnchorContractAddress) => + anchorContractAddress.creator, + ) + @Column({ nullable: true }) + creatorId: number; + + @Index() + @Field(type => User, { nullable: true }) + @ManyToOne(type => User, { eager: true, nullable: true }) + owner: User; + + @RelationId( + (anchorContractAddress: AnchorContractAddress) => + anchorContractAddress.owner, + ) + @Column({ nullable: true }) + ownerId: number; + + @UpdateDateColumn() + updatedAt: Date; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/src/entities/project.ts b/src/entities/project.ts index 11cc9278d..0d8191f33 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -53,6 +53,7 @@ import { import { EstimatedMatching } from '../types/qfTypes'; import { Campaign } from './campaign'; import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView'; +import { AnchorContractAddress } from './anchorContractAddress'; // tslint:disable-next-line:no-var-requires const moment = require('moment'); @@ -278,6 +279,16 @@ export class Project extends BaseEntity { }) addresses?: ProjectAddress[]; + @Field(type => [AnchorContractAddress], { nullable: true }) + @OneToMany( + type => AnchorContractAddress, + anchorContractAddress => anchorContractAddress.project, + { + eager: true, + }, + ) + anchorContracts?: AnchorContractAddress[]; + @Index() @Field(type => ProjectStatus) @ManyToOne(type => ProjectStatus) diff --git a/src/entities/recurringDonation.ts b/src/entities/recurringDonation.ts new file mode 100644 index 000000000..3d6a1bce3 --- /dev/null +++ b/src/entities/recurringDonation.ts @@ -0,0 +1,60 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, + RelationId, + Unique, + UpdateDateColumn, +} from 'typeorm'; +import { Field, ID, ObjectType } from 'type-graphql'; +import { Project } from './project'; +import { User } from './user'; + +@Entity() +@ObjectType() +@Unique(['txHash', 'networkId', 'project']) +// TODO entity is not completed +export class RecurringDonation extends BaseEntity { + @Field(type => ID) + @PrimaryGeneratedColumn() + readonly id: number; + + @Field() + @Column() + networkId: number; + + @Index() + @Field() + @Column() + txHash: string; + + @Index() + @Field(type => Project) + @ManyToOne(type => Project) + project: Project; + + @RelationId( + (recurringDonation: RecurringDonation) => recurringDonation.project, + ) + @Column({ nullable: true }) + projectId: number; + + @Index() + @Field(type => User, { nullable: true }) + @ManyToOne(type => User, { eager: true, nullable: true }) + donor: User; + + @RelationId((recurringDonation: RecurringDonation) => recurringDonation.donor) + @Column({ nullable: true }) + donorId: number; + + @UpdateDateColumn() + updatedAt: Date; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/src/repositories/anchorContractAddressRepository.ts b/src/repositories/anchorContractAddressRepository.ts new file mode 100644 index 000000000..4d4a8044c --- /dev/null +++ b/src/repositories/anchorContractAddressRepository.ts @@ -0,0 +1,31 @@ +import { AnchorContractAddress } from '../entities/anchorContractAddress'; +import { Project } from '../entities/project'; +import { User } from '../entities/user'; + +export const addNewAnchorAddress = async (params: { + project: Project; + owner: User; + creator: User; + address: string; + networkId: number; +}): Promise => { + const anchorContractAddress = await AnchorContractAddress.create({ + ...params, + isActive: true, + }); + return anchorContractAddress.save(); +}; + +export const findActiveAnchorAddress = async (params: { + projectId: number; + networkId: number; +}) => { + const { projectId, networkId } = params; + return AnchorContractAddress.findOne({ + where: { + isActive: true, + networkId, + projectId, + }, + }); +}; diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 1cd9d755e..1bd899b49 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -20,6 +20,7 @@ export const findProjectById = (projectId: number): Promise => { .leftJoinAndSelect('project.status', 'status') .leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.addresses', 'addresses') + .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') .leftJoinAndSelect('project.qfRounds', 'qfRounds') .leftJoin('project.adminUser', 'user') .addSelect(publicSelectionFields) diff --git a/src/resolvers/anchorContractAddressResolver.ts b/src/resolvers/anchorContractAddressResolver.ts new file mode 100644 index 000000000..790099184 --- /dev/null +++ b/src/resolvers/anchorContractAddressResolver.ts @@ -0,0 +1,63 @@ +import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'; + +import { QfRoundHistory } from '../entities/qfRoundHistory'; +import { getQfRoundHistory } from '../repositories/qfRoundHistoryRepository'; +import { AnchorContractAddress } from '../entities/anchorContractAddress'; +import { findProjectById } from '../repositories/projectRepository'; +import { + errorMessages, + i18n, + translationErrorMessagesKeys, +} from '../utils/errorMessages'; +import { + addNewAnchorAddress, + findActiveAnchorAddress, +} from '../repositories/anchorContractAddressRepository'; +import { ApolloContext } from '../types/ApolloContext'; +import { findUserById } from '../repositories/userRepository'; + +@Resolver(of => AnchorContractAddress) +export class AnchorContractAddressResolver { + @Query(() => AnchorContractAddress, { nullable: true }) + async addAnchorContractAddress( + @Ctx() ctx: ApolloContext, + @Arg('projectId', () => Int) projectId: number, + @Arg('networkId', () => Int) networkId: number, + @Arg('address', () => String) address: string, + ): Promise { + const userId = ctx?.req?.user?.userId; + const creatorUser = await findUserById(userId); + if (!creatorUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } + const project = await findProjectById(projectId); + if (!project) { + throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); + } + const currentAnchorProjectAddress = await findActiveAnchorAddress({ + projectId, + networkId, + }); + + if ( + currentAnchorProjectAddress && + project.adminUser.id !== creatorUser.id + ) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.THERE_IS_AN_ACTIVE_ANCHOR_ADDRESS_FOR_THIS_PROJECT_ONLY_ADMIN_CAN_CHANGE_IT, + ), + ); + } + + // Validate anchor address, the owner of contract must be the project owner + + return addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: creatorUser, + address, + networkId, + }); + } +} diff --git a/src/resolvers/resolvers.ts b/src/resolvers/resolvers.ts index 7cfa56d1a..9a97d5748 100644 --- a/src/resolvers/resolvers.ts +++ b/src/resolvers/resolvers.ts @@ -19,6 +19,7 @@ import { ChainvineResolver } from './chainvineResolver'; import { QfRoundResolver } from './qfRoundResolver'; import { QfRoundHistoryResolver } from './qfRoundHistoryResolver'; import { ProjectUserInstantPowerViewResolver } from './instantPowerResolver'; +import { AnchorContractAddressResolver } from './anchorContractAddressResolver'; export const getResolvers = (): Function[] => { return [ @@ -45,5 +46,7 @@ export const getResolvers = (): Function[] => { CampaignResolver, QfRoundResolver, QfRoundHistoryResolver, + + AnchorContractAddressResolver, ]; }; diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 07414c079..85bcfd8d2 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -254,6 +254,8 @@ export const translationErrorMessagesKeys = { YOU_DONT_HAVE_ACCESS_TO_DEACTIVATE_THIS_PROJECT: 'YOU_DONT_HAVE_ACCESS_TO_DEACTIVATE_THIS_PROJECT', PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND', + THERE_IS_AN_ACTIVE_ANCHOR_ADDRESS_FOR_THIS_PROJECT_ONLY_ADMIN_CAN_CHANGE_IT: + 'There is already an anchor address for this project, only project owner can change it', PROJECT_IS_NOT_ACTIVE: 'PROJECT_IS_NOT_ACTIVE', INVALID_FUNCTION: 'INVALID_FUNCTION', PROJECT_UPDATE_NOT_FOUND: 'PROJECT_UPDATE_NOT_FOUND',