Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
293 changes: 293 additions & 0 deletions tests/src/__tests__/rest/settlements/fungible/manualSettlements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import { expectBasicTxInfo } from '~/__tests__/rest/utils';
import { TestFactory } from '~/helpers';
import { RestClient } from '~/rest';
import { createAssetParams } from '~/rest/assets/params';
import { ProcessMode } from '~/rest/common';
import { Identity } from '~/rest/identities/interfaces';
import { RestSuccessResult } from '~/rest/interfaces';
import { fungibleInstructionParams, venueParams } from '~/rest/settlements';
import { awaitMiddlewareSyncedForRestApi } from '~/util';

const handles = ['issuer', 'investor'];
let factory: TestFactory;

describe('Settlements - REST API (Manual Settlement Flow)', () => {
let restClient: RestClient;
let issuer: Identity;
let investor: Identity;
let signer: string;
let assetParams: ReturnType<typeof createAssetParams>;
let assetId: string;
let venueId: string;
let instructionId: string;
let createInstructionParams: ReturnType<typeof fungibleInstructionParams>;
let endAfterBlock: string;

beforeAll(async () => {
factory = await TestFactory.create({ handles });
({ restClient } = factory);
issuer = factory.getSignerIdentity(handles[0]);
investor = factory.getSignerIdentity(handles[1]);
signer = issuer.signer;

assetParams = createAssetParams({
options: { processMode: ProcessMode.Submit, signer },
});
});

afterAll(async () => {
await factory.close();
});

it('should create a fungible asset with initial supply', async () => {
assetId = await restClient.assets.createAndGetAssetId(assetParams);

createInstructionParams = fungibleInstructionParams(assetId, issuer.did, investor.did, {
options: { processMode: ProcessMode.Submit, signer },
});

expect(assetId).toBeTruthy();
});

it('should create a venue, fetch details and update venue', async () => {
const venueTx = await restClient.settlements.createVenue(
venueParams({
options: { signer, processMode: ProcessMode.Submit },
})
);

expect(venueTx).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTag: 'settlement.createVenue',
type: 'single',
...expectBasicTxInfo,
},
]),
});

venueId = (venueTx as RestSuccessResult).venue as string;
expect(venueId).toBeTruthy();

let venueDetails = await restClient.settlements.getVenue(venueId);
expect(venueDetails).toMatchObject({
description: expect.any(String),
type: 'Exchange',
owner: issuer.did,
});

const updatedVenueTx = await restClient.settlements.updateVenue(
venueId,
{
description: 'Updated Venue Description',
type: 'Other',
},
{
options: { signer, processMode: ProcessMode.Submit },
}
);

expect(updatedVenueTx).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTags: ['settlement.updateVenueDetails', 'settlement.updateVenueType'],
type: 'batch',
...expectBasicTxInfo,
},
]),
});

venueDetails = await restClient.settlements.getVenue(venueId);
expect(venueDetails).toMatchObject({
description: 'Updated Venue Description',
type: 'Other',
});
});

// TODO: dryRun needs to be checked - it doesn't seralize the return value correctly -> results in 500 error
it.skip('should check if the instruction will run using dry run', async () => {
const dryRunInstruction = await restClient.settlements.createInstruction(venueId, {
...createInstructionParams,
options: { processMode: ProcessMode.DryRun, signer },
});

expect(dryRunInstruction).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTag: 'settlement.createInstruction',
type: 'single',
...expectBasicTxInfo,
},
]),
});
});

it('should create a settlement instruction', async () => {
const createInstructionTx = await restClient.settlements.createInstruction(venueId, {
...createInstructionParams,
options: { processMode: ProcessMode.Submit, signer },
});

expect(createInstructionTx).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTag: 'settlement.addAndAffirmWithMediators',
type: 'single',
...expectBasicTxInfo,
},
]),
});

instructionId = (createInstructionTx as RestSuccessResult).instruction as string;
expect(instructionId).toBeTruthy();
});

it('should reject the instruction via receiver', async () => {
const rejectInstructionTx = await restClient.settlements.rejectInstruction(instructionId, {
options: { processMode: ProcessMode.Submit, signer: investor.signer },
});

expect(rejectInstructionTx).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTag: 'settlement.rejectInstructionWithCount',
type: 'single',
...expectBasicTxInfo,
},
]),
});
});

it('should create a instruction to be settled manually', async () => {
const latestBlock = await restClient.network.getLatestBlock();
endAfterBlock = (Number(latestBlock.id) + 5).toString();

const createInstructionResult = await restClient.settlements.createInstruction(venueId, {
...createInstructionParams,
endAfterBlock: endAfterBlock.toString(),
options: { processMode: ProcessMode.Submit, signer },
});

expect(createInstructionResult).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTag: 'settlement.addAndAffirmWithMediators',
type: 'single',
...expectBasicTxInfo,
},
]),
});

instructionId = (createInstructionResult as RestSuccessResult).instruction as string;
expect(parseInt(instructionId)).toEqual(expect.any(Number));

await awaitMiddlewareSyncedForRestApi(createInstructionResult, restClient, new BigNumber(1));
});

it('should get the instruction details, legs and status', async () => {
const instructionDetails = await restClient.settlements.getInstruction(instructionId);

expect(instructionDetails).toMatchObject({
venue: venueId,
// TODO: This is not correct, REST API maps endAfterBlock to endBlock, it should be endAfterBlock
endBlock: endAfterBlock,
status: 'Pending',
type: 'SettleManual',
legs: expect.arrayContaining([
{
asset: assetId,
amount: '10',
from: {
did: issuer.did,
},
to: {
did: investor.did,
},
type: 'onChain',
},
]),
});
});

it('should approve the instruction via receiver', async () => {
const approveInstructionTx = await restClient.settlements.affirmInstruction(instructionId, {
options: { processMode: ProcessMode.Submit, signer: investor.signer },
});

expect(approveInstructionTx).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTag: 'settlement.affirmInstructionWithCount',
type: 'single',
...expectBasicTxInfo,
},
]),
});

await awaitMiddlewareSyncedForRestApi(approveInstructionTx, restClient, new BigNumber(1));

const results = await restClient.settlements.getAffirmations(instructionId);

expect(results).toMatchObject({
results: expect.arrayContaining([
{
identity: issuer.did,
status: 'Affirmed',
},
{
identity: investor.did,
status: 'Affirmed',
},
]),
total: '2',
});
});

it('should withdraw affirmation via receiver', async () => {
const withdrawAffirmationTx = await restClient.settlements.withdrawAffirmation(instructionId, {
options: { processMode: ProcessMode.Submit, signer: investor.signer },
});

await awaitMiddlewareSyncedForRestApi(withdrawAffirmationTx, restClient, new BigNumber(1));

const result = await restClient.settlements.getAffirmations(instructionId);
expect(result).toMatchObject({
results: expect.arrayContaining([
{
identity: issuer.did,
status: 'Affirmed',
},
]),
total: '1',
});
});

it('should execute the instruction manually', async () => {
const affirmResult = await restClient.settlements.affirmInstruction(instructionId, {
options: { processMode: ProcessMode.Submit, signer: investor.signer },
});

await awaitMiddlewareSyncedForRestApi(affirmResult, restClient, new BigNumber(1));

const { results } = await restClient.settlements.getPendingInstructions(investor.did);
expect(results).toHaveLength(0);

const executeInstructionTx = await restClient.settlements.executeInstructionManually(
instructionId,
{
options: { processMode: ProcessMode.Submit, signer: investor.signer },
}
);

expect(executeInstructionTx).toMatchObject({
transactions: expect.arrayContaining([
{
transactionTag: 'settlement.executeManualInstruction',
type: 'single',
...expectBasicTxInfo,
},
]),
});
});
});
1 change: 1 addition & 0 deletions tests/src/rest/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface PolymeshLocalSettings {

export interface ResultSet<T> {
results: T[];
total: string;
}
interface SingleResult {
type: 'single';
Expand Down
34 changes: 32 additions & 2 deletions tests/src/rest/settlements/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RestClient } from '~/rest/client';
import { TxBase } from '~/rest/common';
import { PostResult } from '~/rest/interfaces';
import { PostResult, ResultSet } from '~/rest/interfaces';
import {
fungibleInstructionParams,
nftInstructionParams,
Expand Down Expand Up @@ -72,10 +72,36 @@ export class Settlements {
});
}

public async getAffirmations(instructionId: string): Promise<unknown> {
public async getAffirmations(
instructionId: string
): Promise<ResultSet<{ identity: string; status: string }>> {
return this.client.get(`/instructions/${instructionId}/affirmations`);
}

public async getVenue(venueId: string): Promise<unknown> {
return this.client.get(`/venues/${venueId}`);
}

public async updateVenue(
venueId: string,
params: { description?: string; type?: string },
txBase: TxBase
): Promise<PostResult> {
return this.client.post(`/venues/${venueId}/modify`, {
...txBase,
...params,
});
}

public async executeInstructionManually(
instructionId: string,
txBase: TxBase
): Promise<PostResult> {
return this.client.post(`/instructions/${instructionId}/execute-manually`, {
...txBase,
});
}

public async validateLeg({
asset,
toPortfolio,
Expand All @@ -95,4 +121,8 @@ export class Settlements {
`/leg-validations?asset=${asset}&toPortfolio=${toPortfolio}&toDid=${toDid}&fromPortfolio=${fromPortfolio}&fromDid=${fromDid}&amount=${amount}`
);
}

public async getPendingInstructions(did: string): Promise<ResultSet<{ id: string }>> {
return this.client.get(`/identities/${did}/pending-instructions`);
}
}
4 changes: 3 additions & 1 deletion tests/src/rest/settlements/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const fungibleInstructionParams = (
from: string,
to: string,
base: TxBase,
extras: TxExtras = {}
extras: TxExtras = {},
endAfterBlock?: string
) =>
({
memo: 'Testing settlements',
Expand All @@ -32,6 +33,7 @@ export const fungibleInstructionParams = (
asset: assetId,
},
],
endAfterBlock,
...extras,
...base,
} as const);
Expand Down