Skip to content

Commit

Permalink
feat:add reimburse command
Browse files Browse the repository at this point in the history
  • Loading branch information
reyraa committed Nov 18, 2024
1 parent 02a0fa3 commit 2afd0eb
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 0 deletions.
137 changes: 137 additions & 0 deletions src/app/modules/campaign/commands/reimburse_command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/* eslint-disable class-methods-use-this */
import { Modules, StateMachine } from 'klayr-sdk';
import { CampaignStore } from '../stores/campaign';
import { TREASURY_ADDRESS, TRANSFER_FEE } from '../constants';
import { CampaignReimbursementProcessed } from '../events/campaign_reimbursement_processed';
import { reimburseCommandParamsSchema } from '../schemas';
import { CampaignStatus, Contribution, ReimburseCommandParams } from '../types';
import { ContributionStore } from '../stores/contribution';
import { getContributionId } from '../utils';

export class ReimburseCommand extends Modules.BaseCommand {
public addDependencies(tokenMethod: Modules.Token.TokenMethod) {
this._tokenMethod = tokenMethod;
}

// eslint-disable-next-line @typescript-eslint/require-await
public async verify(
context: StateMachine.CommandVerifyContext<ReimburseCommandParams>,
): Promise<StateMachine.VerificationResult> {
const {
params,
transaction: { senderAddress },
} = context;
const campaignStore = this.stores.get(CampaignStore);
const contributionStore = this.stores.get(ContributionStore);
const campaignId = Buffer.from(params.campaignId, 'hex');

const campaignExists = await campaignStore.has(context, campaignId);
if (!campaignExists) {
throw new Error('Campaign does not exist.');
}
const campaign = await campaignStore.get(context, campaignId);
if (campaign.status === CampaignStatus.Failed) {
throw new Error('This campaign is failed. Reimbursements are accomplished');
}

const potentialIds = campaign.contributionTiers.map(({ apiId }) =>
getContributionId({
campaignId: params.campaignId,
address: senderAddress,
tierId: apiId,
}),
);

const contributionsExist: boolean[] = [];
for await (const id of potentialIds) {
const contributionExist = await contributionStore.has(context, id);
contributionsExist.push(contributionExist);
}
if (!contributionsExist.some(item => item)) {
throw new Error('You have not contributed in this campaign.');
}

return { status: StateMachine.VerifyStatus.OK };
}

public async execute(
context: StateMachine.CommandExecuteContext<ReimburseCommandParams>,
): Promise<void> {
const {
params,
transaction: { senderAddress },
chainID,
} = context;
const methodContext = context.getMethodContext();
const campaignStore = this.stores.get(CampaignStore);
const contributionStore = this.stores.get(ContributionStore);
const tokenID = Buffer.concat([chainID, Buffer.alloc(4)]);
const campaignId = Buffer.from(params.campaignId, 'hex');

const campaign = await campaignStore.get(context, campaignId);

const potentialIds = campaign.contributionTiers.map(({ apiId }) =>
getContributionId({
campaignId: params.campaignId,
address: senderAddress,
tierId: apiId,
}),
);
const contributions: (Contribution & { id: Buffer })[] = [];
for await (const id of potentialIds) {
const contributionExist = await contributionStore.has(context, id);
if (contributionExist) {
const contribution = await contributionStore.get(context, id);
contributions.push({
...contribution,
id,
});
}
}

// Reimburse the contribution amount
const totalContributions = contributions.reduce((total, item) => {
const sum = total + item.amount;
return sum;
}, BigInt(0));
const payable =
totalContributions <= campaign.currentFunding ? totalContributions : campaign.currentFunding;
await this._tokenMethod.transfer(
methodContext,
TREASURY_ADDRESS,
senderAddress,
tokenID,
payable - TRANSFER_FEE,
);

// Update campaign
const remainingFunds = campaign.currentFunding - payable;
const status = remainingFunds > 0 ? CampaignStatus.Failing : CampaignStatus.Failed;
const updatedCampaign = {
...campaign,
currentFunding: remainingFunds,
status,
};
await campaignStore.set(context, campaignId, updatedCampaign);

// Delete the contributions
for await (const item of contributions) {
await contributionStore.del(context, item.id);
}

// Fire event
const reimbursementProcessed = this.events.get(CampaignReimbursementProcessed);
reimbursementProcessed.add(
context,
{
submitter: senderAddress,
amount: payable - TRANSFER_FEE,
campaignId,
},
[senderAddress],
);
}

public schema = reimburseCommandParamsSchema;
private _tokenMethod!: Modules.Token.TokenMethod;
}
1 change: 1 addition & 0 deletions src/app/modules/campaign/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { address as cryptoAddress } from '@klayr/cryptography';

export const DEV_SHARE = BigInt(15) / BigInt(100);
export const TRANSFER_FEE = BigInt(200000);

const devAddress32 = 'klyh96jgzfftzff2fta2zvsmba9mvs5cnz9ahr3ke';
const treasuryAddress32 = 'klyyg9ujmpkbn7ex96ejedhfrkj6avryn5nwgngbp';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable @typescript-eslint/member-ordering */
// eslint-disable-next-line import/no-extraneous-dependencies
import { Modules } from 'klayr-framework';
import { CampaignReimbursedEventData } from '../types';
import { campaignReimbursementProcessedEventDataSchema } from '../schemas';

export class CampaignReimbursementProcessed extends Modules.BaseEvent<CampaignReimbursedEventData> {
public schema = campaignReimbursementProcessedEventDataSchema;

public log(ctx: Modules.EventQueuer, data: CampaignReimbursedEventData): void {
this.add(ctx, data, [data.submitter]);
}
}
2 changes: 2 additions & 0 deletions src/app/modules/campaign/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ContributeCommand } from './commands/contribute_command';
import { CreateCommand } from './commands/create_command';
import { PublishCommand } from './commands/publish_command';
import { PayoutCommand } from './commands/payout_command';
import { ReimburseCommand } from './commands/reimburse_command';

// Events
import { CampaignCreated } from './events/campaign_created';
Expand All @@ -34,6 +35,7 @@ export class CampaignModule extends Modules.BaseModule {
new PublishCommand(this.stores, this.events),
new ContributeCommand(this.stores, this.events),
new PayoutCommand(this.stores, this.events),
new ReimburseCommand(this.stores, this.events),
];

public constructor() {
Expand Down
34 changes: 34 additions & 0 deletions src/app/modules/campaign/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,19 @@ export const contributeCommandParamsSchema = {
},
};

export const reimburseCommandParamsSchema = {
$id: 'campaign/reimburse',
title: 'Reimburse transaction asset for campaign module',
type: 'object',
required: ['campaignId'],
properties: {
campaignId: {
dataType: 'string',
fieldNumber: 1,
},
},
};

// Events
export const campaignCreatedEventDataSchema = {
$id: '/campaign/events/campaignCreatedEventData',
Expand Down Expand Up @@ -285,3 +298,24 @@ export const campaignPayoutProcessedEventDataSchema = {
},
},
};

export const campaignReimbursementProcessedEventDataSchema = {
$id: '/campaign/events/campaignReimbursementProcessedEventDataSchema',
type: 'object',
required: ['submitter', 'campaignId', 'amount'],
properties: {
submitter: {
dataType: 'bytes',
format: 'klayr32',
fieldNumber: 1,
},
campaignId: {
dataType: 'bytes',
fieldNumber: 2,
},
amount: {
dataType: 'uint64',
fieldNumber: 3,
},
},
};
10 changes: 10 additions & 0 deletions src/app/modules/campaign/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export interface PayoutCommandParams {
campaignId: string;
}

export interface ReimburseCommandParams {
campaignId: string;
}

export interface CreateCommandParams {
softGoal: string;
hardGoal: string;
Expand Down Expand Up @@ -139,3 +143,9 @@ export interface CampaignPayoutProcessedEventData {
campaignId: Buffer;
amount: bigint;
}

export interface CampaignReimbursedEventData {
submitter: Buffer;
campaignId: Buffer;
amount: bigint;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ReimburseCommand constructor should have valid schema 1`] = `
{
"$id": "campaign/reimburse",
"properties": {
"campaignId": {
"dataType": "string",
"fieldNumber": 1,
},
},
"required": [
"campaignId",
],
"title": "Reimburse transaction asset for campaign module",
"type": "object",
}
`;
38 changes: 38 additions & 0 deletions test/unit/modules/campaign/commands/reimburse_command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ReimburseCommand } from '../../../../../src/app/modules/campaign/commands/reimburse_command';
import { CampaignModule } from '../../../../../src/app/modules/campaign/module';

describe('ReimburseCommand', () => {
let command: ReimburseCommand;
const module = new CampaignModule();

beforeEach(() => {
command = new ReimburseCommand(module.stores, module.events);
});

describe('constructor', () => {
it('should have valid name', () => {
expect(command.name).toBe('reimburse');
});

it('should have valid schema', () => {
expect(command.schema).toMatchSnapshot();
});
});

describe('verify', () => {
describe('schema validation', () => {
it.todo('should throw errors for invalid schema');
it.todo('should be ok for valid schema');
});
});

describe('execute', () => {
describe('valid cases', () => {
it.todo('should update the state store');
});

describe('invalid cases', () => {
it.todo('should throw error');
});
});
});

0 comments on commit 2afd0eb

Please sign in to comment.