Skip to content

Commit

Permalink
WIP Adding recurring donations and anchorContractAddress entities
Browse files Browse the repository at this point in the history
related to #1142
  • Loading branch information
mohammadranjbarz committed Dec 12, 2023
1 parent 4ef7d20 commit f7c452a
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 10 deletions.
24 changes: 14 additions & 10 deletions migration/1701979390554-addnullableTransactionId.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await queryRunner.query(`ALTER TABLE public.donation ALTER COLUMN "transactionId" DROP NOT NULL`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE public.donation ALTER COLUMN "transactionId" SET NOT NULL`);
}
export class addnullableTransactionId1701979390554
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE public.donation ALTER COLUMN "transactionId" DROP NOT NULL`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE public.donation ALTER COLUMN "transactionId" SET NOT NULL`,
);
}
}
47 changes: 47 additions & 0 deletions migration/1702364570535-create_anchor_contract_address_table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class createAnchorContractAddressTable1702364570535
implements MigrationInterface
{
async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`
ALTER TABLE "anchor_contract_address"
DROP CONSTRAINT "FK_anchor_contract_address_project";
`);

await queryRunner.query(`
DROP TABLE "anchor_contract_address";
`);
}
}
76 changes: 76 additions & 0 deletions src/entities/anchorContractAddress.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions src/entities/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions src/entities/recurringDonation.ts
Original file line number Diff line number Diff line change
@@ -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;
}
31 changes: 31 additions & 0 deletions src/repositories/anchorContractAddressRepository.ts
Original file line number Diff line number Diff line change
@@ -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<AnchorContractAddress> => {
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,
},
});
};
1 change: 1 addition & 0 deletions src/repositories/projectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const findProjectById = (projectId: number): Promise<Project | null> => {
.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)
Expand Down
63 changes: 63 additions & 0 deletions src/resolvers/anchorContractAddressResolver.ts
Original file line number Diff line number Diff line change
@@ -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<AnchorContractAddress> {
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,
});
}
}
3 changes: 3 additions & 0 deletions src/resolvers/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand All @@ -45,5 +46,7 @@ export const getResolvers = (): Function[] => {
CampaignResolver,
QfRoundResolver,
QfRoundHistoryResolver,

AnchorContractAddressResolver,
];
};
2 changes: 2 additions & 0 deletions src/utils/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit f7c452a

Please sign in to comment.