From e2a16153e9bbfca643eae7daea24df4cf2272d92 Mon Sep 17 00:00:00 2001 From: Toms Date: Tue, 12 Aug 2025 18:44:46 +0300 Subject: [PATCH] test: e2e REST manual settlements --- .../settlements/fungible/manualSettlements.ts | 293 ++++++++++++++++++ tests/src/rest/interfaces.ts | 1 + tests/src/rest/settlements/client.ts | 34 +- tests/src/rest/settlements/params.ts | 4 +- 4 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 tests/src/__tests__/rest/settlements/fungible/manualSettlements.ts diff --git a/tests/src/__tests__/rest/settlements/fungible/manualSettlements.ts b/tests/src/__tests__/rest/settlements/fungible/manualSettlements.ts new file mode 100644 index 0000000..26bb2f1 --- /dev/null +++ b/tests/src/__tests__/rest/settlements/fungible/manualSettlements.ts @@ -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; + let assetId: string; + let venueId: string; + let instructionId: string; + let createInstructionParams: ReturnType; + 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, + }, + ]), + }); + }); +}); diff --git a/tests/src/rest/interfaces.ts b/tests/src/rest/interfaces.ts index d5b4b81..56f9298 100644 --- a/tests/src/rest/interfaces.ts +++ b/tests/src/rest/interfaces.ts @@ -20,6 +20,7 @@ export interface PolymeshLocalSettings { export interface ResultSet { results: T[]; + total: string; } interface SingleResult { type: 'single'; diff --git a/tests/src/rest/settlements/client.ts b/tests/src/rest/settlements/client.ts index a4b98a3..2e6bcfe 100644 --- a/tests/src/rest/settlements/client.ts +++ b/tests/src/rest/settlements/client.ts @@ -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, @@ -72,10 +72,36 @@ export class Settlements { }); } - public async getAffirmations(instructionId: string): Promise { + public async getAffirmations( + instructionId: string + ): Promise> { return this.client.get(`/instructions/${instructionId}/affirmations`); } + public async getVenue(venueId: string): Promise { + return this.client.get(`/venues/${venueId}`); + } + + public async updateVenue( + venueId: string, + params: { description?: string; type?: string }, + txBase: TxBase + ): Promise { + return this.client.post(`/venues/${venueId}/modify`, { + ...txBase, + ...params, + }); + } + + public async executeInstructionManually( + instructionId: string, + txBase: TxBase + ): Promise { + return this.client.post(`/instructions/${instructionId}/execute-manually`, { + ...txBase, + }); + } + public async validateLeg({ asset, toPortfolio, @@ -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> { + return this.client.get(`/identities/${did}/pending-instructions`); + } } diff --git a/tests/src/rest/settlements/params.ts b/tests/src/rest/settlements/params.ts index 03854da..eed7d5e 100644 --- a/tests/src/rest/settlements/params.ts +++ b/tests/src/rest/settlements/params.ts @@ -13,7 +13,8 @@ export const fungibleInstructionParams = ( from: string, to: string, base: TxBase, - extras: TxExtras = {} + extras: TxExtras = {}, + endAfterBlock?: string ) => ({ memo: 'Testing settlements', @@ -32,6 +33,7 @@ export const fungibleInstructionParams = ( asset: assetId, }, ], + endAfterBlock, ...extras, ...base, } as const);