-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
src/app/modules/campaign/events/campaign_reimbursement_processed.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
test/unit/modules/campaign/commands/__snapshots__/reimburse_command.spec.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
38
test/unit/modules/campaign/commands/reimburse_command.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |