From 3b0c07ccfe185f08b4afd0f6e01fca20dd7e5a14 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:03 +0530 Subject: [PATCH 01/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20bump=20SDK=20to?= =?UTF-8?q?=20v31.0.0-beta.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops chain v7 support; the SDK now targets chain v8 only. Also bumps polymesh-types to ^7.5.0, which the SDK requires. BREAKING CHANGE: chain v7 is no longer supported. The SDK throws on initialization when pointed at a v7 node, so deployments must run a chain 8.0.x or 8.1.x node. --- package.json | 4 ++-- yarn.lock | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index bead5571..0a0e55f6 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,8 @@ "@polymeshassociation/fireblocks-signing-manager": "^3.0.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^4.1.0", "@polymeshassociation/local-signing-manager": "^4.1.1", - "@polymeshassociation/polymesh-sdk": "v30.0.0", - "@polymeshassociation/polymesh-types": "^7.4.0", + "@polymeshassociation/polymesh-sdk": "31.0.0-beta.7", + "@polymeshassociation/polymesh-types": "^7.5.0", "@polymeshassociation/signing-manager-types": "^3.7.1", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/yarn.lock b/yarn.lock index c8e35006..d00d0e45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2797,15 +2797,15 @@ __metadata: languageName: node linkType: hard -"@polymeshassociation/polymesh-sdk@npm:v30.0.0": - version: 30.0.0 - resolution: "@polymeshassociation/polymesh-sdk@npm:30.0.0" +"@polymeshassociation/polymesh-sdk@npm:31.0.0-beta.7": + version: 31.0.0-beta.7 + resolution: "@polymeshassociation/polymesh-sdk@npm:31.0.0-beta.7" dependencies: "@apollo/client": "npm:^3.8.1" "@polkadot/api": "npm:16.5.2" "@polkadot/util": "npm:13.5.9" "@polkadot/util-crypto": "npm:13.5.9" - "@polymeshassociation/polymesh-types": "npm:^7.4.0" + "@polymeshassociation/polymesh-types": "npm:^7.5.0" bignumber.js: "npm:9.0.1" cross-fetch: "npm:^4.0.0" dayjs: "npm:1.11.9" @@ -2817,20 +2817,20 @@ __metadata: semver: "npm:^7.5.4" ts-morph: "npm:^25.0.1" ws: "npm:^8.18.3" - checksum: 10c0/c1689fc9ef7632d9f7743dce0e4250f8a7005e161fac034b3d0f02530dea50152a9b21fd5e6bb08d5f5bd2ace22494b9194ba0129d36c966c9310673c120f4e5 + checksum: 10c0/15239455c18e2af83ee4e5b01ece76a36905d705713cfad0d8674fb8ba56163144d274f696538c2be9a1ad8d58f7cd2bfe1b92facd2cfcce3f00ede0a32d2e4b languageName: node linkType: hard -"@polymeshassociation/polymesh-types@npm:^7.4.0": - version: 7.4.0 - resolution: "@polymeshassociation/polymesh-types@npm:7.4.0" +"@polymeshassociation/polymesh-types@npm:^7.5.0": + version: 7.5.0 + resolution: "@polymeshassociation/polymesh-types@npm:7.5.0" dependencies: "@polkadot/api": "npm:16.5.2" "@polkadot/api-base": "npm:16.5.2" "@polkadot/rpc-core": "npm:16.5.2" "@polkadot/types": "npm:16.5.2" "@polkadot/types-codec": "npm:16.5.2" - checksum: 10c0/e0ba7d34eee7171f8ed02dfe47295fdbb55bcc4b315d327a724312242fcea3b485064f64a453c9f13cdd1b75197626714482a0e16fe15274223bd3f150c554b8 + checksum: 10c0/cdb8191954e864e5eee65aca83205b6e2e002c7bcf78b6bc6a24d101bc09e0d904d7015a08dd3734cdead839bc457b9acb884d5799fae73a52ab4de131d7c03c languageName: node linkType: hard @@ -11409,8 +11409,8 @@ __metadata: "@polymeshassociation/fireblocks-signing-manager": "npm:^3.0.0" "@polymeshassociation/hashicorp-vault-signing-manager": "npm:^4.1.0" "@polymeshassociation/local-signing-manager": "npm:^4.1.1" - "@polymeshassociation/polymesh-sdk": "npm:v30.0.0" - "@polymeshassociation/polymesh-types": "npm:^7.4.0" + "@polymeshassociation/polymesh-sdk": "npm:31.0.0-beta.7" + "@polymeshassociation/polymesh-types": "npm:^7.5.0" "@polymeshassociation/signing-manager-types": "npm:^3.7.1" "@semantic-release/changelog": "npm:^6.0.3" "@semantic-release/exec": "npm:^7.1.0" From 25cd4fb32b20bd3fda7f37bc751fc087eb49624e Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:12 +0530 Subject: [PATCH 02/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20remove=20subsidize?= =?UTF-8?q?=20account=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccountManagement.subsidizeAccount is removed in SDK v31. The approve/accept subsidy pair replaces the authorization based flow. BREAKING CHANGE: POST /accounts/subsidy/create is removed. Use POST /accounts/subsidy/approve followed by POST /accounts/subsidy/accept. --- src/subsidy/subsidy.controller.spec.ts | 43 +------------------------ src/subsidy/subsidy.controller.ts | 27 +--------------- src/subsidy/subsidy.service.spec.ts | 44 +------------------------- src/subsidy/subsidy.service.ts | 9 ------ src/test-utils/mocks.ts | 1 - 5 files changed, 3 insertions(+), 121 deletions(-) diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts index b8ecb4d2..093baf19 100644 --- a/src/subsidy/subsidy.controller.spec.ts +++ b/src/subsidy/subsidy.controller.spec.ts @@ -4,8 +4,6 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { AllowanceOperation, TxTags } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; import { ProcessMode, TransactionType } from '~/common/types'; import { AcceptSubsidyDto } from '~/subsidy/dto/accept-subsidy.dto'; import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; @@ -15,7 +13,7 @@ import { RevokeSubsidyDto } from '~/subsidy/dto/revoke-subsidy.dto'; import { SubsidyController } from '~/subsidy/subsidy.controller'; import { SubsidyService } from '~/subsidy/subsidy.service'; import { processedTxResult, txResult } from '~/test-utils/consts'; -import { createMockTransactionResult, MockAuthorizationRequest } from '~/test-utils/mocks'; +import { createMockTransactionResult } from '~/test-utils/mocks'; import { mockSubsidyServiceProvider } from '~/test-utils/service-mocks'; describe('SubsidyController', () => { @@ -62,45 +60,6 @@ describe('SubsidyController', () => { }); }); - describe('subsidizeAccount', () => { - it('should accept CreateSubsidyDto and return the authorization request for adding as paying key', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.relayer.AcceptPayingKey, - }; - const mockAuthorization = new MockAuthorizationRequest(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - result: mockAuthorization, - }); - const mockPayload: CreateSubsidyDto = { - signer: 'Alice', - beneficiary, - allowance, - }; - - when(mockService.subsidizeAccount) - .calledWith(mockPayload) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - const result = await controller.subsidizeAccount(mockPayload); - - expect(result).toEqual( - new CreatedAuthorizationRequestModel({ - ...processedTxResult, - transactions: [transaction], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }) - ); - }); - }); - describe('approveSubsidy', () => { it('should accept CreateSubsidyDto and return transaction details', async () => { const mockPayload: CreateSubsidyDto = { diff --git a/src/subsidy/subsidy.controller.ts b/src/subsidy/subsidy.controller.ts index db65b90f..fc60fba9 100644 --- a/src/subsidy/subsidy.controller.ts +++ b/src/subsidy/subsidy.controller.ts @@ -8,8 +8,6 @@ import { } from '@nestjs/swagger'; import { AllowanceOperation } from '@polymeshassociation/polymesh-sdk/types'; -import { authorizationRequestResolver } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; @@ -63,32 +61,9 @@ export class SubsidyController { }); } - @ApiOperation({ - summary: 'Subsidize an account', - description: - 'This endpoint sends an Authorization Request to an Account to subsidize its transaction fees. Deprecated on chain v8; use approve and accept instead', - deprecated: true, - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: [ - 'The Beneficiary Account already has a pending invitation to add this account as a subsidizer with the same allowance', - ], - }) - @Post('create') - async subsidizeAccount(@Body() params: CreateSubsidyDto): Promise { - const serviceResult = await this.subsidyService.subsidizeAccount(params); - - return handleServiceResult(serviceResult, authorizationRequestResolver); - } - @ApiOperation({ summary: 'Approve a subsidy for a beneficiary account', - description: - 'This endpoint allows a subsidizer to approve an allowance for a beneficiary account on chain v8', + description: 'This endpoint allows a subsidizer to approve an allowance for a beneficiary', }) @ApiTransactionResponse({ description: 'Details about the transaction', diff --git a/src/subsidy/subsidy.service.spec.ts b/src/subsidy/subsidy.service.spec.ts index 9a03100b..c1cd44ee 100644 --- a/src/subsidy/subsidy.service.spec.ts +++ b/src/subsidy/subsidy.service.spec.ts @@ -16,13 +16,7 @@ import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; import { SubsidyService } from '~/subsidy/subsidy.service'; import { testValues } from '~/test-utils/consts'; -import { - createMockSubsidy, - MockAccount, - MockAuthorizationRequest, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; +import { createMockSubsidy, MockAccount, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; import { MockAccountsService, mockTransactionsProvider, @@ -117,42 +111,6 @@ describe('SubsidyService', () => { }); }); - describe('subsidizeAccount', () => { - it('should run a subsidizeAccount procedure and return the queue results', async () => { - const mockAuthRequest = new MockAuthorizationRequest(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.relayer.SetPayingKey, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockAuthRequest, - transactions: [mockTransaction], - }); - - const body = { - signer, - beneficiary, - allowance: new BigNumber(100), - }; - - const result = await service.subsidizeAccount(body); - expect(result).toEqual({ - result: mockAuthRequest, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.subsidizeAccount, - { beneficiary, allowance }, - expect.objectContaining({ signer }) - ); - }); - }); - describe('quit', () => { it('should run a quit procedure and return the queue results', async () => { const mockTransactions = { diff --git a/src/subsidy/subsidy.service.ts b/src/subsidy/subsidy.service.ts index 19569910..a6393b4d 100644 --- a/src/subsidy/subsidy.service.ts +++ b/src/subsidy/subsidy.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { AllowanceOperation, - AuthorizationRequest, Subsidy, SubsidyWithAllowance, } from '@polymeshassociation/polymesh-sdk/types'; @@ -40,14 +39,6 @@ export class SubsidyService { }); } - public async subsidizeAccount(params: CreateSubsidyDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { subsidizeAccount } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(subsidizeAccount, args, options); - } - public async approveSubsidy(params: CreateSubsidyDto): ServiceReturn { const { options, args } = extractTxOptions(params); diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 7e383e01..daa8c8f0 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -108,7 +108,6 @@ export class MockPolymesh { unfreezeSecondaryAccounts: jest.fn(), revokePermissions: jest.fn(), modifyPermissions: jest.fn(), - subsidizeAccount: jest.fn(), approveSubsidy: jest.fn(), acceptSubsidy: jest.fn(), revokeSubsidy: jest.fn(), From cfcda5bdde6a9c1cf8d3404f6f9671fc272be66b Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:13 +0530 Subject: [PATCH 03/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20remove=20instructi?= =?UTF-8?q?on=20affirmation=20withdrawal=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instruction.withdraw and withdrawAsMediator are removed in SDK v31 with no v8 equivalent. BREAKING CHANGE: POST /instructions/:id/withdraw and POST /instructions/:id/withdraw-as-mediator are removed. Reject the instruction instead, via POST /instructions/:id/reject or POST /instructions/:id/reject-as-mediator. --- .../settlements.controller.spec.ts | 22 ------- src/settlements/settlements.controller.ts | 60 ----------------- src/settlements/settlements.service.spec.ts | 64 ------------------- src/settlements/settlements.service.ts | 20 ------ src/test-utils/mocks.ts | 2 - src/test-utils/service-mocks.ts | 2 - 6 files changed, 170 deletions(-) diff --git a/src/settlements/settlements.controller.spec.ts b/src/settlements/settlements.controller.spec.ts index db4d6c45..aed6a6bb 100644 --- a/src/settlements/settlements.controller.spec.ts +++ b/src/settlements/settlements.controller.spec.ts @@ -275,17 +275,6 @@ describe('SettlementsController', () => { }); }); - describe('withdrawAffirmation', () => { - it('should withdraw affirmation from an instruction and return the data returned by the service', async () => { - await testControllerTxResult( - controller.withdrawAffirmation.bind(controller), - mockSettlementsService.withdrawAffirmation, - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - }); - }); - describe('affirmInstructionAsMediator', () => { it('should affirm an instruction and return the data returned by the service', async () => { await testControllerTxResult( @@ -309,17 +298,6 @@ describe('SettlementsController', () => { }); }); - describe('withdrawAffirmationAsMediator', () => { - it('should withdraw affirmation from an instruction and return the data returned by the service', async () => { - await testControllerTxResult( - controller.withdrawAffirmationAsMediator.bind(controller), - mockSettlementsService.withdrawAffirmationAsMediator, - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - }); - }); - describe('getAffirmations', () => { it('should return the list of affirmations generated for a Instruction', async () => { const mockAffirmations = { diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index 5ca73cdc..c0ecafd2 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -114,36 +114,6 @@ export class SettlementsController { return handleServiceResult(result); } - @ApiTags('instructions') - @ApiOperation({ - summary: 'Withdraw affirmation from an existing Instruction', - description: - 'This endpoint will withdraw an affirmation from an Instruction. Deprecated on chain v8 where affirmation withdraw is no longer supported', - deprecated: true, - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction from which to withdraw the affirmation', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The requested Instruction was not found', - }) - @Post('instructions/:id/withdraw') - public async withdrawAffirmation( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.withdrawAffirmation(id, signerDto); - - return handleServiceResult(result); - } - @ApiTags('instructions') @ApiOperation({ summary: 'Affirm an existing Instruction as a mediator', @@ -192,36 +162,6 @@ export class SettlementsController { return handleServiceResult(result); } - @ApiTags('instructions') - @ApiOperation({ - summary: 'Withdraw affirmation from an existing Instruction as a mediator', - description: - 'This endpoint will withdraw a mediator affirmation from an Instruction. Deprecated on chain v8 where affirmation withdraw is no longer supported', - deprecated: true, - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction from which to withdraw the affirmation', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The requested Instruction was not found', - }) - @Post('instructions/:id/withdraw-as-mediator') - public async withdrawAffirmationAsMediator( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.withdrawAffirmationAsMediator(id, signerDto); - - return handleServiceResult(result); - } - @ApiTags('instructions') @ApiOperation({ summary: 'List of affirmations', diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index 13e670db..ff1d8ec1 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -739,38 +739,6 @@ describe('SettlementsService', () => { }); }); - describe('withdrawAffirmation', () => { - it('should run a withdraw affirmation procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.WithdrawAffirmation, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.withdrawAffirmation(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.withdraw, - {}, - expect.objectContaining({ signer }) - ); - }); - }); - describe('affirmInstructionAsMediator', () => { it('should run an affirm procedure and return the queue data', async () => { const expiry = new Date(); @@ -838,36 +806,4 @@ describe('SettlementsService', () => { ); }); }); - - describe('withdrawAffirmationAsMediator', () => { - it('should run a withdraw affirmation procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.WithdrawAffirmationAsMediator, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.withdrawAffirmationAsMediator(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.withdrawAsMediator, - {}, - expect.objectContaining({ signer }) - ); - }); - }); }); diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts index 0fa574b6..c1c942be 100644 --- a/src/settlements/settlements.service.ts +++ b/src/settlements/settlements.service.ts @@ -245,16 +245,6 @@ export class SettlementsService { return this.transactionsService.submit(instruction.executeManually, args, options); } - public async withdrawAffirmation( - id: BigNumber, - signerDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(signerDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.withdraw, {}, options); - } - public async affirmInstructionAsMediator( id: BigNumber, transactionBaseDto: AffirmAsMediatorDto @@ -274,14 +264,4 @@ export class SettlementsService { return this.transactionsService.submit(instruction.rejectAsMediator, {}, options); } - - public async withdrawAffirmationAsMediator( - id: BigNumber, - signerDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(signerDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.withdrawAsMediator, {}, options); - } } diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index daa8c8f0..e458ed3f 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -297,12 +297,10 @@ export class MockInstruction { public getAffirmations = jest.fn(); public getOffChainAffirmations = jest.fn(); public getOffChainAffirmationForLeg = jest.fn(); - public withdraw = jest.fn(); public reschedule = jest.fn(); public getMediators = jest.fn(); public affirmAsMediator = jest.fn(); public rejectAsMediator = jest.fn(); - public withdrawAsMediator = jest.fn(); public generateOffChainAffirmationReceipt = jest.fn(); public toHuman = jest.fn().mockImplementation(() => { return { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 41d9e0d5..a933a2b8 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -195,11 +195,9 @@ export class MockSettlementsService { canTransfer = jest.fn(); findGroupedInstructionsByDid = jest.fn(); findVenuesByOwner = jest.fn(); - withdrawAffirmation = jest.fn(); rescheduleInstruction = jest.fn(); affirmInstructionAsMediator = jest.fn(); rejectInstructionAsMediator = jest.fn(); - withdrawAffirmationAsMediator = jest.fn(); executeInstruction = jest.fn(); fetchAllowedSigners = jest.fn(); updateVenueSigners = jest.fn(); From 7ed4bb8eb980a2ac8cb8340ecb3aad8d15888ade Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:14 +0530 Subject: [PATCH 04/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20remove=20CDD=20cla?= =?UTF-8?q?ims=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claims.getCddClaims is removed in SDK v31. BREAKING CHANGE: GET /identities/:did/cdd-claims is removed. It already threw NotSupported against a v8 chain, so it has not been functional since the v8 migration. --- src/claims/claims.service.spec.ts | 45 -------------------- src/claims/claims.service.ts | 11 ----- src/identities/identities.controller.spec.ts | 34 --------------- src/identities/identities.controller.ts | 42 ------------------ src/test-utils/mocks.ts | 1 - src/test-utils/service-mocks.ts | 1 - 6 files changed, 134 deletions(-) diff --git a/src/claims/claims.service.spec.ts b/src/claims/claims.service.spec.ts index 29084f45..179276d7 100644 --- a/src/claims/claims.service.spec.ts +++ b/src/claims/claims.service.spec.ts @@ -278,51 +278,6 @@ describe('ClaimsService', () => { }); }); - describe('findCddClaimsByDid', () => { - const date = new Date().toISOString(); - const mockCddClaims = [ - { - target: did, - issuer: did, - issuedAt: date, - expiry: date, - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ]; - - it('should return a list of CDD Claims for given DID', async () => { - mockPolymeshApi.claims.getCddClaims.mockResolvedValue(mockCddClaims); - - const result = await claimsService.findCddClaimsByDid(did); - - expect(result).toBe(mockCddClaims); - - expect(mockPolymeshApi.claims.getCddClaims).toHaveBeenCalledWith({ - target: did, - includeExpired: true, - }); - }); - - it('should return a list of CDD Claims for given DID without including expired claims', async () => { - mockPolymeshApi.claims.getCddClaims.mockResolvedValue(mockCddClaims); - - const result = await claimsService.findCddClaimsByDid(did, false); - - expect(result).toBe(mockCddClaims); - - expect(mockPolymeshApi.claims.getCddClaims).toHaveBeenCalledWith({ - target: did, - includeExpired: false, - }); - }); - }); - describe('findClaimScopesByDid', () => { it('should return claim scopes for the target identity', async () => { const mockClaims = [ diff --git a/src/claims/claims.service.ts b/src/claims/claims.service.ts index 6e9ba4c1..0f9ba558 100644 --- a/src/claims/claims.service.ts +++ b/src/claims/claims.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { AddClaimsParams, - CddClaim, ClaimData, ClaimScope, CustomClaimType, @@ -95,16 +94,6 @@ export class ClaimsService { }); } - public async findCddClaimsByDid( - target: string, - includeExpired = true - ): Promise[]> { - return await this.polymeshService.polymeshApi.claims.getCddClaims({ - target, - includeExpired, - }); - } - public async getCustomClaimTypeByName(name: string): Promise { return this.polymeshService.polymeshApi.claims.getCustomClaimTypeByName(name); } diff --git a/src/identities/identities.controller.spec.ts b/src/identities/identities.controller.spec.ts index 29d224b6..9151797d 100644 --- a/src/identities/identities.controller.spec.ts +++ b/src/identities/identities.controller.spec.ts @@ -4,7 +4,6 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { Asset, AuthorizationType, - CddClaim, ClaimData, ClaimScope, ClaimType, @@ -671,39 +670,6 @@ describe('IdentitiesController', () => { }); }); - describe('getCddClaims', () => { - const date = new Date().toISOString(); - const mockCddClaims = [ - { - target: did, - issuer: did, - issuedAt: date, - expiry: date, - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ] as unknown as ClaimData[]; - - it('should call the service and return list of CDD Claims', async () => { - mockClaimsService.findCddClaimsByDid.mockResolvedValue(mockCddClaims); - const result = await controller.getCddClaims({ did }, { includeExpired: false }); - expect(result).toEqual(new ResultsModel({ results: mockCddClaims })); - expect(mockClaimsService.findCddClaimsByDid).toHaveBeenCalledWith(did, false); - }); - - it('should call the service and return list of CDD Claims including expired claims', async () => { - mockClaimsService.findCddClaimsByDid.mockResolvedValue(mockCddClaims); - const result = await controller.getCddClaims({ did }, { includeExpired: true }); - expect(result).toEqual(new ResultsModel({ results: mockCddClaims })); - expect(mockClaimsService.findCddClaimsByDid).toHaveBeenCalledWith(did, true); - }); - }); - describe('selfRegisterDid', () => { it('should return transaction details for self-registration', async () => { const identity = new MockIdentity(); diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index e30709a5..3a771816 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -35,12 +35,10 @@ import { CreatedAuthorizationRequestModel } from '~/authorizations/models/create import { PendingAuthorizationsModel } from '~/authorizations/models/pending-authorizations.model'; import { ClaimsService } from '~/claims/claims.service'; import { ClaimsFilterDto } from '~/claims/dto/claims-filter.dto'; -import { CddClaimModel } from '~/claims/models/cdd-claim.model'; import { ClaimModel } from '~/claims/models/claim.model'; import { ClaimScopeModel } from '~/claims/models/claim-scope.model'; import { ApiArrayResponse, - ApiArrayResponseReplaceModelProperties, ApiTransactionFailedResponse, ApiTransactionResponse, } from '~/common/decorators/'; @@ -549,46 +547,6 @@ export class IdentitiesController { return new ResultsModel({ results }); } - @ApiTags('claims') - @ApiOperation({ - summary: 'Fetch all CDD claims for an Identity', - description: - 'This endpoint will fetch the list of CDD claims for a target DID. Deprecated on chain v8 where CDD claims are no longer supported', - deprecated: true, - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose CDD claims are to be fetched', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired CDD claims or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiArrayResponseReplaceModelProperties( - ClaimModel, - { - description: 'List of CDD claims for the target DID', - paginated: false, - }, - { claim: CddClaimModel } - ) - @Get(':did/cdd-claims') - public async getCddClaims( - @Param() { did }: DidDto, - @Query() { includeExpired }: IncludeExpiredFilterDto - ): Promise>> { - const cddClaims = await this.claimsService.findCddClaimsByDid(did, includeExpired); - - const results = cddClaims.map(claim => new ClaimModel(claim)); - - return { results }; - } - @ApiTags('claims') @ApiOperation({ summary: 'Fetch all claim scopes for an Identity', diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index e458ed3f..4bc7d65c 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -136,7 +136,6 @@ export class MockPolymesh { addClaims: jest.fn(), editClaims: jest.fn(), revokeClaims: jest.fn(), - getCddClaims: jest.fn(), getClaimScopes: jest.fn(), addInvestorUniquenessClaim: jest.fn(), getInvestorUniquenessClaims: jest.fn(), diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index a933a2b8..97a0ee74 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -206,7 +206,6 @@ export class MockSettlementsService { export class MockClaimsService { findIssuedByDid = jest.fn(); findAssociatedByDid = jest.fn(); - findCddClaimsByDid = jest.fn(); } export class MockPortfoliosService { From 43a863a83401c1b61102abed415db2a362870f7c Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:14 +0530 Subject: [PATCH 05/34] =?UTF-8?q?fix:=20=F0=9F=90=9B=20replace=20TransferE?= =?UTF-8?q?rror=20members=20removed=20in=20v31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InvalidSenderCdd and ScopeClaimMissing are removed, as transfers are no longer CDD gated on v8. Use InvalidReceiverIdentity, a new member the chain now reports for an inactive receiving Identity. --- src/settlements/models/transfer-breakdown.model.ts | 2 +- src/settlements/settlements.controller.spec.ts | 2 +- src/settlements/settlements.service.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/settlements/models/transfer-breakdown.model.ts b/src/settlements/models/transfer-breakdown.model.ts index dae3e6ec..123902ae 100644 --- a/src/settlements/models/transfer-breakdown.model.ts +++ b/src/settlements/models/transfer-breakdown.model.ts @@ -14,7 +14,7 @@ export class TransferBreakdownModel { items: { oneOf: [{ type: 'string', enum: Object.values(TransferError) }, { type: 'string' }], }, - example: [TransferError.InvalidSenderPortfolio, TransferError.InvalidSenderCdd], + example: [TransferError.InvalidSenderPortfolio, TransferError.InvalidReceiverIdentity], }) readonly general: (TransferError | string)[]; diff --git a/src/settlements/settlements.controller.spec.ts b/src/settlements/settlements.controller.spec.ts index aed6a6bb..fce50bc8 100644 --- a/src/settlements/settlements.controller.spec.ts +++ b/src/settlements/settlements.controller.spec.ts @@ -431,7 +431,7 @@ describe('SettlementsController', () => { describe('validateLeg', () => { it('should call the service and return the Leg validations', async () => { const mockTransferBreakdown = { - general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], + general: [TransferError.SelfTransfer, TransferError.InvalidReceiverIdentity], compliance: { requirements: [], complies: false, diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index ff1d8ec1..cad1c057 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -663,7 +663,7 @@ describe('SettlementsService', () => { describe('canTransfer', () => { const mockTransferBreakdown = { - general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], + general: [TransferError.SelfTransfer, TransferError.InvalidReceiverIdentity], compliance: { requirements: [], complies: false, From f02191b023a0dce3683be66e7a73778207bfd255 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:15 +0530 Subject: [PATCH 06/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20return=20pending?= =?UTF-8?q?=20Instruction=20from=20transfer=20funds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assets.transferFunds now resolves to Instruction | undefined, since the holders may belong to different Identities. It settles in the same transaction when the receiver affirms automatically, otherwise a pending Instruction is returned for the receiver to affirm. --- src/assets/assets.controller.spec.ts | 29 ++++++++++++++++++- src/assets/assets.controller.ts | 19 ++++++++---- src/assets/assets.service.ts | 3 +- .../models/transfer-funds-result.model.ts | 25 ++++++++++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 src/assets/models/transfer-funds-result.model.ts diff --git a/src/assets/assets.controller.spec.ts b/src/assets/assets.controller.spec.ts index a0148f6f..1784f572 100644 --- a/src/assets/assets.controller.spec.ts +++ b/src/assets/assets.controller.spec.ts @@ -36,7 +36,7 @@ import { ProcessMode } from '~/common/types'; import { MetadataService } from '~/metadata/metadata.service'; import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; import { processedTxResult, testValues } from '~/test-utils/consts'; -import { MockAsset, MockAuthorizationRequest } from '~/test-utils/mocks'; +import { MockAsset, MockAuthorizationRequest, MockInstruction } from '~/test-utils/mocks'; import { MockAssetService, mockMetadataServiceProvider } from '~/test-utils/service-mocks'; const { signer, did, txResult, assetId } = testValues; @@ -437,6 +437,33 @@ describe('AssetsController', () => { expect(result).toEqual(processedTxResult); expect(mockAssetsService.transferFunds).toHaveBeenCalledWith(body); }); + + it('should return the pending Instruction when the transfer does not settle immediately', async () => { + const from = new AssetHolderDto({ + type: AssetHolderType.account, + address: 'fromAddress', + }); + const to = new AssetHolderDto({ + type: AssetHolderType.account, + address: 'toAddress', + }); + const amount = new BigNumber(100); + const body = { signer, from, to, asset: assetId, amount }; + + const mockInstruction = new MockInstruction(); + + mockAssetsService.transferFunds.mockResolvedValue({ + ...txResult, + result: mockInstruction, + }); + + const result = await controller.transferFunds(body); + + expect(result).toEqual({ + ...processedTxResult, + instruction: mockInstruction, + }); + }); }); describe('approveAllowance', () => { diff --git a/src/assets/assets.controller.ts b/src/assets/assets.controller.ts index 91ee9a7f..6bc56419 100644 --- a/src/assets/assets.controller.ts +++ b/src/assets/assets.controller.ts @@ -10,7 +10,7 @@ import { ApiTags, ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; -import { Asset } from '@polymeshassociation/polymesh-sdk/types'; +import { Asset, Instruction } from '@polymeshassociation/polymesh-sdk/types'; import { AssetsService } from '~/assets/assets.service'; import { createAssetDetailsModel } from '~/assets/assets.util'; @@ -35,6 +35,7 @@ import { AssetStatModel } from '~/assets/models/asset-stat.model'; import { CreatedAssetModel } from '~/assets/models/created-asset.model'; import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model'; +import { TransferFundsResultModel } from '~/assets/models/transfer-funds-result.model'; import { TransferRestrictionsModel } from '~/assets/models/transfer-restrictions.model'; import { TransferRestrictionsValueModel } from '~/assets/models/transfer-restrictions-values.model'; import { VenueFilteringDetailsModel } from '~/assets/models/venue-filtering-details.model'; @@ -84,16 +85,24 @@ export class AssetsController { @ApiOperation({ summary: 'Transfer funds between asset holders', description: - 'Transfers fungible tokens or NFTs between asset holders (accounts or portfolios) owned by the same identity', + 'Transfers fungible tokens or NFTs between asset holders (accounts or portfolios). The holders may belong to different Identities, in which case a settlement Instruction is created and auto affirmed on behalf of the sender. It settles in the same transaction if the receiving Identity affirms automatically, otherwise the returned Instruction is left pending the receiver affirmation', }) @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, + description: + 'Details about the transaction, along with the pending Instruction if the transfer did not settle immediately', + type: TransferFundsResultModel, }) @Post('transfer-funds') public async transferFunds(@Body() params: TransferFundsDto): Promise { const result = await this.assetsService.transferFunds(params); - return handleServiceResult(result); + + const resolver: TransactionResolver = ({ + result: instruction, + transactions, + details, + }) => new TransferFundsResultModel({ instruction, transactions, details }); + + return handleServiceResult(result, resolver); } @ApiTags('nfts') diff --git a/src/assets/assets.service.ts b/src/assets/assets.service.ts index 76c7ac37..3a18a542 100644 --- a/src/assets/assets.service.ts +++ b/src/assets/assets.service.ts @@ -10,6 +10,7 @@ import { HistoricAgentOperation, Identity, IdentityBalance, + Instruction, NftCollection, ResultSet, TransferRestrictionParams, @@ -265,7 +266,7 @@ export class AssetsService { ); } - public async transferFunds(params: TransferFundsDto): ServiceReturn { + public async transferFunds(params: TransferFundsDto): ServiceReturn { const { options, args } = extractTxOptions(params); const { from, to, asset, amount, nfts, memo } = args; diff --git a/src/assets/models/transfer-funds-result.model.ts b/src/assets/models/transfer-funds-result.model.ts new file mode 100644 index 00000000..46ddb024 --- /dev/null +++ b/src/assets/models/transfer-funds-result.model.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ + +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Instruction } from '@polymeshassociation/polymesh-sdk/types'; + +import { FromEntityObject } from '~/common/decorators/transformation'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; + +export class TransferFundsResultModel extends TransactionQueueModel { + @ApiPropertyOptional({ + type: 'string', + description: + 'ID of the settlement Instruction awaiting the receiving Identity affirmation. Absent when the transfer settled immediately, which is the case for a transfer within the same Identity or when the receiving Identity has automatic affirmation enabled', + example: '123', + }) + @FromEntityObject() + readonly instruction?: Instruction; + + constructor(model: TransferFundsResultModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} From f2f4f48bd828ab4a10f6f22ae7770b7000c72068 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:16 +0530 Subject: [PATCH 07/34] =?UTF-8?q?fix:=20=F0=9F=90=9B=20grant=20test=20admi?= =?UTF-8?q?ns=20DID=20Registrar=20status=20on=20chain=20v8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context.isV7 is removed in v31, so the v7 branches are dropped. The cddServiceProviders pallet no longer exists either, which would have thrown at runtime, so test admins are now added via didRegistrars. This branch was previously gated on isChainV7, meaning create-test-admins granted nothing at all on a v8 chain. --- .../developer-testing.service.spec.ts | 12 +- .../developer-testing.service.ts | 105 ++++-------------- src/test-utils/mocks.ts | 2 +- 3 files changed, 32 insertions(+), 87 deletions(-) diff --git a/src/developer-testing/developer-testing.service.spec.ts b/src/developer-testing/developer-testing.service.spec.ts index ba42c4b8..ef210d98 100644 --- a/src/developer-testing/developer-testing.service.spec.ts +++ b/src/developer-testing/developer-testing.service.spec.ts @@ -61,7 +61,7 @@ describe('DeveloperTestingService', () => { polymeshService.execTransaction = jest.fn(); mockPolymeshApi.network.getSs58Format.mockReturnValue(new BigNumber(42)); - Object.assign(mockPolymeshApi, { context: { isV7: true } }); + mockAccountsService.getAccountBalance.mockResolvedValue({ free: new BigNumber(0) }); }); afterEach(async () => { @@ -97,6 +97,12 @@ describe('DeveloperTestingService', () => { const identities = await service.createTestAdmins(params); expect(identities).toEqual([{ did: 'fakeId' }, { did: 'fakeSecondaryId' }]); + expect(mockPolymeshApi._polkadotApi.tx.didRegistrars.addMember).toHaveBeenCalledWith( + 'fakeId' + ); + expect(mockPolymeshApi._polkadotApi.tx.didRegistrars.addMember).toHaveBeenCalledWith( + 'fakeSecondaryId' + ); }); }); @@ -149,7 +155,9 @@ describe('DeveloperTestingService', () => { expect(polymeshService.execTransaction).toHaveBeenCalledWith( expect.objectContaining({ address: defaultAdminAddress }), expect.anything(), - expect.anything() + address, + expect.anything(), + null ); }); }); diff --git a/src/developer-testing/developer-testing.service.ts b/src/developer-testing/developer-testing.service.ts index 15e74ab7..f6e889cf 100644 --- a/src/developer-testing/developer-testing.service.ts +++ b/src/developer-testing/developer-testing.service.ts @@ -23,10 +23,6 @@ import { TransactionsService } from '~/transactions/transactions.service'; const unitsPerPolyx = 1000000; -type PolymeshWithContext = PolymeshService['polymeshApi'] & { - context: { isV7: boolean }; -}; - @Injectable() export class DeveloperTestingService { private _sudoPair: KeyringPair; @@ -46,25 +42,30 @@ export class DeveloperTestingService { public async createTestAdmins({ accounts }: CreateTestAdminsDto): Promise { const identities = await this.createTestAccounts({ accounts }); - if (this.isChainV7()) { - await this.createCddProvidersBatch(identities); - } + await this.createDidRegistrarsBatch(identities); return identities; } /** - * @note the `signer` must be a CDD provider and have sufficient POLYX to cover the `initialPolyx` + * @note the `signer` must have sufficient POLYX to cover the `initialPolyx` */ public async createTestAccounts({ accounts, signer, }: CreateTestAccountsDto): Promise { - if (this.isChainV7()) { - return this.createTestAccountsV7({ accounts, signer }); + const accountsWithoutIdentity = await this.findAccountsWithoutIdentity(accounts); + + if (accountsWithoutIdentity.length) { + await this.prefundNewAccounts(accountsWithoutIdentity, signer); } - return this.createTestAccountsV8({ accounts, signer }); + await this.selfRegisterNewAccounts(accounts); + await this.fundAccountsWithInitialPolyx(accounts, signer); + + const madeAccounts = await this.fetchAccountForAccountParams(accounts); + + return this.fetchAccountsIdentities(madeAccounts); } /** @@ -83,7 +84,7 @@ export class DeveloperTestingService { } = this.polymeshService.polymeshApi; if (!signer) { - throw new AppInternalError('A signer is required to prefund accounts on chain v8'); + throw new AppInternalError('A signer is required to prefund accounts'); } const fundingAddress = await this.signingService.getAddressByHandle(signer); @@ -107,66 +108,6 @@ export class DeveloperTestingService { } } - private async createTestAccountsV7({ - accounts, - signer, - }: CreateTestAccountsDto): Promise { - const { - _polkadotApi: { - tx: { utility, balances, identity }, - }, - } = this.polymeshService.polymeshApi; - - const signerAddress = signer - ? await this.signingService.getAddressByHandle(signer) - : this.sudoPair; - - // Create a DID to attach claim too - const createDidCalls = accounts.map(({ address }) => identity.cddRegisterDid(address, [])); - await this.polymeshService.execTransaction(signerAddress, utility.batch, createDidCalls); - - // Fetch the Account and Identity that was made - const madeAccounts = await this.fetchAccountForAccountParams(accounts); - const identities = await this.fetchAccountsIdentities(madeAccounts); - - // Now create a CDD claim for each Identity - const createCddCalls = identities.map(({ did }) => - identity.addClaim(did, { CustomerDueDiligence: did }, null) - ); - - // and provide POLYX for those that are supposed to get some - const initialPolyxCalls = accounts - .filter(({ initialPolyx }) => initialPolyx.gt(0)) - .map(({ address, initialPolyx }) => - balances.transferWithMemo(address, initialPolyx.toNumber() * unitsPerPolyx, null) - ); - - await this.polymeshService.execTransaction(signerAddress, utility.batchAll, [ - ...createCddCalls, - ...initialPolyxCalls, - ]); - - return identities; - } - - private async createTestAccountsV8({ - accounts, - signer, - }: CreateTestAccountsDto): Promise { - const accountsWithoutIdentity = await this.findAccountsWithoutIdentity(accounts); - - if (accountsWithoutIdentity.length) { - await this.prefundNewAccountsForV8(accountsWithoutIdentity, signer); - } - - await this.selfRegisterNewAccountsForV8(accounts); - await this.fundAccountsWithInitialPolyxForV8(accounts, signer); - - const madeAccounts = await this.fetchAccountForAccountParams(accounts); - - return this.fetchAccountsIdentities(madeAccounts); - } - private async findAccountsWithoutIdentity( accounts: CreateTestAccountsDto['accounts'] ): Promise { @@ -184,7 +125,7 @@ export class DeveloperTestingService { return accountsWithoutIdentity; } - private async prefundNewAccountsForV8( + private async prefundNewAccounts( accountsWithoutIdentity: CreateTestAccountsDto['accounts'], signer?: string ): Promise { @@ -224,7 +165,7 @@ export class DeveloperTestingService { } } - private async selfRegisterNewAccountsForV8( + private async selfRegisterNewAccounts( accounts: CreateTestAccountsDto['accounts'] ): Promise { const { selfRegisterDid } = this.polymeshService.polymeshApi.identities; @@ -258,7 +199,7 @@ export class DeveloperTestingService { } } - private async fundAccountsWithInitialPolyxForV8( + private async fundAccountsWithInitialPolyx( accounts: CreateTestAccountsDto['accounts'], signer?: string ): Promise { @@ -298,30 +239,26 @@ export class DeveloperTestingService { } } - private isChainV7(): boolean { - return (this.polymeshService.polymeshApi as PolymeshWithContext).context.isV7; - } - /** * @note relies on having a sudo account configured */ - private async createCddProvidersBatch(identities: Identity[]): Promise { + private async createDidRegistrarsBatch(identities: Identity[]): Promise { const { polymeshService: { polymeshApi: { _polkadotApi: { - tx: { cddServiceProviders, sudo, utility }, + tx: { didRegistrars, sudo, utility }, }, }, }, sudoPair, } = this; - const cddCalls = identities.map(({ did }) => { - return cddServiceProviders.addMember(did); + const registrarCalls = identities.map(({ did }) => { + return didRegistrars.addMember(did); }); - const batchTx = utility.batchAll(cddCalls); + const batchTx = utility.batchAll(registrarCalls); await this.polymeshService.execTransaction(sudoPair, sudo.sudo, batchTx); } diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 4bc7d65c..a03c8642 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -152,7 +152,7 @@ export class MockPolymesh { transferWithMemo: jest.fn(), setBalance: jest.fn(), }, - cddServiceProviders: { + didRegistrars: { addMember: jest.fn(), }, identity: { From 4dd60649112a6c5e582d83f9d53a4af8f610a274 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:17 +0530 Subject: [PATCH 08/34] =?UTF-8?q?test:=20=F0=9F=92=8D=20update=20expected?= =?UTF-8?q?=20transaction=20groups=20for=20v31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TxGroup.RelayerManagement is removed, since every relayer extrinsic is ensure_signed only and the group never gated anything. DidRegistration and InstructionMediation are new. --- src/identities/dto/add-secondary-account-params.dto.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/identities/dto/add-secondary-account-params.dto.spec.ts b/src/identities/dto/add-secondary-account-params.dto.spec.ts index b14064a5..8ac2df69 100644 --- a/src/identities/dto/add-secondary-account-params.dto.spec.ts +++ b/src/identities/dto/add-secondary-account-params.dto.spec.ts @@ -271,7 +271,7 @@ describe('addSecondaryAccountParamsDto', () => { signer, }, [ - 'permissions.each value in transactionGroups must be one of the following values: AdvancedAssetManagement, AssetManagement, AssetDocumentManagement, AssetMetadataManagement, AssetRegistration, AuthorizationManagement, CapitalDistribution, CddRegistration, CheckpointManagement, ClaimsManagement, ComplianceManagement, CorporateActionsManagement, CorporateBallotManagement, CorporateVoting, ExternalAgentManagement, ExternalAgentParticipation, Issuance, MultiSigManagement, PortfolioManagement, RelayerManagement, Redemption, SettlementManagement, StoManagement, TrustedClaimIssuersManagement', + 'permissions.each value in transactionGroups must be one of the following values: AdvancedAssetManagement, AssetManagement, AssetDocumentManagement, AssetMetadataManagement, AssetRegistration, AuthorizationManagement, CapitalDistribution, CddRegistration, DidRegistration, InstructionMediation, CheckpointManagement, ClaimsManagement, ComplianceManagement, CorporateActionsManagement, CorporateBallotManagement, CorporateVoting, ExternalAgentManagement, ExternalAgentParticipation, Issuance, MultiSigManagement, PortfolioManagement, Redemption, SettlementManagement, StoManagement, TrustedClaimIssuersManagement', ], ], [ From 9bfbaafdd92ffe77c30a68303afcda6eee414211 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:56:18 +0530 Subject: [PATCH 09/34] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20update=20sup?= =?UTF-8?q?ported=20chain=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 810c793d..594ad13f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A REST API wrapper for the Polymesh blockchain. -This version is compatible with chain versions 7.3.x - 8.x +This version is compatible with chain versions 8.0.x - 8.1.x. Chain v7 is no longer supported — the SDK throws on initialization if pointed at a v7 node. ## Setup From 46de6a834677c039522b9a5adaf653646317d881 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:47:43 +0530 Subject: [PATCH 10/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20surface=20reserved?= =?UTF-8?q?=20and=20frozen=20fields=20on=20Account=20balance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccountBalance gained reserved and frozen fields in SDK v30.2, and locked now includes held funds (e.g. bonded for staking) rather than only the chain's frozen value. Add a dedicated AccountBalanceModel for GET /accounts/:account/balance so these are documented and serialized, rather than reusing the generic asset BalanceModel which has no staking-specific semantics. --- src/accounts/accounts.controller.spec.ts | 2 ++ src/accounts/accounts.controller.ts | 11 ++++--- src/accounts/models/account-balance.model.ts | 32 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/accounts/models/account-balance.model.ts diff --git a/src/accounts/accounts.controller.spec.ts b/src/accounts/accounts.controller.spec.ts index f4e1abbb..88d9c4f4 100644 --- a/src/accounts/accounts.controller.spec.ts +++ b/src/accounts/accounts.controller.spec.ts @@ -94,6 +94,8 @@ describe('AccountsController', () => { free: new BigNumber(10), locked: new BigNumber(1), total: new BigNumber(11), + reserved: new BigNumber(1), + frozen: new BigNumber(0), }; mockAccountsService.getAccountBalance.mockResolvedValue(mockResult); diff --git a/src/accounts/accounts.controller.ts b/src/accounts/accounts.controller.ts index c1db36b7..9dbd3e78 100644 --- a/src/accounts/accounts.controller.ts +++ b/src/accounts/accounts.controller.ts @@ -28,6 +28,7 @@ import { ModifyPermissionsDto } from '~/accounts/dto/modify-permissions.dto'; import { RevokePermissionsDto } from '~/accounts/dto/revoke-permissions.dto'; import { TransactionHistoryFiltersDto } from '~/accounts/dto/transaction-history-filters.dto'; import { TransferPolyxDto } from '~/accounts/dto/transfer-polyx.dto'; +import { AccountBalanceModel } from '~/accounts/models/account-balance.model'; import { AccountCollectionModel, createAccountCollectionModel, @@ -36,7 +37,6 @@ import { AccountDetailsModel } from '~/accounts/models/account-details.model'; import { MultiSigDetailsModel } from '~/accounts/models/multi-sig-details.model'; import { PermissionsModel } from '~/accounts/models/permissions.model'; import { AssetBalanceModel } from '~/assets/models/asset-balance.model'; -import { BalanceModel } from '~/assets/models/balance.model'; import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { ExtrinsicModel } from '~/common/models/extrinsic.model'; @@ -106,7 +106,8 @@ export class AccountsController { @ApiOperation({ summary: 'Get POLYX balance of an Account', - description: 'This endpoint provides the free, locked and total POLYX balance of an Account', + description: + 'This endpoint provides the free, locked and total POLYX balance of an Account. Note: as of chain v8, `locked` also includes funds on hold (e.g. bonded for staking), which is reflected in the `reserved` and `frozen` fields', }) @ApiParam({ name: 'account', @@ -116,12 +117,12 @@ export class AccountsController { }) @ApiOkResponse({ description: 'Free, locked and total POLYX balance of the Account', - type: BalanceModel, + type: AccountBalanceModel, }) @Get(':account/balance') - async getAccountBalance(@Param() { account }: AccountParamsDto): Promise { + async getAccountBalance(@Param() { account }: AccountParamsDto): Promise { const accountBalance = await this.accountsService.getAccountBalance(account); - return new BalanceModel(accountBalance); + return new AccountBalanceModel(accountBalance); } @ApiOperation({ diff --git a/src/accounts/models/account-balance.model.ts b/src/accounts/models/account-balance.model.ts new file mode 100644 index 00000000..b77c7221 --- /dev/null +++ b/src/accounts/models/account-balance.model.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { BalanceModel } from '~/assets/models/balance.model'; +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class AccountBalanceModel extends BalanceModel { + @ApiProperty({ + type: 'string', + description: + 'POLYX placed on hold by the protocol, e.g. bonded for staking. Not part of `free` and cannot be spent until released', + example: '100', + }) + @FromBigNumber() + readonly reserved: BigNumber; + + @ApiProperty({ + type: 'string', + description: + 'Minimum balance (out of `total`) that must remain in the Account due to freezes/locks (e.g. vesting). May overlap with `reserved`', + example: '0', + }) + @FromBigNumber() + readonly frozen: BigNumber; + + constructor(model: AccountBalanceModel) { + super(model); + Object.assign(this, model); + } +} From 85d26548afff8fc4c6c68542d6c3ab072765ab78 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:47:58 +0530 Subject: [PATCH 11/34] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20remove=20sta?= =?UTF-8?q?le=20CDD=20claim=20wording=20from=20transferPolyx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transfers are no longer CDD gated on chain v8, so the receiver can no longer fail a POLYX transfer for lacking a CDD claim. --- src/accounts/accounts.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/accounts/accounts.controller.ts b/src/accounts/accounts.controller.ts index 9dbd3e78..94002ad8 100644 --- a/src/accounts/accounts.controller.ts +++ b/src/accounts/accounts.controller.ts @@ -137,7 +137,6 @@ export class AccountsController { description: '
    ' + "
  • The destination Account doesn't have an associated Identity
  • " + - '
  • The receiver Identity has an invalid CDD claim
  • ' + '
  • Insufficient free balance
  • ' + '
', }) From 75f0a2c6b999c376af5b08294bb9fa6e70fe2349 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:36:24 +0530 Subject: [PATCH 12/34] =?UTF-8?q?fix:=20=F0=9F=90=9B=20make=20createCdd=20?= =?UTF-8?q?and=20expiry=20optional=20on=20RegisterIdentityDto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As of chain v8, createCdd and expiry no longer have any on-chain effect on identity.cddRegisterDid/cddRegisterDidWithCdd, so requiring createCdd in the request body no longer reflects reality. Both fields are now optional and marked deprecated, and the stale "Expiry cannot be set unless a CDD claim is being created" failure doc is dropped. --- src/identities/dto/register-identity.dto.ts | 11 +++++++---- src/identities/identities.controller.ts | 11 ++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/identities/dto/register-identity.dto.ts b/src/identities/dto/register-identity.dto.ts index 4619ad7b..e9414730 100644 --- a/src/identities/dto/register-identity.dto.ts +++ b/src/identities/dto/register-identity.dto.ts @@ -25,20 +25,23 @@ export class RegisterIdentityDto extends TransactionBaseDto { @Type(() => PermissionedAccountDto) readonly secondaryAccounts?: PermissionedAccountDto[]; - @ApiProperty({ + @ApiPropertyOptional({ description: - 'Issue a CDD claim for the created DID, completing the onboarding process for the Account. Deprecated on chain v8', + 'Issue a CDD claim for the created DID. Deprecated: as of chain v8 this no longer has any on-chain effect', type: 'boolean', example: false, deprecated: true, }) + @IsOptional() @IsBoolean() - readonly createCdd: boolean; + readonly createCdd?: boolean; @ApiPropertyOptional({ - description: 'Date at which the Identity will expire (to be used together with createCdd)', + description: + 'Date at which the Identity will expire. Deprecated: as of chain v8 this no longer has any on-chain effect', example: new Date(new Date().getTime() + +365 * 24 * 60 * 60 * 1000).toISOString(), type: 'string', + deprecated: true, }) @IsOptional() @IsDate() diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index 3a771816..ff00ba4f 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; import { ApiBadRequestResponse, ApiInternalServerErrorResponse, @@ -37,11 +37,7 @@ import { ClaimsService } from '~/claims/claims.service'; import { ClaimsFilterDto } from '~/claims/dto/claims-filter.dto'; import { ClaimModel } from '~/claims/models/claim.model'; import { ClaimScopeModel } from '~/claims/models/claim-scope.model'; -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/'; +import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/'; import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; import { DidDto, IncludeExpiredFilterDto } from '~/common/dto/params.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; @@ -110,9 +106,6 @@ export class IdentitiesController { description: 'Newly created Authorization Request along with transaction details', type: CreatedIdentityModel, }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: ['Expiry cannot be set unless a CDD claim is being created'], - }) async registerIdentity( @Body() registerIdentityDto: RegisterIdentityDto ): Promise { From cebcf1096b58ab0ad43565422f321e303682994e Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:37:40 +0530 Subject: [PATCH 13/34] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20remove=20sta?= =?UTF-8?q?le=20chain=20v7/v8=20wording=20from=20endpoint=20descriptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chain v8 is now the only supported chain, so qualifiers like "on chain v8" and references to v7-only behavior no longer add information. --- src/accounts/accounts.controller.ts | 7 +++---- src/assets/assets.controller.ts | 3 +-- .../dto/set-mandatory-receiver-affirmation.dto.ts | 2 +- src/identities/identities.controller.ts | 7 +++---- src/settlements/dto/offchain-affirmation-receipt.dto.ts | 3 +-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/accounts/accounts.controller.ts b/src/accounts/accounts.controller.ts index 94002ad8..6ccc7d71 100644 --- a/src/accounts/accounts.controller.ts +++ b/src/accounts/accounts.controller.ts @@ -232,8 +232,7 @@ export class AccountsController { @ApiOperation({ summary: 'Get pending subsidies for an Account', - description: - 'Returns pending subsidy requests awaiting acceptance by the beneficiary account on chain v8', + description: 'Returns pending subsidy requests awaiting acceptance by the beneficiary account', }) @ApiParam({ name: 'account', @@ -258,7 +257,7 @@ export class AccountsController { @ApiOperation({ summary: 'Get fungible asset balances held by an Account', - description: 'Returns balances of all fungible assets held in the Account on chain v8', + description: 'Returns balances of all fungible assets held in the Account', }) @ApiParam({ name: 'account', @@ -291,7 +290,7 @@ export class AccountsController { @ApiOperation({ summary: 'Get NFT collections held by an Account', - description: 'Returns NFT collections and holdings for the Account on chain v8', + description: 'Returns NFT collections and holdings for the Account', }) @ApiParam({ name: 'account', diff --git a/src/assets/assets.controller.ts b/src/assets/assets.controller.ts index 6bc56419..a74d6bd0 100644 --- a/src/assets/assets.controller.ts +++ b/src/assets/assets.controller.ts @@ -578,8 +578,7 @@ export class AssetsController { @ApiOperation({ summary: 'Approve fungible asset allowance', - description: - 'Approves a spender account allowance for transferring fungible asset tokens on chain v8', + description: 'Approves a spender account allowance for transferring fungible asset tokens', }) @ApiParam({ name: 'asset', diff --git a/src/identities/dto/set-mandatory-receiver-affirmation.dto.ts b/src/identities/dto/set-mandatory-receiver-affirmation.dto.ts index ecc1ddac..b2968834 100644 --- a/src/identities/dto/set-mandatory-receiver-affirmation.dto.ts +++ b/src/identities/dto/set-mandatory-receiver-affirmation.dto.ts @@ -9,7 +9,7 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; export class SetMandatoryReceiverAffirmationDto extends TransactionBaseDto { @ApiProperty({ description: - 'Whether receiver affirmation is required for incoming settlement transfers. Automatic affirms by default on chain v8', + 'Whether receiver affirmation is required for incoming settlement transfers. Automatic affirms by default', enum: ReceiverAffirmationRequirement, example: ReceiverAffirmationRequirement.Required, }) diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index ff00ba4f..75eb4084 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -81,8 +81,7 @@ export class IdentitiesController { @Post('self-register') @ApiOperation({ summary: 'Self-register Identity', - description: - 'Registers a new DID for the signing Account on chain v8 without a CDD provider. Not supported on chain v7', + description: 'Registers a new DID for the signing Account without requiring a DID Registrar', }) @ApiTransactionResponse({ description: 'Newly created Identity along with transaction details', @@ -100,7 +99,7 @@ export class IdentitiesController { @ApiOperation({ summary: 'Register Identity', description: - 'This endpoint allows registering a new Identity. The transaction signer must be a CDD provider (Did Registrar on chain v8). This will create Authorization Requests which have to be accepted by any secondary accounts if they were specified.', + 'This endpoint allows registering a new Identity. The transaction signer must be a DID Registrar. This will create Authorization Requests which have to be accepted by any secondary accounts if they were specified.', }) @ApiTransactionResponse({ description: 'Newly created Authorization Request along with transaction details', @@ -592,7 +591,7 @@ export class IdentitiesController { @ApiOperation({ summary: 'Set mandatory receiver affirmation', description: - 'Enable or disable mandatory receiver affirmation for incoming settlement transfers on chain v8', + 'Enable or disable mandatory receiver affirmation for incoming settlement transfers', }) @ApiParam({ name: 'did', diff --git a/src/settlements/dto/offchain-affirmation-receipt.dto.ts b/src/settlements/dto/offchain-affirmation-receipt.dto.ts index d572e437..219f70ed 100644 --- a/src/settlements/dto/offchain-affirmation-receipt.dto.ts +++ b/src/settlements/dto/offchain-affirmation-receipt.dto.ts @@ -55,8 +55,7 @@ export class OffChainAffirmationReceiptDto { readonly metadata?: string; @ApiPropertyOptional({ - description: - 'Expiration date for the off-chain affirmation receipt. Required on chain v8 when generating receipts', + description: 'Expiration date for the off-chain affirmation receipt. Required when generating receipts', type: 'string', example: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), }) From a99bb63a8d667f2f8e499f442e07699b80bb8aa6 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:38:00 +0530 Subject: [PATCH 14/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20remove=20unrefere?= =?UTF-8?q?nced=20ScopeClaimProofDto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScopeClaimProof was removed from the SDK in v31, since Investor Uniqueness has been removed from the chain for some time. The DTO was unreferenced anywhere in this codebase. --- src/claims/dto/scope-claim-proof.dto.ts | 30 ------------------------- 1 file changed, 30 deletions(-) delete mode 100644 src/claims/dto/scope-claim-proof.dto.ts diff --git a/src/claims/dto/scope-claim-proof.dto.ts b/src/claims/dto/scope-claim-proof.dto.ts deleted file mode 100644 index 17ec59eb..00000000 --- a/src/claims/dto/scope-claim-proof.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsString, ValidateIf } from 'class-validator'; - -import { ProofScopeIdCddIdMatchDto } from '~/claims/dto/proof-scope-id-cdd-id-match.dto'; -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; - -@ApiExtraModels(ProofScopeIdCddIdMatchDto) -export class ScopeClaimProofDto { - @ApiProperty({ - description: 'The proof scope Id of the claim', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsString() - readonly proofScopeIdWellFormed: string; - - @ApiPropertyOneOf({ - description: 'The proof scope Id of the claim', - union: [ - { - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - ProofScopeIdCddIdMatchDto, - ], - }) - @ValidateIf(({ proofScopeIdCddIdMatch }) => typeof proofScopeIdCddIdMatch !== 'string') - @Type(() => ProofScopeIdCddIdMatchDto) - readonly proofScopeIdCddIdMatch: string | ProofScopeIdCddIdMatchDto; -} From 204289c9e53179a9c1d4ad99e43626471c77d99e Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:40:29 +0530 Subject: [PATCH 15/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20portfolio-le?= =?UTF-8?q?vel=20asset=20pre-approval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Portfolio can now pre-approve receiving a specific Asset, so incoming transfers of it auto-affirm without a manual affirm step. Adds POST /identities/:did/portfolios/:id/pre-approve-asset, POST .../remove-pre-approval, GET .../is-pre-approved and GET .../pre-approved-assets, mirroring the existing Identity-level pre-approval endpoints. --- src/portfolios/dto/pre-approve-asset.dto.ts | 15 ++ src/portfolios/porfolios.controller.spec.ts | 91 +++++++++++- src/portfolios/portfolios.controller.ts | 148 ++++++++++++++++++++ src/portfolios/portfolios.service.spec.ts | 111 +++++++++++++++ src/portfolios/portfolios.service.ts | 47 +++++++ src/test-utils/mocks.ts | 4 + src/test-utils/service-mocks.ts | 4 + 7 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 src/portfolios/dto/pre-approve-asset.dto.ts diff --git a/src/portfolios/dto/pre-approve-asset.dto.ts b/src/portfolios/dto/pre-approve-asset.dto.ts new file mode 100644 index 00000000..6c3efafa --- /dev/null +++ b/src/portfolios/dto/pre-approve-asset.dto.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +import { IsAsset } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class PreApproveAssetDto extends TransactionBaseDto { + @ApiProperty({ + description: 'The Asset (Ticker/Asset ID) to pre-approve', + example: '3616b82e-8e10-80ae-dc95-2ea28b9db8b3', + }) + @IsAsset() + readonly asset: string; +} diff --git a/src/portfolios/porfolios.controller.spec.ts b/src/portfolios/porfolios.controller.spec.ts index ea73dfed..79481aca 100644 --- a/src/portfolios/porfolios.controller.spec.ts +++ b/src/portfolios/porfolios.controller.spec.ts @@ -13,7 +13,12 @@ import { PortfoliosController } from '~/portfolios/portfolios.controller'; import { PortfoliosService } from '~/portfolios/portfolios.service'; import { createPortfolioIdentifierModel, createPortfolioModel } from '~/portfolios/portfolios.util'; import { processedTxResult, testValues } from '~/test-utils/consts'; -import { createMockResultSet, MockHistoricSettlement, MockPortfolio } from '~/test-utils/mocks'; +import { + createMockResultSet, + MockAsset, + MockHistoricSettlement, + MockPortfolio, +} from '~/test-utils/mocks'; import { MockPortfoliosService } from '~/test-utils/service-mocks'; const { did, signer, txResult, assetId } = testValues; @@ -286,4 +291,88 @@ describe('PortfoliosController', () => { }); }); }); + + describe('preApproveAsset', () => { + it('should return the transaction details', async () => { + mockPortfoliosService.preApproveAsset.mockResolvedValue(txResult); + + const portfolioParams = new PortfolioDto({ id: new BigNumber(1), did }); + const body = { signer, asset: assetId }; + + const result = await controller.preApproveAsset(portfolioParams, body); + + expect(result).toEqual(processedTxResult); + expect(mockPortfoliosService.preApproveAsset).toHaveBeenCalledWith(portfolioParams, body); + }); + }); + + describe('removeAssetPreApproval', () => { + it('should return the transaction details', async () => { + mockPortfoliosService.removeAssetPreApproval.mockResolvedValue(txResult); + + const portfolioParams = new PortfolioDto({ id: new BigNumber(1), did }); + const body = { signer, asset: assetId }; + + const result = await controller.removeAssetPreApproval(portfolioParams, body); + + expect(result).toEqual(processedTxResult); + expect(mockPortfoliosService.removeAssetPreApproval).toHaveBeenCalledWith( + portfolioParams, + body + ); + }); + }); + + describe('getIsAssetPreApproved', () => { + it('should return the asset pre-approval status', async () => { + mockPortfoliosService.isAssetPreApproved.mockResolvedValue(true); + + const portfolioParams = new PortfolioDto({ id: new BigNumber(1), did }); + + const result = await controller.getIsAssetPreApproved(portfolioParams, { asset: assetId }); + + expect(result).toEqual({ asset: assetId, did, isPreApproved: true }); + }); + }); + + describe('getPreApprovedAssets', () => { + const paginatedResult = { + data: [new MockAsset()], + next: null, + count: new BigNumber(1), + }; + + it('should return pre-approved assets without start value', async () => { + mockPortfoliosService.getPreApprovedAssets.mockResolvedValue(paginatedResult); + + const portfolioParams = new PortfolioDto({ id: new BigNumber(1), did }); + + const result = await controller.getPreApprovedAssets(portfolioParams, { + size: new BigNumber(10), + }); + + expect(result).toEqual({ + total: paginatedResult.count, + next: paginatedResult.next, + results: [expect.objectContaining({ asset: assetId, did, isPreApproved: true })], + }); + }); + + it('should give pre-approved assets with start value', async () => { + mockPortfoliosService.getPreApprovedAssets.mockResolvedValue(paginatedResult); + + const portfolioParams = new PortfolioDto({ id: new BigNumber(1), did }); + + const result = await controller.getPreApprovedAssets(portfolioParams, { + size: new BigNumber(10), + start: new BigNumber(1), + }); + + expect(result).toEqual({ + total: paginatedResult.count, + next: paginatedResult.next, + results: [expect.objectContaining({ asset: assetId, did, isPreApproved: true })], + }); + }); + }); }); diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index c25be688..d7f0d003 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -18,6 +18,7 @@ import { } from '@nestjs/swagger'; import { NumberedPortfolio } from '@polymeshassociation/polymesh-sdk/types'; +import { AssetParamsDto } from '~/assets/dto/asset-params.dto'; import { ApiArrayResponse, ApiTransactionFailedResponse, @@ -31,12 +32,14 @@ import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; +import { PreApprovedModel } from '~/identities/models/pre-approved.model'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; import { CreatePortfolioDto } from '~/portfolios/dto/create-portfolio.dto'; import { GetTransactionsDto } from '~/portfolios/dto/get-transactions.dto'; import { ModifyPortfolioDto } from '~/portfolios/dto/modify-portfolio.dto'; import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; +import { PreApproveAssetDto } from '~/portfolios/dto/pre-approve-asset.dto'; import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; import { CreatedPortfolioModel } from '~/portfolios/models/created-portfolio.model'; import { HistoricSettlementModel } from '~/portfolios/models/historic-settlement.model'; @@ -421,4 +424,149 @@ export class PortfoliosController { return new EventIdentifierModel(result); } + + @ApiOperation({ + summary: 'Pre-approve receiving an Asset for a Portfolio', + description: + 'This endpoint pre-approves receiving an Asset for the given Portfolio, so incoming transfers of it auto-affirm without a manual affirm step', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Portfolio owner', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Portfolio for which to pre-approve the Asset', + type: 'string', + example: '1', + }) + @ApiTransactionResponse({ + description: 'Information about the transaction', + type: TransactionQueueModel, + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: [ + 'The Portfolio with provided ID was not found', + 'The Identity with provided DID was not found', + ], + [HttpStatus.UNPROCESSABLE_ENTITY]: ['The Portfolio has already pre-approved the Asset'], + }) + @Post('/identities/:did/portfolios/:id/pre-approve-asset') + public async preApproveAsset( + @Param() portfolioParams: PortfolioDto, + @Body() params: PreApproveAssetDto + ): Promise { + const result = await this.portfoliosService.preApproveAsset(portfolioParams, params); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Remove pre-approval for receiving an Asset for a Portfolio', + description: + 'This endpoint disables automatic affirmation when the Portfolio receives the Asset', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Portfolio owner', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Portfolio for which to remove the Asset pre-approval', + type: 'string', + example: '1', + }) + @ApiTransactionResponse({ + description: 'Information about the transaction', + type: TransactionQueueModel, + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: [ + 'The Portfolio with provided ID was not found', + 'The Identity with provided DID was not found', + ], + [HttpStatus.UNPROCESSABLE_ENTITY]: ['The Asset is not pre-approved for the Portfolio'], + }) + @Post('/identities/:did/portfolios/:id/remove-pre-approval') + public async removeAssetPreApproval( + @Param() portfolioParams: PortfolioDto, + @Body() params: PreApproveAssetDto + ): Promise { + const result = await this.portfoliosService.removeAssetPreApproval(portfolioParams, params); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Check if an Asset is pre-approved for a Portfolio', + description: 'This endpoint returns whether or not an Asset is pre-approved for a Portfolio', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Portfolio owner', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Portfolio for which to check the Asset pre-approval', + type: 'string', + example: '1', + }) + @ApiOkResponse({ + description: 'Returns pre-approval status for the Asset', + type: PreApprovedModel, + }) + @Get('/identities/:did/portfolios/:id/is-pre-approved') + public async getIsAssetPreApproved( + @Param() portfolioParams: PortfolioDto, + @Query() { asset }: AssetParamsDto + ): Promise { + const { did } = portfolioParams; + const isPreApproved = await this.portfoliosService.isAssetPreApproved(portfolioParams, asset); + + return new PreApprovedModel({ asset, did, isPreApproved }); + } + + @ApiOperation({ + summary: 'Get Assets pre-approved by a Portfolio', + description: 'This endpoint returns the Assets the given Portfolio has pre-approved', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Portfolio owner', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Portfolio for which to fetch pre-approved Assets', + type: 'string', + example: '1', + }) + @ApiOkResponse({ + description: 'Returns pre-approved Assets for the Portfolio', + type: PaginatedResultsModel, + }) + @Get('/identities/:did/portfolios/:id/pre-approved-assets') + public async getPreApprovedAssets( + @Param() portfolioParams: PortfolioDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { did } = portfolioParams; + const { data, count, next } = await this.portfoliosService.getPreApprovedAssets( + portfolioParams, + { size, start: start?.toString() } + ); + + return new PaginatedResultsModel({ + results: data.map(({ id }) => new PreApprovedModel({ asset: id, did, isPreApproved: true })), + total: count, + next, + }); + } } diff --git a/src/portfolios/portfolios.service.spec.ts b/src/portfolios/portfolios.service.spec.ts index 385b9c67..e1d1cbfd 100644 --- a/src/portfolios/portfolios.service.spec.ts +++ b/src/portfolios/portfolios.service.spec.ts @@ -528,4 +528,115 @@ describe('PortfoliosService', () => { }); }); }); + + describe('preApproveAsset', () => { + it('should return the transaction details', async () => { + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.asset.PreApproveAsset, + }; + const mockTransaction = new MockTransaction(transaction); + const mockPortfolio = new MockPortfolio(); + const mockIdentity = new MockIdentity(); + + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); + mockPortfolio.preApproveAsset.mockResolvedValue(mockTransaction); + + mockTransactionsService.submit.mockResolvedValue({ + result: undefined, + transactions: [mockTransaction], + }); + + const result = await service.preApproveAsset( + new PortfolioDto({ did, id: mockPortfolio.id }), + { signer, asset: assetId } + ); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockPortfolio.preApproveAsset, + { asset: assetId }, + expect.objectContaining({ signer }) + ); + }); + }); + + describe('removeAssetPreApproval', () => { + it('should return the transaction details', async () => { + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.asset.RemoveAssetPreApproval, + }; + const mockTransaction = new MockTransaction(transaction); + const mockPortfolio = new MockPortfolio(); + const mockIdentity = new MockIdentity(); + + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); + mockPortfolio.removeAssetPreApproval.mockResolvedValue(mockTransaction); + + mockTransactionsService.submit.mockResolvedValue({ + result: undefined, + transactions: [mockTransaction], + }); + + const result = await service.removeAssetPreApproval( + new PortfolioDto({ did, id: mockPortfolio.id }), + { signer, asset: assetId } + ); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockPortfolio.removeAssetPreApproval, + { asset: assetId }, + expect.objectContaining({ signer }) + ); + }); + }); + + describe('isAssetPreApproved', () => { + it('should return whether the Asset is pre-approved for the Portfolio', async () => { + const mockPortfolio = new MockPortfolio(); + const mockIdentity = new MockIdentity(); + + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); + mockPortfolio.isAssetPreApproved.mockResolvedValue(true); + + const result = await service.isAssetPreApproved(new PortfolioDto({ did, id: mockPortfolio.id }), assetId); + + expect(result).toBe(true); + expect(mockPortfolio.isAssetPreApproved).toHaveBeenCalledWith(assetId); + }); + }); + + describe('getPreApprovedAssets', () => { + it('should return the pre-approved Assets for the Portfolio', async () => { + const mockPortfolio = new MockPortfolio(); + const mockIdentity = new MockIdentity(); + const mockResultSet = createMockResultSet([{ id: assetId }]); + + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); + mockPortfolio.preApprovedAssets.mockResolvedValue(mockResultSet); + + const result = await service.getPreApprovedAssets( + new PortfolioDto({ did, id: mockPortfolio.id }), + { size: new BigNumber(10) } + ); + + expect(result).toEqual(mockResultSet); + }); + }); }); diff --git a/src/portfolios/portfolios.service.ts b/src/portfolios/portfolios.service.ts index 28c2d6ca..a72187f6 100644 --- a/src/portfolios/portfolios.service.ts +++ b/src/portfolios/portfolios.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { + Asset, AuthorizationRequest, DefaultPortfolio, EventIdentifier, @@ -20,6 +21,7 @@ import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; import { CreatePortfolioDto } from '~/portfolios/dto/create-portfolio.dto'; import { ModifyPortfolioDto } from '~/portfolios/dto/modify-portfolio.dto'; import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; +import { PreApproveAssetDto } from '~/portfolios/dto/pre-approve-asset.dto'; import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; import { toPortfolioId } from '~/portfolios/portfolios.util'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -173,4 +175,49 @@ export class PortfoliosService { const portfolio = await this.findOne(did, portfolioId); return portfolio.createdAt(); } + + public async preApproveAsset( + portfolioParams: PortfolioDto, + params: PreApproveAssetDto + ): ServiceReturn { + const { did, id } = portfolioParams; + const { + options, + args: { asset }, + } = extractTxOptions(params); + const portfolio = await this.findOne(did, id); + + return this.transactionsService.submit(portfolio.preApproveAsset, { asset }, options); + } + + public async removeAssetPreApproval( + portfolioParams: PortfolioDto, + params: PreApproveAssetDto + ): ServiceReturn { + const { did, id } = portfolioParams; + const { + options, + args: { asset }, + } = extractTxOptions(params); + const portfolio = await this.findOne(did, id); + + return this.transactionsService.submit(portfolio.removeAssetPreApproval, { asset }, options); + } + + public async isAssetPreApproved(portfolioParams: PortfolioDto, asset: string): Promise { + const { did, id } = portfolioParams; + const portfolio = await this.findOne(did, id); + + return portfolio.isAssetPreApproved(asset); + } + + public async getPreApprovedAssets( + portfolioParams: PortfolioDto, + paginationOptions: PaginationOptions + ): Promise> { + const { did, id } = portfolioParams; + const portfolio = await this.findOne(did, id); + + return portfolio.preApprovedAssets(paginationOptions); + } } diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index a03c8642..f015b9a6 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -362,6 +362,10 @@ export class MockPortfolio { public moveFunds = jest.fn(); public getTransactionHistory = jest.fn(); public quitCustody = jest.fn(); + public preApproveAsset = jest.fn(); + public removeAssetPreApproval = jest.fn(); + public isAssetPreApproved = jest.fn(); + public preApprovedAssets = jest.fn(); public toHuman = jest.fn().mockImplementation(() => { return { id: '1', diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 97a0ee74..9cc33d29 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -220,6 +220,10 @@ export class MockPortfoliosService { createdAt = jest.fn(); setCustodian = jest.fn(); quitCustody = jest.fn(); + preApproveAsset = jest.fn(); + removeAssetPreApproval = jest.fn(); + isAssetPreApproved = jest.fn(); + getPreApprovedAssets = jest.fn(); } export class MockOfferingsService { From cee678628635099fde7b3cf17c949d0bb3495498 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:44:02 +0530 Subject: [PATCH 16/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20instruction?= =?UTF-8?q?=20lock,=20unlock=20and=20relock=20status=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instruction.lockForExecution existed in the SDK since v30 but was never wired up, and unlockForExecution/getRelockStatus are new in v31. Adds POST /instructions/:id/lock, POST /instructions/:id/unlock and GET /instructions/:id/relock-status, completing the lock/relock cycle a mediator uses to pause and resume execution of a pending Instruction. --- src/settlements/models/relock-status.model.ts | 46 ++++++++++ .../settlements.controller.spec.ts | 39 +++++++++ src/settlements/settlements.controller.ts | 73 ++++++++++++++++ src/settlements/settlements.service.spec.ts | 85 +++++++++++++++++++ src/settlements/settlements.service.ts | 27 ++++++ src/test-utils/mocks.ts | 3 + src/test-utils/service-mocks.ts | 3 + 7 files changed, 276 insertions(+) create mode 100644 src/settlements/models/relock-status.model.ts diff --git a/src/settlements/models/relock-status.model.ts b/src/settlements/models/relock-status.model.ts new file mode 100644 index 00000000..1b3a6848 --- /dev/null +++ b/src/settlements/models/relock-status.model.ts @@ -0,0 +1,46 @@ +/* istanbul ignore file */ + +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class RelockStatusModel { + @ApiPropertyOptional({ + description: + 'Date and time when the Instruction was last unlocked by a mediator, null if it has never been unlocked', + type: 'string', + example: new Date('10/14/1987').toISOString(), + nullable: true, + }) + readonly unlockedAt: Date | null; + + @ApiPropertyOptional({ + description: 'The number of times the Instruction has been relocked', + type: 'string', + example: '0', + }) + @FromBigNumber() + readonly relockCount: BigNumber; + + @ApiPropertyOptional({ + description: 'The maximum number of times the Instruction can be relocked', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly maxRelockCount: BigNumber; + + @ApiPropertyOptional({ + description: + 'Date and time after which the Instruction can be locked again, null if it has never been unlocked', + type: 'string', + example: new Date('10/14/1987').toISOString(), + nullable: true, + }) + readonly cooldownEndsAt: Date | null; + + constructor(model: RelockStatusModel) { + Object.assign(this, model); + } +} diff --git a/src/settlements/settlements.controller.spec.ts b/src/settlements/settlements.controller.spec.ts index fce50bc8..3a095d2f 100644 --- a/src/settlements/settlements.controller.spec.ts +++ b/src/settlements/settlements.controller.spec.ts @@ -23,6 +23,7 @@ import { InstructionAffirmationPartyType, } from '~/settlements/models/instruction-affirmation.model'; import { LegModel } from '~/settlements/models/leg.model'; +import { RelockStatusModel } from '~/settlements/models/relock-status.model'; import { SettlementsController } from '~/settlements/settlements.controller'; import { SettlementsService } from '~/settlements/settlements.service'; import * as settlementsUtil from '~/settlements/settlements.util'; @@ -298,6 +299,44 @@ describe('SettlementsController', () => { }); }); + describe('lockInstructionForExecution', () => { + it('should lock an instruction and return the data returned by the service', async () => { + await testControllerTxResult( + controller.lockInstructionForExecution.bind(controller), + mockSettlementsService.lockInstructionForExecution, + { id: new BigNumber(3) }, + { signer: 'signer' } + ); + }); + }); + + describe('unlockInstructionForExecution', () => { + it('should unlock an instruction and return the data returned by the service', async () => { + await testControllerTxResult( + controller.unlockInstructionForExecution.bind(controller), + mockSettlementsService.unlockInstructionForExecution, + { id: new BigNumber(3) }, + { signer: 'signer' } + ); + }); + }); + + describe('getRelockStatus', () => { + it('should return the relock status of the Instruction', async () => { + const mockRelockStatus = { + unlockedAt: null, + relockCount: new BigNumber(0), + maxRelockCount: new BigNumber(1), + cooldownEndsAt: null, + }; + mockSettlementsService.getRelockStatus.mockResolvedValue(mockRelockStatus); + + const result = await controller.getRelockStatus({ id: new BigNumber(3) }); + + expect(result).toEqual(new RelockStatusModel(mockRelockStatus)); + }); + }); + describe('getAffirmations', () => { it('should return the list of affirmations generated for a Instruction', async () => { const mockAffirmations = { diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index c0ecafd2..4108dec8 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -32,6 +32,7 @@ import { InstructionAffirmationPartyType, } from '~/settlements/models/instruction-affirmation.model'; import { OffChainAffirmationModel } from '~/settlements/models/off-chain-affirmation.model'; +import { RelockStatusModel } from '~/settlements/models/relock-status.model'; import { TransferBreakdownModel } from '~/settlements/models/transfer-breakdown.model'; import { SettlementsService } from '~/settlements/settlements.service'; import { createInstructionModel, legsToLegModel } from '~/settlements/settlements.util'; @@ -162,6 +163,78 @@ export class SettlementsController { return handleServiceResult(result); } + @ApiTags('instructions') + @ApiOperation({ + summary: 'Lock an Instruction for execution', + description: + 'This endpoint locks a pending Instruction, moving it into the LockedForExecution status', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Instruction to be locked', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post('instructions/:id/lock') + public async lockInstructionForExecution( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.settlementsService.lockInstructionForExecution(id, signerDto); + return handleServiceResult(result); + } + + @ApiTags('instructions') + @ApiOperation({ + summary: 'Unlock an Instruction from execution', + description: + 'This endpoint moves a LockedForExecution Instruction back to Pending. Can only be called by a mediator on the Instruction, and is subject to a relock cooldown period', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Instruction to be unlocked', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post('instructions/:id/unlock') + public async unlockInstructionForExecution( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.settlementsService.unlockInstructionForExecution(id, signerDto); + return handleServiceResult(result); + } + + @ApiTags('instructions') + @ApiOperation({ + summary: 'Get the relock status of an Instruction', + description: + "This endpoint returns the mediator's last unlock timestamp, relock count, max relock count and the relock cooldown window for an Instruction", + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Instruction whose relock status is to be fetched', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'The relock status of the Instruction', + type: RelockStatusModel, + }) + @Get('instructions/:id/relock-status') + public async getRelockStatus(@Param() { id }: IdParamsDto): Promise { + const result = await this.settlementsService.getRelockStatus(id); + return new RelockStatusModel(result); + } + @ApiTags('instructions') @ApiOperation({ summary: 'List of affirmations', diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index cad1c057..34e2b21f 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -806,4 +806,89 @@ describe('SettlementsService', () => { ); }); }); + + describe('lockInstructionForExecution', () => { + it('should run a lock procedure and return the queue data', async () => { + const mockInstruction = new MockInstruction(); + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.settlement.LockInstruction, + }; + const mockTransaction = new MockTransaction(transaction); + mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); + + const findInstructionSpy = jest.spyOn(service, 'findInstruction'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findInstructionSpy.mockResolvedValue(mockInstruction as any); + + const result = await service.lockInstructionForExecution(new BigNumber(123), { + signer, + }); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockInstruction.lockForExecution, + {}, + expect.objectContaining({ signer }) + ); + }); + }); + + describe('unlockInstructionForExecution', () => { + it('should run an unlock procedure and return the queue data', async () => { + const mockInstruction = new MockInstruction(); + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.settlement.UnlockInstruction, + }; + const mockTransaction = new MockTransaction(transaction); + mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); + + const findInstructionSpy = jest.spyOn(service, 'findInstruction'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findInstructionSpy.mockResolvedValue(mockInstruction as any); + + const result = await service.unlockInstructionForExecution(new BigNumber(123), { + signer, + }); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockInstruction.unlockForExecution, + {}, + expect.objectContaining({ signer }) + ); + }); + }); + + describe('getRelockStatus', () => { + it('should return the relock status of the Instruction', async () => { + const mockInstruction = new MockInstruction(); + const mockRelockStatus = { + unlockedAt: null, + relockCount: new BigNumber(0), + maxRelockCount: new BigNumber(1), + cooldownEndsAt: null, + }; + mockInstruction.getRelockStatus.mockResolvedValue(mockRelockStatus); + + const findInstructionSpy = jest.spyOn(service, 'findInstruction'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findInstructionSpy.mockResolvedValue(mockInstruction as any); + + const result = await service.getRelockStatus(new BigNumber(123)); + + expect(result).toEqual(mockRelockStatus); + }); + }); }); diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts index c1c942be..b56e3ca6 100644 --- a/src/settlements/settlements.service.ts +++ b/src/settlements/settlements.service.ts @@ -8,6 +8,7 @@ import { GroupedInstructions, Instruction, InstructionAffirmation, + InstructionRelockStatus, OffChainAffirmation, OffChainAffirmationReceipt, ResultSet, @@ -264,4 +265,30 @@ export class SettlementsService { return this.transactionsService.submit(instruction.rejectAsMediator, {}, options); } + + public async lockInstructionForExecution( + id: BigNumber, + transactionBaseDto: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); + const instruction = await this.findInstruction(id); + + return this.transactionsService.submit(instruction.lockForExecution, {}, options); + } + + public async unlockInstructionForExecution( + id: BigNumber, + transactionBaseDto: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); + const instruction = await this.findInstruction(id); + + return this.transactionsService.submit(instruction.unlockForExecution, {}, options); + } + + public async getRelockStatus(id: BigNumber): Promise { + const instruction = await this.findInstruction(id); + + return instruction.getRelockStatus(); + } } diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index f015b9a6..c9a3e909 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -300,6 +300,9 @@ export class MockInstruction { public getMediators = jest.fn(); public affirmAsMediator = jest.fn(); public rejectAsMediator = jest.fn(); + public lockForExecution = jest.fn(); + public unlockForExecution = jest.fn(); + public getRelockStatus = jest.fn(); public generateOffChainAffirmationReceipt = jest.fn(); public toHuman = jest.fn().mockImplementation(() => { return { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 9cc33d29..95cdaacb 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -201,6 +201,9 @@ export class MockSettlementsService { executeInstruction = jest.fn(); fetchAllowedSigners = jest.fn(); updateVenueSigners = jest.fn(); + lockInstructionForExecution = jest.fn(); + unlockInstructionForExecution = jest.fn(); + getRelockStatus = jest.fn(); } export class MockClaimsService { From b4e034b450955b3f2b528546dea490088225821a Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:46:04 +0530 Subject: [PATCH 17/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20Instruction?= =?UTF-8?q?=20leg=20status=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /instructions/:id/legs/:legId/status, returning the execution status of a specific leg (PendingTokenLock, ExecutionPending, or ExecutionToBeSkipped with the skipping signer and receipt UID). --- src/settlements/models/leg-status.model.ts | 38 ++++++++++++++++++ .../settlements.controller.spec.ts | 39 +++++++++++++++++++ src/settlements/settlements.controller.ts | 36 ++++++++++++++++- src/settlements/settlements.service.spec.ts | 17 ++++++++ src/settlements/settlements.service.ts | 7 ++++ src/test-utils/mocks.ts | 1 + src/test-utils/service-mocks.ts | 1 + 7 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/settlements/models/leg-status.model.ts diff --git a/src/settlements/models/leg-status.model.ts b/src/settlements/models/leg-status.model.ts new file mode 100644 index 00000000..48c95249 --- /dev/null +++ b/src/settlements/models/leg-status.model.ts @@ -0,0 +1,38 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { LegStatusType } from '@polymeshassociation/polymesh-sdk/types'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class LegStatusModel { + @ApiProperty({ + description: 'The status of the Instruction leg', + type: 'string', + enum: LegStatusType, + example: LegStatusType.ExecutionPending, + }) + readonly type: LegStatusType; + + @ApiPropertyOptional({ + description: + 'The Account that skipped executing this leg. Present when type is ExecutionToBeSkipped', + type: 'string', + example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', + }) + readonly signer?: string; + + @ApiPropertyOptional({ + description: + 'Unique ID of the off-chain receipt that caused this leg to be skipped. Present when type is ExecutionToBeSkipped', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly uid?: BigNumber; + + constructor(model: LegStatusModel) { + Object.assign(this, model); + } +} diff --git a/src/settlements/settlements.controller.spec.ts b/src/settlements/settlements.controller.spec.ts index 3a095d2f..6a7c3f9d 100644 --- a/src/settlements/settlements.controller.spec.ts +++ b/src/settlements/settlements.controller.spec.ts @@ -7,6 +7,7 @@ import { Identity, InstructionStatus, InstructionType, + LegStatusType, Nft, TransferError, } from '@polymeshassociation/polymesh-sdk/types'; @@ -23,6 +24,7 @@ import { InstructionAffirmationPartyType, } from '~/settlements/models/instruction-affirmation.model'; import { LegModel } from '~/settlements/models/leg.model'; +import { LegStatusModel } from '~/settlements/models/leg-status.model'; import { RelockStatusModel } from '~/settlements/models/relock-status.model'; import { SettlementsController } from '~/settlements/settlements.controller'; import { SettlementsService } from '~/settlements/settlements.service'; @@ -337,6 +339,43 @@ describe('SettlementsController', () => { }); }); + describe('getLegStatus', () => { + it('should return the status of a Leg with ExecutionPending', async () => { + mockSettlementsService.getLegStatus.mockResolvedValue({ + type: LegStatusType.ExecutionPending, + }); + + const result = await controller.getLegStatus({ + id: new BigNumber(3), + legId: new BigNumber(0), + }); + + expect(result).toEqual(new LegStatusModel({ type: LegStatusType.ExecutionPending })); + }); + + it('should return the status of a Leg with ExecutionToBeSkipped, mapping the signer to its address', async () => { + const address = '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV'; + mockSettlementsService.getLegStatus.mockResolvedValue({ + type: LegStatusType.ExecutionToBeSkipped, + signer: { address }, + uid: new BigNumber(1), + }); + + const result = await controller.getLegStatus({ + id: new BigNumber(3), + legId: new BigNumber(0), + }); + + expect(result).toEqual( + new LegStatusModel({ + type: LegStatusType.ExecutionToBeSkipped, + signer: address, + uid: new BigNumber(1), + }) + ); + }); + }); + describe('getAffirmations', () => { it('should return the list of affirmations generated for a Instruction', async () => { const mockAffirmations = { diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index 4108dec8..03678184 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -7,7 +7,7 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { Instruction } from '@polymeshassociation/polymesh-sdk/types'; +import { Instruction, LegStatusType } from '@polymeshassociation/polymesh-sdk/types'; import { ApiArrayResponse } from '~/common/decorators/swagger'; import { AssetHolderDto } from '~/common/dto/asset-holder.dto'; @@ -31,6 +31,7 @@ import { InstructionAffirmationModel, InstructionAffirmationPartyType, } from '~/settlements/models/instruction-affirmation.model'; +import { LegStatusModel } from '~/settlements/models/leg-status.model'; import { OffChainAffirmationModel } from '~/settlements/models/off-chain-affirmation.model'; import { RelockStatusModel } from '~/settlements/models/relock-status.model'; import { TransferBreakdownModel } from '~/settlements/models/transfer-breakdown.model'; @@ -357,6 +358,39 @@ export class SettlementsController { }); } + @ApiTags('instructions') + @ApiOperation({ + summary: 'Get the status of a Leg in an Instruction', + description: 'This endpoint returns the execution status of a specific Leg in an Instruction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Instruction the Leg belongs to', + type: 'string', + example: '123', + }) + @ApiParam({ + name: 'legId', + description: 'The index of the Leg whose status is to be fetched', + type: 'string', + example: '0', + }) + @ApiOkResponse({ + description: 'The status of the Leg', + type: LegStatusModel, + }) + @Get('instructions/:id/legs/:legId/status') + public async getLegStatus(@Param() { id, legId }: LegIdParamsDto): Promise { + const result = await this.settlementsService.getLegStatus(id, legId); + + if (result.type === LegStatusType.ExecutionToBeSkipped) { + const { type, signer, uid } = result; + return new LegStatusModel({ type, signer: signer.address, uid }); + } + + return new LegStatusModel(result); + } + @ApiTags('assets') @ApiOperation({ summary: 'Check if a Leg meets the transfer requirements', diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index 34e2b21f..ede40138 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -891,4 +891,21 @@ describe('SettlementsService', () => { expect(result).toEqual(mockRelockStatus); }); }); + + describe('getLegStatus', () => { + it('should return the status of the Leg', async () => { + const mockInstruction = new MockInstruction(); + const mockLegStatus = { type: 'ExecutionPending' }; + mockInstruction.getLegStatus.mockResolvedValue(mockLegStatus); + + const findInstructionSpy = jest.spyOn(service, 'findInstruction'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findInstructionSpy.mockResolvedValue(mockInstruction as any); + + const result = await service.getLegStatus(new BigNumber(123), new BigNumber(0)); + + expect(result).toEqual(mockLegStatus); + expect(mockInstruction.getLegStatus).toHaveBeenCalledWith({ legId: new BigNumber(0) }); + }); + }); }); diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts index b56e3ca6..a7606667 100644 --- a/src/settlements/settlements.service.ts +++ b/src/settlements/settlements.service.ts @@ -9,6 +9,7 @@ import { Instruction, InstructionAffirmation, InstructionRelockStatus, + LegStatus, OffChainAffirmation, OffChainAffirmationReceipt, ResultSet, @@ -291,4 +292,10 @@ export class SettlementsService { return instruction.getRelockStatus(); } + + public async getLegStatus(id: BigNumber, legId: BigNumber): Promise { + const instruction = await this.findInstruction(id); + + return instruction.getLegStatus({ legId }); + } } diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index c9a3e909..83aafe72 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -303,6 +303,7 @@ export class MockInstruction { public lockForExecution = jest.fn(); public unlockForExecution = jest.fn(); public getRelockStatus = jest.fn(); + public getLegStatus = jest.fn(); public generateOffChainAffirmationReceipt = jest.fn(); public toHuman = jest.fn().mockImplementation(() => { return { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 95cdaacb..d8266e7b 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -204,6 +204,7 @@ export class MockSettlementsService { lockInstructionForExecution = jest.fn(); unlockInstructionForExecution = jest.fn(); getRelockStatus = jest.fn(); + getLegStatus = jest.fn(); } export class MockClaimsService { From 42d8762b6afda89b828207313fef7433fda65f9a Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:47:25 +0530 Subject: [PATCH 18/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20DID=20regist?= =?UTF-8?q?rar=20registration=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds POST /identities/register-did, wrapping the new SDK Identities.registerDid procedure. Unlike the existing register endpoint, this only takes a targetAccount and does not support secondary keys or CDD claims. --- src/identities/dto/register-did.dto.ts | 15 +++++++ src/identities/identities.controller.spec.ts | 47 ++++++++++++++++++++ src/identities/identities.controller.ts | 20 +++++++++ src/identities/identities.service.spec.ts | 32 +++++++++++++ src/identities/identities.service.ts | 16 +++++++ src/test-utils/service-mocks.ts | 1 + 6 files changed, 131 insertions(+) create mode 100644 src/identities/dto/register-did.dto.ts diff --git a/src/identities/dto/register-did.dto.ts b/src/identities/dto/register-did.dto.ts new file mode 100644 index 00000000..a5ed0a15 --- /dev/null +++ b/src/identities/dto/register-did.dto.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class RegisterDidDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Account address for which to create an Identity', + example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx', + }) + @IsString() + readonly targetAccount: string; +} diff --git a/src/identities/identities.controller.spec.ts b/src/identities/identities.controller.spec.ts index 9151797d..57a56fac 100644 --- a/src/identities/identities.controller.spec.ts +++ b/src/identities/identities.controller.spec.ts @@ -781,6 +781,53 @@ describe('IdentitiesController', () => { }); }); + describe('registerDid', () => { + it('should return the transaction details on registering a DID', async () => { + const identity = new MockIdentity(); + const address = 'address'; + identity.getPrimaryAccount.mockResolvedValue({ + account: { address }, + permissions: [], + }); + identity.areSecondaryAccountsFrozen.mockResolvedValue(false); + identity.getSecondaryAccounts.mockResolvedValue({ data: [] }); + + const identityData = new IdentityModel({ + did, + primaryAccount: new PermissionedAccountModel({ + account: new AccountModel({ address }), + permissions: new PermissionsModel({ + assets: null, + portfolios: null, + transactionGroups: [], + transactions: null, + }), + }), + secondaryAccounts: [], + secondaryAccountsFrozen: false, + }); + + const mockData = { + ...txResult, + result: identity, + }; + mockIdentitiesService.registerDidAsRegistrar.mockResolvedValue(mockData); + + const data = { + signer: 'Ox60', + targetAccount: 'address', + }; + + const result = await controller.registerDid(data); + + expect(result).toEqual({ + ...processedTxResult, + identity: identityData, + }); + expect(mockIdentitiesService.registerDidAsRegistrar).toHaveBeenCalledWith(data); + }); + }); + describe('getGroupedInstructions', () => { it("should return the Identity's Instructions", async () => { const expectedInstructionIds = [new BigNumber(1), new BigNumber(2), new BigNumber(3)]; diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index 75eb4084..044d250a 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -48,6 +48,7 @@ import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { createDividendDistributionDetailsModel } from '~/corporate-actions/corporate-actions.util'; import { DividendDistributionDetailsModel } from '~/corporate-actions/models/dividend-distribution-details.model'; import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; +import { RegisterDidDto } from '~/identities/dto/register-did.dto'; import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; import { RotatePrimaryKeyParamsDto } from '~/identities/dto/rotate-primary-key-params.dto'; import { SetMandatoryReceiverAffirmationDto } from '~/identities/dto/set-mandatory-receiver-affirmation.dto'; @@ -114,6 +115,25 @@ export class IdentitiesController { return handleServiceResult(serviceResult, createIdentityResolver); } + @Post('register-did') + @ApiOperation({ + summary: 'Register DID', + description: + "Registers a new DID for the target Account. The transaction signer must be an active DID Registrar. Unlike 'Register Identity', this does not support secondary keys or CDD claims", + }) + @ApiTransactionResponse({ + description: 'Newly created Identity along with transaction details', + type: CreatedIdentityModel, + }) + async registerDid( + @Body() registerDidDto: RegisterDidDto + ): Promise { + this.logger.debug('Registering new DID'); + const serviceResult = await this.identitiesService.registerDidAsRegistrar(registerDidDto); + + return handleServiceResult(serviceResult, createIdentityResolver); + } + @Get(':did') @ApiOperation({ summary: 'Get Identity details', diff --git a/src/identities/identities.service.spec.ts b/src/identities/identities.service.spec.ts index b2699ea3..1fab9853 100644 --- a/src/identities/identities.service.spec.ts +++ b/src/identities/identities.service.spec.ts @@ -374,6 +374,38 @@ describe('IdentitiesService', () => { }); }); + describe('registerDidAsRegistrar', () => { + it('should return the transaction details', async () => { + const body = { + signer, + targetAccount: 'address', + }; + + await testServiceTransactionResult( + service.registerDidAsRegistrar.bind(service), + mockTransactionsService, + body, + TxTags.identity.RegisterDid + ); + }); + + it('should submit only the targetAccount', async () => { + const mockTransaction = setupMockTransaction( + mockTransactionsService, + TxTags.identity.RegisterDid + ); + + const body = { + signer, + targetAccount: 'address', + }; + + const result = await service.registerDidAsRegistrar(body); + expectTransactionResult(result, mockTransaction); + expectLastSubmitCall(mockTransactionsService, { targetAccount: 'address' }); + }); + }); + describe('selfRegisterDid', () => { it('should submit selfRegisterDid and return transaction details', async () => { const transaction = { diff --git a/src/identities/identities.service.ts b/src/identities/identities.service.ts index 7ee2e444..9b1d14c8 100644 --- a/src/identities/identities.service.ts +++ b/src/identities/identities.service.ts @@ -14,6 +14,7 @@ import { import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; +import { RegisterDidDto } from '~/identities/dto/register-did.dto'; import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; import { RotatePrimaryKeyParamsDto } from '~/identities/dto/rotate-primary-key-params.dto'; import { SetMandatoryReceiverAffirmationDto } from '~/identities/dto/set-mandatory-receiver-affirmation.dto'; @@ -124,6 +125,21 @@ export class IdentitiesService { return this.transactionsService.submit(registerIdentity, params, options); } + public async registerDidAsRegistrar(registerDidDto: RegisterDidDto): ServiceReturn { + const { + polymeshService: { polymeshApi }, + } = this; + + const { + options, + args: { targetAccount }, + } = extractTxOptions(registerDidDto); + + const { registerDid } = polymeshApi.identities; + + return this.transactionsService.submit(registerDid, { targetAccount }, options); + } + public async rotatePrimaryKey( rotatePrimaryKeyDto: RotatePrimaryKeyParamsDto ): ServiceReturn { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index d8266e7b..f640a9ec 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -171,6 +171,7 @@ export class MockIdentitiesService { addSecondaryAccount = jest.fn(); createMockCdd = jest.fn(); registerDid = jest.fn(); + registerDidAsRegistrar = jest.fn(); selfRegisterDid = jest.fn(); setMandatoryReceiverAffirmation = jest.fn(); rotatePrimaryKey = jest.fn(); From e5f94e7d0724ae121254e81e901c1a6a6b211853 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:49:18 +0530 Subject: [PATCH 19/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20Venue=20sign?= =?UTF-8?q?er=20count=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /venues/:id/signer-count, wrapping the new SDK Venue.getSignerCount procedure. --- src/settlements/models/signer-count.model.ts | 20 +++++++++++++++++++ src/settlements/settlements.service.spec.ts | 15 ++++++++++++++ src/settlements/settlements.service.ts | 6 ++++++ src/settlements/venues.controller.spec.ts | 11 ++++++++++ src/settlements/venues.controller.ts | 21 ++++++++++++++++++++ src/test-utils/mocks.ts | 1 + src/test-utils/service-mocks.ts | 1 + 7 files changed, 75 insertions(+) create mode 100644 src/settlements/models/signer-count.model.ts diff --git a/src/settlements/models/signer-count.model.ts b/src/settlements/models/signer-count.model.ts new file mode 100644 index 00000000..fb2ac8d8 --- /dev/null +++ b/src/settlements/models/signer-count.model.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class SignerCountModel { + @ApiProperty({ + description: 'The number of signers allowed by the Venue', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly count: BigNumber; + + constructor(model: SignerCountModel) { + Object.assign(this, model); + } +} diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index ede40138..6cd0e265 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -592,6 +592,21 @@ describe('SettlementsService', () => { }); }); + describe('fetchSignerCount', () => { + it('should return the number of signers allowed by a Venue', async () => { + const mockVenue = new MockVenue(); + mockVenue.getSignerCount.mockResolvedValue(new BigNumber(2)); + + const findVenueSpy = jest.spyOn(service, 'findVenue'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findVenueSpy.mockResolvedValue(mockVenue as any); + + const result = await service.fetchSignerCount(new BigNumber(12)); + + expect(result).toEqual(new BigNumber(2)); + }); + }); + describe('findAffirmations', () => { it('should return a list of affirmations for an Instruction', async () => { const mockAffirmations = { diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts index a7606667..d4049a66 100644 --- a/src/settlements/settlements.service.ts +++ b/src/settlements/settlements.service.ts @@ -110,6 +110,12 @@ export class SettlementsService { return venue.getAllowedSigners(); } + public async fetchSignerCount(id: BigNumber): Promise { + const venue = await this.findVenue(id); + + return venue.getSignerCount(); + } + public async findAffirmations( id: BigNumber, size: BigNumber, diff --git a/src/settlements/venues.controller.spec.ts b/src/settlements/venues.controller.spec.ts index 7f9ca8cb..a4cc0123 100644 --- a/src/settlements/venues.controller.spec.ts +++ b/src/settlements/venues.controller.spec.ts @@ -3,6 +3,7 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { VenueType } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; +import { SignerCountModel } from '~/settlements/models/signer-count.model'; import { SettlementsService } from '~/settlements/settlements.service'; import { VenuesController } from '~/settlements/venues.controller'; import { processedTxResult, testValues } from '~/test-utils/consts'; @@ -122,6 +123,16 @@ describe('VenuesController', () => { }); }); + describe('getSignerCount', () => { + it('should return the number of signers allowed by the Venue', async () => { + mockSettlementsService.fetchSignerCount.mockResolvedValue(new BigNumber(2)); + + const result = await controller.getSignerCount({ id: new BigNumber(3) }); + + expect(result).toEqual(new SignerCountModel({ count: new BigNumber(2) })); + }); + }); + describe('addVenueSigners', () => { it('should add signers to a venue and return the data returned by the service', async () => { mockSettlementsService.updateVenueSigners.mockResolvedValue(txResult); diff --git a/src/settlements/venues.controller.ts b/src/settlements/venues.controller.ts index e9d3a415..c2275561 100644 --- a/src/settlements/venues.controller.ts +++ b/src/settlements/venues.controller.ts @@ -13,6 +13,7 @@ import { ModifyVenueDto } from '~/settlements/dto/modify-venue.dto'; import { UpdateVenueSignersDto } from '~/settlements/dto/update-venue-signers.dto'; import { CreatedInstructionModel } from '~/settlements/models/created-instruction.model'; import { CreatedVenueModel } from '~/settlements/models/created-venue.model'; +import { SignerCountModel } from '~/settlements/models/signer-count.model'; import { VenueDetailsModel } from '~/settlements/models/venue-details.model'; import { SettlementsService } from '~/settlements/settlements.service'; import { legsToLegModel } from '~/settlements/settlements.util'; @@ -66,6 +67,26 @@ export class VenuesController { }); } + @ApiOperation({ + summary: 'Get the number of signers allowed by a Venue', + description: 'This endpoint returns the number of signers allowed by a Venue', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Venue whose signer count is being fetched', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'The number of signers allowed by the Venue', + type: SignerCountModel, + }) + @Get('/:id/signer-count') + public async getSignerCount(@Param() { id }: IdParamsDto): Promise { + const count = await this.settlementsService.fetchSignerCount(id); + return new SignerCountModel({ count }); + } + @ApiOperation({ summary: 'Create a Venue', description: 'This endpoint creates a new Venue', diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 83aafe72..e92379f8 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -321,6 +321,7 @@ export class MockVenue { public getAllowedSigners = jest.fn(); public addSigners = jest.fn(); public removeSigners = jest.fn(); + public getSignerCount = jest.fn(); } export class MockIdentityAuthorization { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index f640a9ec..c957cb2f 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -201,6 +201,7 @@ export class MockSettlementsService { rejectInstructionAsMediator = jest.fn(); executeInstruction = jest.fn(); fetchAllowedSigners = jest.fn(); + fetchSignerCount = jest.fn(); updateVenueSigners = jest.fn(); lockInstructionForExecution = jest.fn(); unlockInstructionForExecution = jest.fn(); From 0cb5015c0ab510232ae5e090798f5681e49a3c15 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:51:14 +0530 Subject: [PATCH 20/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20issued-in-fu?= =?UTF-8?q?nding-round=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /assets/:asset/funding-rounds/:round/issued, wrapping the new SDK BaseAsset.getIssuedInFundingRound procedure. --- src/assets/assets.controller.spec.ts | 16 ++++++++++ src/assets/assets.controller.ts | 32 +++++++++++++++++++ src/assets/assets.service.spec.ts | 15 +++++++++ src/assets/assets.service.ts | 8 +++++ src/assets/dto/funding-round-params.dto.ts | 10 ++++++ .../models/issued-in-funding-round.model.ts | 26 +++++++++++++++ src/test-utils/mocks.ts | 1 + src/test-utils/service-mocks.ts | 1 + 8 files changed, 109 insertions(+) create mode 100644 src/assets/dto/funding-round-params.dto.ts create mode 100644 src/assets/models/issued-in-funding-round.model.ts diff --git a/src/assets/assets.controller.spec.ts b/src/assets/assets.controller.spec.ts index 1784f572..5ef0610d 100644 --- a/src/assets/assets.controller.spec.ts +++ b/src/assets/assets.controller.spec.ts @@ -20,6 +20,7 @@ import { AssetsService } from '~/assets/assets.service'; import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; import { SetStatsDto } from '~/assets/dto/transfer-restrictions/set-stats.dto'; import { SetTransferRestrictionsDto } from '~/assets/dto/transfer-restrictions/set-transfer-restrictions.dto'; +import { IssuedInFundingRoundModel } from '~/assets/models/issued-in-funding-round.model'; import { StatAccreditedClaimModel } from '~/assets/models/stat-claim-accredited.model'; import { StatAffiliateClaimModel } from '~/assets/models/stat-claim-affiliate.model'; import { StatJurisdictionClaimModel } from '~/assets/models/stat-claim-jurisdiction.model'; @@ -546,6 +547,21 @@ describe('AssetsController', () => { }); }); + describe('getIssuedInFundingRound', () => { + it('should call the service and return the amount issued in the funding round', async () => { + mockAssetsService.getIssuedInFundingRound.mockResolvedValue(new BigNumber(1000)); + + const result = await controller.getIssuedInFundingRound({ + asset: assetId, + round: 'Series A', + }); + + expect(result).toEqual( + new IssuedInFundingRoundModel({ fundingRound: 'Series A', issued: new BigNumber(1000) }) + ); + }); + }); + describe('addRequiredMediators', () => { it('should call the service and return the results', async () => { const mediators = ['someDid']; diff --git a/src/assets/assets.controller.ts b/src/assets/assets.controller.ts index a74d6bd0..96034274 100644 --- a/src/assets/assets.controller.ts +++ b/src/assets/assets.controller.ts @@ -18,6 +18,7 @@ import { ApproveAllowanceDto } from '~/assets/dto/approve-allowance.dto'; import { AssetParamsDto } from '~/assets/dto/asset-params.dto'; import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto'; import { CreateAssetDto } from '~/assets/dto/create-asset.dto'; +import { FundingRoundParamsDto } from '~/assets/dto/funding-round-params.dto'; import { GetAllowanceParamsDto } from '~/assets/dto/get-allowance-params.dto'; import { IssueDto } from '~/assets/dto/issue.dto'; import { LinkTickerDto } from '~/assets/dto/link-ticker.dto'; @@ -34,6 +35,7 @@ import { AssetDocumentModel } from '~/assets/models/asset-document.model'; import { AssetStatModel } from '~/assets/models/asset-stat.model'; import { CreatedAssetModel } from '~/assets/models/created-asset.model'; import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; +import { IssuedInFundingRoundModel } from '~/assets/models/issued-in-funding-round.model'; import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model'; import { TransferFundsResultModel } from '~/assets/models/transfer-funds-result.model'; import { TransferRestrictionsModel } from '~/assets/models/transfer-restrictions.model'; @@ -673,6 +675,36 @@ export class AssetsController { return new RequiredMediatorsModel({ mediators }); } + @ApiOperation({ + summary: 'Get the total amount issued in a funding round', + description: + 'This endpoint returns the total amount of the Asset issued in the given funding round', + }) + @ApiParam({ + name: 'asset', + description: 'The Asset (Ticker/Asset ID) whose issued amount is to be fetched', + type: 'string', + example: '3616b82e-8e10-80ae-dc95-2ea28b9db8b3', + }) + @ApiParam({ + name: 'round', + description: 'The name of the funding round', + type: 'string', + example: 'Series A', + }) + @ApiOkResponse({ + description: 'The total amount of the Asset issued in the funding round', + type: IssuedInFundingRoundModel, + }) + @Get(':asset/funding-rounds/:round/issued') + public async getIssuedInFundingRound( + @Param() { asset, round }: FundingRoundParamsDto + ): Promise { + const issued = await this.assetsService.getIssuedInFundingRound(asset, round); + + return new IssuedInFundingRoundModel({ fundingRound: round, issued }); + } + @ApiOperation({ summary: 'Add required mediators', description: diff --git a/src/assets/assets.service.spec.ts b/src/assets/assets.service.spec.ts index dc554088..4a8f0ef5 100644 --- a/src/assets/assets.service.spec.ts +++ b/src/assets/assets.service.spec.ts @@ -808,6 +808,21 @@ describe('AssetsService', () => { }); }); + describe('getIssuedInFundingRound', () => { + it('should return the amount issued in the funding round', async () => { + const mockAsset = new MockAsset(); + mockAsset.getIssuedInFundingRound.mockResolvedValue(new BigNumber(1000)); + + const findOneSpy = jest.spyOn(service, 'findOne'); + findOneSpy.mockResolvedValue(mockAsset as unknown as FungibleAsset); + + const result = await service.getIssuedInFundingRound('TICKER', 'Series A'); + + expect(result).toEqual(new BigNumber(1000)); + expect(mockAsset.getIssuedInFundingRound).toHaveBeenCalledWith('Series A'); + }); + }); + describe('addRequiredMediators', () => { it('should run a addRequiredMediators procedure and return the transaction results', async () => { const mockTransaction = setupMockTransaction( diff --git a/src/assets/assets.service.ts b/src/assets/assets.service.ts index 3a18a542..2a8c5b6f 100644 --- a/src/assets/assets.service.ts +++ b/src/assets/assets.service.ts @@ -324,6 +324,14 @@ export class AssetsService { }); } + public async getIssuedInFundingRound( + assetInput: string, + fundingRound: string + ): Promise { + const asset = await this.findOne(assetInput); + return asset.getIssuedInFundingRound(fundingRound); + } + public async addRequiredMediators( assetInput: string, params: RequiredMediatorsDto diff --git a/src/assets/dto/funding-round-params.dto.ts b/src/assets/dto/funding-round-params.dto.ts new file mode 100644 index 00000000..9506482c --- /dev/null +++ b/src/assets/dto/funding-round-params.dto.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ + +import { IsString } from 'class-validator'; + +import { AssetParamsDto } from '~/assets/dto/asset-params.dto'; + +export class FundingRoundParamsDto extends AssetParamsDto { + @IsString() + readonly round: string; +} diff --git a/src/assets/models/issued-in-funding-round.model.ts b/src/assets/models/issued-in-funding-round.model.ts new file mode 100644 index 00000000..91985382 --- /dev/null +++ b/src/assets/models/issued-in-funding-round.model.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class IssuedInFundingRoundModel { + @ApiProperty({ + description: 'The name of the funding round', + example: 'Series A', + }) + readonly fundingRound: string; + + @ApiProperty({ + description: 'The total amount of the Asset issued in the funding round', + type: 'string', + example: '1000', + }) + @FromBigNumber() + readonly issued: BigNumber; + + constructor(model: IssuedInFundingRoundModel) { + Object.assign(this, model); + } +} diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index e92379f8..803c03fe 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -195,6 +195,7 @@ export class MockAsset { public getRequiredMediators = jest.fn(); public addRequiredMediators = jest.fn(); public removeRequiredMediators = jest.fn(); + public getIssuedInFundingRound = jest.fn(); public linkTicker = jest.fn(); public unlinkTicker = jest.fn(); diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index c957cb2f..38922934 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -49,6 +49,7 @@ export class MockAssetService { getRequiredMediators = jest.fn(); addRequiredMediators = jest.fn(); removeRequiredMediators = jest.fn(); + getIssuedInFundingRound = jest.fn(); preApprove = jest.fn(); removePreApproval = jest.fn(); linkTickerToAsset = jest.fn(); From 63e616722d5c9a66814d2428c8263217902f2c44 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:52:37 +0530 Subject: [PATCH 21/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20next=20check?= =?UTF-8?q?point=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /assets/:asset/checkpoints/schedules/next, wrapping the new SDK Schedules.getNextCheckpoint procedure. Returns the closest upcoming Checkpoint across all of an Asset's active Schedules in a single query, or null if the Asset has no active Schedules. --- .../checkpoints.controller.spec.ts | 24 ++++++++++++ src/checkpoints/checkpoints.controller.ts | 30 +++++++++++++++ src/checkpoints/checkpoints.service.spec.ts | 29 +++++++++++++++ src/checkpoints/checkpoints.service.ts | 7 ++++ .../models/next-checkpoint.model.ts | 37 +++++++++++++++++++ .../models/schedule-next-checkpoint.model.ts | 27 ++++++++++++++ src/test-utils/mocks.ts | 1 + src/test-utils/service-mocks.ts | 1 + 8 files changed, 156 insertions(+) create mode 100644 src/checkpoints/models/next-checkpoint.model.ts create mode 100644 src/checkpoints/models/schedule-next-checkpoint.model.ts diff --git a/src/checkpoints/checkpoints.controller.spec.ts b/src/checkpoints/checkpoints.controller.spec.ts index fd6d90b2..0d1a136f 100644 --- a/src/checkpoints/checkpoints.controller.spec.ts +++ b/src/checkpoints/checkpoints.controller.spec.ts @@ -6,6 +6,7 @@ import { CheckpointsController } from '~/checkpoints/checkpoints.controller'; import { CheckpointsService } from '~/checkpoints/checkpoints.service'; import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details.model'; import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; +import { NextCheckpointModel } from '~/checkpoints/models/next-checkpoint.model'; import { PeriodComplexityModel } from '~/checkpoints/models/period-complexity.model'; import { ScheduleComplexityModel } from '~/checkpoints/models/schedule-complexity.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; @@ -161,6 +162,29 @@ describe('CheckpointsController', () => { }); }); + describe('getNextCheckpoint', () => { + it('should return the next Checkpoint details', async () => { + const mockNextCheckpoint = { + nextAt: new Date('10/14/1987'), + totalPending: new BigNumber(1), + schedules: [{ id: new BigNumber(1), nextAt: new Date('10/14/1987') }], + }; + mockCheckpointsService.getNextCheckpoint.mockResolvedValue(mockNextCheckpoint); + + const result = await controller.getNextCheckpoint({ asset: assetId }); + + expect(result).toEqual(new NextCheckpointModel(mockNextCheckpoint)); + }); + + it('should return null if the Asset has no active Schedules', async () => { + mockCheckpointsService.getNextCheckpoint.mockResolvedValue(null); + + const result = await controller.getNextCheckpoint({ asset: assetId }); + + expect(result).toBeNull(); + }); + }); + describe('getSchedule', () => { it('should call the service and return the Checkpoint Schedule details', async () => { const mockDate = new Date('10/14/1987'); diff --git a/src/checkpoints/checkpoints.controller.ts b/src/checkpoints/checkpoints.controller.ts index aa44f76c..20a39d0e 100644 --- a/src/checkpoints/checkpoints.controller.ts +++ b/src/checkpoints/checkpoints.controller.ts @@ -22,6 +22,7 @@ import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details. import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; import { CreatedCheckpointModel } from '~/checkpoints/models/created-checkpoint.model'; import { CreatedCheckpointScheduleModel } from '~/checkpoints/models/created-checkpoint-schedule.model'; +import { NextCheckpointModel } from '~/checkpoints/models/next-checkpoint.model'; import { PeriodComplexityModel } from '~/checkpoints/models/period-complexity.model'; import { ScheduleComplexityModel } from '~/checkpoints/models/schedule-complexity.model'; import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/'; @@ -176,6 +177,35 @@ export class CheckpointsController { }); } + @ApiOperation({ + summary: "Fetch the closest upcoming Checkpoint across an Asset's active Schedules", + description: + 'This endpoint returns the closest upcoming Checkpoint across all of the given Asset active Schedules', + }) + @ApiParam({ + name: 'asset', + description: 'The Asset (Ticker/Asset ID) whose next Checkpoint is to be fetched', + type: 'string', + example: '3616b82e-8e10-80ae-dc95-2ea28b9db8b3', + }) + @ApiOkResponse({ + description: + 'The next Checkpoint details, or null if the Asset has no active Schedules', + type: NextCheckpointModel, + }) + @Get('schedules/next') + public async getNextCheckpoint( + @Param() { asset }: AssetParamsDto + ): Promise { + const nextCheckpoint = await this.checkpointsService.getNextCheckpoint(asset); + + if (!nextCheckpoint) { + return null; + } + + return new NextCheckpointModel(nextCheckpoint); + } + @ApiOperation({ summary: 'Fetch details of an Asset Checkpoint Schedule', }) diff --git a/src/checkpoints/checkpoints.service.spec.ts b/src/checkpoints/checkpoints.service.spec.ts index b77d39df..51c4c339 100644 --- a/src/checkpoints/checkpoints.service.spec.ts +++ b/src/checkpoints/checkpoints.service.spec.ts @@ -159,6 +159,35 @@ describe('CheckpointsService', () => { }); }); + describe('getNextCheckpoint', () => { + it('should return the next Checkpoint details', async () => { + const mockAsset = new MockAsset(); + const mockNextCheckpoint = { + nextAt: new Date('10/14/1987'), + totalPending: new BigNumber(1), + schedules: [{ id: new BigNumber(1), nextAt: new Date('10/14/1987') }], + }; + mockAsset.checkpoints.schedules.getNextCheckpoint.mockResolvedValue(mockNextCheckpoint); + + mockAssetsService.findFungible.mockResolvedValue(mockAsset); + + const result = await service.getNextCheckpoint(assetId); + + expect(result).toEqual(mockNextCheckpoint); + }); + + it('should return null if the Asset has no active Schedules', async () => { + const mockAsset = new MockAsset(); + mockAsset.checkpoints.schedules.getNextCheckpoint.mockResolvedValue(null); + + mockAssetsService.findFungible.mockResolvedValue(mockAsset); + + const result = await service.getNextCheckpoint(assetId); + + expect(result).toBeNull(); + }); + }); + describe('findScheduleById', () => { let mockAsset: MockAsset; diff --git a/src/checkpoints/checkpoints.service.ts b/src/checkpoints/checkpoints.service.ts index 04c77b20..608b3c82 100644 --- a/src/checkpoints/checkpoints.service.ts +++ b/src/checkpoints/checkpoints.service.ts @@ -5,6 +5,7 @@ import { CheckpointSchedule, CheckpointWithData, IdentityBalance, + NextCheckpoints, ResultSet, ScheduleWithDetails, } from '@polymeshassociation/polymesh-sdk/types'; @@ -60,6 +61,12 @@ export class CheckpointsService { }); } + public async getNextCheckpoint(asset: string): Promise { + const fungibleAsset = await this.assetsService.findFungible(asset); + + return fungibleAsset.checkpoints.schedules.getNextCheckpoint(); + } + public async createByAsset( asset: string, signerDto: TransactionBaseDto diff --git a/src/checkpoints/models/next-checkpoint.model.ts b/src/checkpoints/models/next-checkpoint.model.ts new file mode 100644 index 00000000..e6a975a8 --- /dev/null +++ b/src/checkpoints/models/next-checkpoint.model.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { Type } from 'class-transformer'; + +import { ScheduleNextCheckpointModel } from '~/checkpoints/models/schedule-next-checkpoint.model'; +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class NextCheckpointModel { + @ApiProperty({ + description: 'The closest upcoming Checkpoint creation date across all active Schedules', + type: 'string', + example: new Date('10/14/1987').toISOString(), + }) + readonly nextAt: Date; + + @ApiProperty({ + description: 'The total amount of pending Checkpoints across all active Schedules', + type: 'string', + example: '3', + }) + @FromBigNumber() + readonly totalPending: BigNumber; + + @ApiProperty({ + description: 'The next Checkpoint creation date for each active Schedule', + type: () => ScheduleNextCheckpointModel, + isArray: true, + }) + @Type(() => ScheduleNextCheckpointModel) + readonly schedules: ScheduleNextCheckpointModel[]; + + constructor(model: NextCheckpointModel) { + Object.assign(this, model); + } +} diff --git a/src/checkpoints/models/schedule-next-checkpoint.model.ts b/src/checkpoints/models/schedule-next-checkpoint.model.ts new file mode 100644 index 00000000..bc1a34a5 --- /dev/null +++ b/src/checkpoints/models/schedule-next-checkpoint.model.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class ScheduleNextCheckpointModel { + @ApiProperty({ + description: 'The ID of the Checkpoint Schedule', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly id: BigNumber; + + @ApiProperty({ + description: 'The next Checkpoint creation date for this Schedule', + type: 'string', + example: new Date('10/14/1987').toISOString(), + }) + readonly nextAt: Date; + + constructor(model: ScheduleNextCheckpointModel) { + Object.assign(this, model); + } +} diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 803c03fe..e5779c17 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -243,6 +243,7 @@ export class MockAsset { create: jest.fn(), remove: jest.fn(), maxComplexity: jest.fn(), + getNextCheckpoint: jest.fn(), }, }; diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 38922934..3390d695 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -256,6 +256,7 @@ export class MockCheckpointsService { findAllByAsset = jest.fn(); findSchedulesByAsset = jest.fn(); findScheduleById = jest.fn(); + getNextCheckpoint = jest.fn(); createByAsset = jest.fn(); createScheduleByAsset = jest.fn(); getAssetBalance = jest.fn(); From eb3a6101d37aa498d3e4d9fa93202f4d14e3988c Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:53:36 +0530 Subject: [PATCH 22/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20Corporate=20?= =?UTF-8?q?Action=20documents=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /assets/:asset/corporate-actions/:id/documents, wrapping the new SDK CorporateActionBase.getDocuments procedure. This is the missing read-side counterpart to the existing documents/link endpoint. --- .../models/asset-document-with-id.model.ts | 22 +++++++++++++ .../corporate-actions.controller.spec.ts | 15 +++++++++ .../corporate-actions.controller.ts | 32 +++++++++++++++++++ .../corporate-actions.service.spec.ts | 16 ++++++++++ .../corporate-actions.service.ts | 7 ++++ .../mocks/dividend-distribution.mock.ts | 1 + 6 files changed, 93 insertions(+) create mode 100644 src/assets/models/asset-document-with-id.model.ts diff --git a/src/assets/models/asset-document-with-id.model.ts b/src/assets/models/asset-document-with-id.model.ts new file mode 100644 index 00000000..52cdbdeb --- /dev/null +++ b/src/assets/models/asset-document-with-id.model.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { AssetDocumentModel } from '~/assets/models/asset-document.model'; +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class AssetDocumentWithIdModel extends AssetDocumentModel { + @ApiProperty({ + description: 'The on-chain ID of the document', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly id: BigNumber; + + constructor(model: AssetDocumentWithIdModel) { + super(model); + Object.assign(this, model); + } +} diff --git a/src/corporate-actions/corporate-actions.controller.spec.ts b/src/corporate-actions/corporate-actions.controller.spec.ts index a31cb2b2..4af8cafd 100644 --- a/src/corporate-actions/corporate-actions.controller.spec.ts +++ b/src/corporate-actions/corporate-actions.controller.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; +import { AssetDocumentWithIdModel } from '~/assets/models/asset-document-with-id.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; import { CorporateActionsController } from '~/corporate-actions/corporate-actions.controller'; @@ -36,6 +37,7 @@ describe('CorporateActionsController', () => { payDividends: jest.fn(), claimDividends: jest.fn(), linkDocuments: jest.fn(), + getDocuments: jest.fn(), reclaimRemainingFunds: jest.fn(), modifyCheckpoint: jest.fn(), findUnclaimedDistributionsByAsset: jest.fn(), @@ -213,6 +215,19 @@ describe('CorporateActionsController', () => { }); }); + describe('getDocuments', () => { + it('should return the documents linked to the Corporate Action', async () => { + const mockDocuments = [{ name: 'DOC_NAME', uri: 'DOC_URI', id: new BigNumber(1) }]; + mockCorporateActionsService.getDocuments.mockResolvedValue(mockDocuments); + + const result = await controller.getDocuments({ asset: assetId, id: new BigNumber(1) }); + + expect(result).toEqual({ + results: mockDocuments.map(document => new AssetDocumentWithIdModel(document)), + }); + }); + }); + describe('claimDividends', () => { it('should call the service and return the transaction details', async () => { await testControllerTxResult( diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 23959e9b..83555a50 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -12,6 +12,7 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { DividendDistribution } from '@polymeshassociation/polymesh-sdk/types'; import { AssetParamsDto } from '~/assets/dto/asset-params.dto'; +import { AssetDocumentWithIdModel } from '~/assets/models/asset-document-with-id.model'; import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/'; import { IsAsset } from '~/common/decorators/validation'; import { IdParamsDto } from '~/common/dto/id-params.dto'; @@ -349,6 +350,37 @@ export class CorporateActionsController { return handleServiceResult(result); } + @ApiOperation({ + summary: 'Get documents linked to a Corporate Action', + description: 'This endpoint retrieves the documents linked to a Corporate Action', + }) + @ApiParam({ + name: 'asset', + description: 'The Asset (Ticker/Asset ID) of the Corporate Action', + type: 'string', + example: '3616b82e-8e10-80ae-dc95-2ea28b9db8b3', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Corporate Action', + type: 'string', + example: '123', + }) + @ApiArrayResponse(AssetDocumentWithIdModel, { + description: 'List of documents linked to the Corporate Action', + paginated: false, + }) + @Get(':id/documents') + public async getDocuments( + @Param() { asset, id }: DividendDistributionParamsDto + ): Promise> { + const documents = await this.corporateActionsService.getDocuments(asset, id); + + const results = documents.map(document => new AssetDocumentWithIdModel(document)); + + return new ResultsModel({ results }); + } + @ApiTags('dividend-distributions') @ApiOperation({ summary: 'Claim dividend payment for a Dividend Distribution', diff --git a/src/corporate-actions/corporate-actions.service.spec.ts b/src/corporate-actions/corporate-actions.service.spec.ts index c50460e0..141ace0f 100644 --- a/src/corporate-actions/corporate-actions.service.spec.ts +++ b/src/corporate-actions/corporate-actions.service.spec.ts @@ -304,6 +304,22 @@ describe('CorporateActionsService', () => { }); }); + describe('getDocuments', () => { + it('should return the documents linked to the Corporate Action', async () => { + const mockDistributionWithDetails = new MockDistributionWithDetails(); + const mockDocuments = [{ name: 'DOC_NAME', uri: 'DOC_URI', id: new BigNumber(1) }]; + mockDistributionWithDetails.distribution.getDocuments.mockResolvedValue(mockDocuments); + + const findDistributionSpy = jest.spyOn(service, 'findDistribution'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findDistributionSpy.mockResolvedValue(mockDistributionWithDetails as any); + + const result = await service.getDocuments(assetId, new BigNumber(1)); + + expect(result).toEqual(mockDocuments); + }); + }); + describe('claimDividends', () => { describe('otherwise', () => { it('should return the transaction details', async () => { diff --git a/src/corporate-actions/corporate-actions.service.ts b/src/corporate-actions/corporate-actions.service.ts index ae9f0e02..c35629a8 100644 --- a/src/corporate-actions/corporate-actions.service.ts +++ b/src/corporate-actions/corporate-actions.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { + AssetDocumentWithId, CorporateActionDefaultConfig, DistributionPayment, DistributionWithDetails, @@ -122,6 +123,12 @@ export class CorporateActionsService { return this.transactionService.submit(distribution.linkDocuments, params, options); } + public async getDocuments(asset: string, id: BigNumber): Promise { + const { distribution } = await this.findDistribution(asset, id); + + return distribution.getDocuments(); + } + public async claimDividends( asset: string, id: BigNumber, diff --git a/src/corporate-actions/mocks/dividend-distribution.mock.ts b/src/corporate-actions/mocks/dividend-distribution.mock.ts index 47d75648..1408c787 100644 --- a/src/corporate-actions/mocks/dividend-distribution.mock.ts +++ b/src/corporate-actions/mocks/dividend-distribution.mock.ts @@ -20,6 +20,7 @@ export class MockDistribution extends MockCorporateActionDefaultConfig { public pay = jest.fn(); public claim = jest.fn(); public linkDocuments = jest.fn(); + public getDocuments = jest.fn(); public reclaimFunds = jest.fn(); public modifyCheckpoint = jest.fn(); } From 49aafd2c8084865f4850044981f06d6710c820bb Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:54:37 +0530 Subject: [PATCH 23/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20missing=20g?= =?UTF-8?q?etDocuments=20to=20MockCorporateActionsService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropped during commit splitting; this mock class isn't currently referenced by any spec (they use an inline mock object instead), so this had no test impact, but it belongs alongside the rest of the class's methods. --- src/test-utils/service-mocks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 3390d695..3f0e9e5b 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -248,6 +248,7 @@ export class MockCorporateActionsService { payDividends = jest.fn(); claimDividends = jest.fn(); linkDocuments = jest.fn(); + getDocuments = jest.fn(); reclaimRemainingFunds = jest.fn(); modifyCheckpoint = jest.fn(); } From 0a735b86847169ab4df184d9737b14967272823b Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:02:01 +0530 Subject: [PATCH 24/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20ticker=20reg?= =?UTF-8?q?istration=20config=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /ticker-reservations/config, wrapping the new SDK Assets.getTickerRegistrationConfig procedure. Returns the chain-wide maxTickerLength and registrationLength rules used to validate ticker registrations. --- src/test-utils/mocks.ts | 1 + src/test-utils/service-mocks.ts | 1 + .../ticker-registration-config.model.ts | 30 +++++++++++++++++++ .../ticker-reservations.controller.spec.ts | 16 ++++++++++ .../ticker-reservations.controller.ts | 16 ++++++++++ .../ticker-reservations.service.spec.ts | 13 ++++++++ .../ticker-reservations.service.ts | 13 +++++++- 7 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/ticker-reservations/models/ticker-registration-config.model.ts diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index e5779c17..03e21790 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -97,6 +97,7 @@ export class MockPolymesh { transferFunds: jest.fn(), getTickerReservation: jest.fn(), getTickerReservations: jest.fn(), + getTickerRegistrationConfig: jest.fn(), getGlobalMetadataKeys: jest.fn(), }; diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 3f0e9e5b..415a51d3 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -107,6 +107,7 @@ export class MockTickerReservationsService { transferOwnership = jest.fn(); extend = jest.fn(); findAllByOwner = jest.fn(); + getTickerRegistrationConfig = jest.fn(); } export class MockAuthorizationsService { diff --git a/src/ticker-reservations/models/ticker-registration-config.model.ts b/src/ticker-reservations/models/ticker-registration-config.model.ts new file mode 100644 index 00000000..2e91ba25 --- /dev/null +++ b/src/ticker-reservations/models/ticker-registration-config.model.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class TickerRegistrationConfigModel { + @ApiProperty({ + description: 'Maximum allowed length for a ticker', + type: 'string', + example: '12', + }) + @FromBigNumber() + readonly maxTickerLength: BigNumber; + + @ApiPropertyOptional({ + description: + 'Amount of time (in milliseconds) a ticker reservation is valid for before it expires, starting from the moment it is reserved. Absent if ticker reservations never expire', + type: 'string', + nullable: true, + example: '5184000000', + }) + @FromBigNumber() + readonly registrationLength: BigNumber | null; + + constructor(model: TickerRegistrationConfigModel) { + Object.assign(this, model); + } +} diff --git a/src/ticker-reservations/ticker-reservations.controller.spec.ts b/src/ticker-reservations/ticker-reservations.controller.spec.ts index f961147b..a5572f9d 100644 --- a/src/ticker-reservations/ticker-reservations.controller.spec.ts +++ b/src/ticker-reservations/ticker-reservations.controller.spec.ts @@ -1,10 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { TickerReservationStatus } from '@polymeshassociation/polymesh-sdk/types'; import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; import { processedTxResult, testValues } from '~/test-utils/consts'; import { MockAuthorizationRequest, MockIdentity, MockTickerReservation } from '~/test-utils/mocks'; import { MockTickerReservationsService } from '~/test-utils/service-mocks'; +import { TickerRegistrationConfigModel } from '~/ticker-reservations/models/ticker-registration-config.model'; import { TickerReservationsController } from '~/ticker-reservations/ticker-reservations.controller'; import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; @@ -41,6 +43,20 @@ describe('TickerReservationsController', () => { }); }); + describe('getTickerRegistrationConfig', () => { + it('should call the service and return the configuration', async () => { + const mockConfig = { + maxTickerLength: new BigNumber(12), + registrationLength: new BigNumber(5184000000), + }; + mockTickerReservationsService.getTickerRegistrationConfig.mockResolvedValue(mockConfig); + + const result = await controller.getTickerRegistrationConfig(); + + expect(result).toEqual(new TickerRegistrationConfigModel(mockConfig)); + }); + }); + describe('getDetails', () => { it('should call the service and return the details', async () => { const mockDetails = { diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts index 5f57abd0..ec82427c 100644 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ b/src/ticker-reservations/ticker-reservations.controller.ts @@ -18,6 +18,7 @@ import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { ReserveTickerDto } from '~/ticker-reservations/dto/reserve-ticker.dto'; import { ExtendedTickerReservationModel } from '~/ticker-reservations/models/extended-ticker-reservation.model'; +import { TickerRegistrationConfigModel } from '~/ticker-reservations/models/ticker-registration-config.model'; import { TickerReservationModel } from '~/ticker-reservations/models/ticker-reservation.model'; import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; import { createTickerReservationModel } from '~/ticker-reservations/ticker-reservations.util'; @@ -47,6 +48,21 @@ export class TickerReservationsController { return handleServiceResult(result); } + @ApiOperation({ + summary: 'Get ticker registration configuration', + description: + 'This endpoint returns the chain-wide rules used to validate ticker registrations', + }) + @ApiOkResponse({ + description: 'Chain-wide ticker registration configuration', + type: TickerRegistrationConfigModel, + }) + @Get('config') + public async getTickerRegistrationConfig(): Promise { + const config = await this.tickerReservationsService.getTickerRegistrationConfig(); + return new TickerRegistrationConfigModel(config); + } + @ApiOperation({ summary: 'Get ticker reservation details', description: 'This endpoint returns details of ticker reservation', diff --git a/src/ticker-reservations/ticker-reservations.service.spec.ts b/src/ticker-reservations/ticker-reservations.service.spec.ts index 833f55ff..b623a79a 100644 --- a/src/ticker-reservations/ticker-reservations.service.spec.ts +++ b/src/ticker-reservations/ticker-reservations.service.spec.ts @@ -182,4 +182,17 @@ describe('TickerReservationsService', () => { expect(result).toEqual([mockTickerReservation]); }); }); + + describe('getTickerRegistrationConfig', () => { + it('should return the chain-wide ticker registration configuration', async () => { + const mockConfig = { + maxTickerLength: new BigNumber(12), + registrationLength: new BigNumber(5184000000), + }; + mockPolymeshApi.assets.getTickerRegistrationConfig.mockResolvedValue(mockConfig); + + const result = await service.getTickerRegistrationConfig(); + expect(result).toEqual(mockConfig); + }); + }); }); diff --git a/src/ticker-reservations/ticker-reservations.service.ts b/src/ticker-reservations/ticker-reservations.service.ts index c1e287cb..bf585f75 100644 --- a/src/ticker-reservations/ticker-reservations.service.ts +++ b/src/ticker-reservations/ticker-reservations.service.ts @@ -1,5 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { AuthorizationRequest, TickerReservation } from '@polymeshassociation/polymesh-sdk/types'; +import { + AuthorizationRequest, + TickerRegistrationConfig, + TickerReservation, +} from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; @@ -57,4 +61,11 @@ export class TickerReservationsService { } = this; return polymeshApi.assets.getTickerReservations({ owner }); } + + public async getTickerRegistrationConfig(): Promise { + const { + polymeshService: { polymeshApi }, + } = this; + return polymeshApi.assets.getTickerRegistrationConfig(); + } } From c4313e427dcf6809e65d12ace6d34044c912894e Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:07:40 +0530 Subject: [PATCH 25/34] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20support=20creating?= =?UTF-8?q?=20SettleAfterLock=20Instructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateInstructionDto only exposed endBlock/endAfterBlock, so there was no way to create a SettleAfterLock Instruction through the REST API, meaning the lock/unlock/relock-status endpoints had no reachable happy path. Adds endAfterLock, matching the SDK's AddInstructionParams. Verified end-to-end against a local chain: create with endAfterLock, affirm, lock, check relock-status, unlock, and confirm the cooldown window populates correctly. --- src/settlements/dto/create-instruction.dto.ts | 19 ++++++- src/settlements/settlements.service.spec.ts | 52 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/settlements/dto/create-instruction.dto.ts b/src/settlements/dto/create-instruction.dto.ts index c768ec88..cab1e0b0 100644 --- a/src/settlements/dto/create-instruction.dto.ts +++ b/src/settlements/dto/create-instruction.dto.ts @@ -3,7 +3,14 @@ import { ApiExtraModels, ApiPropertyOptional } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { Type } from 'class-transformer'; -import { IsByteLength, IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { + IsBoolean, + IsByteLength, + IsDate, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; import { ApiPropertyOneOf } from '~/common/decorators/swagger'; import { ToBigNumber } from '~/common/decorators/transformation'; @@ -78,6 +85,16 @@ export class CreateInstructionDto extends TransactionBaseDto { @ToBigNumber() readonly endAfterBlock?: BigNumber; + @ApiPropertyOptional({ + type: 'boolean', + description: + 'If true, the Instruction will only execute once all parties (including mediators) have affirmed and a mediator has locked it for execution via lockForExecution. Ignored if endBlock or endAfterBlock is also provided. Requires at least one mediator', + example: true, + }) + @IsOptional() + @IsBoolean() + readonly endAfterLock?: boolean; + @ApiPropertyOptional({ description: 'Identifier string to help differentiate instructions. Maximum 32 bytes', example: 'Transfer of GROWTH Asset', diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index 6cd0e265..30ebfd35 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -253,6 +253,58 @@ describe('SettlementsService', () => { expect.objectContaining({ signer }) ); }); + + it('should pass through endAfterLock and mediators', async () => { + const mockVenue = new MockVenue(); + const venueId = new BigNumber(123); + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.settlement.AddAndAffirmWithMediators, + }; + const mockTransaction = new MockTransaction(transaction); + const mockInstruction = 'instruction'; + mockTransactionsService.submit.mockResolvedValue({ + result: mockInstruction, + transactions: [mockTransaction], + }); + + const findVenueSpy = jest.spyOn(service, 'findVenue'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findVenueSpy.mockResolvedValue(mockVenue as any); + + const onChainLeg = { + type: LegType.onChain, + from: new PortfolioDto({ did: 'fromDid', id: new BigNumber(0) }), + to: new PortfolioDto({ did: 'toDid', id: new BigNumber(1) }), + amount: new BigNumber(100), + asset: 'FAKE_TICKER', + }; + + const body = { + signer, + legs: [plainToInstance(LegDto, onChainLeg)], + endAfterLock: true, + mediators: ['mediatorDid'], + }; + + const result = await service.createInstruction(venueId, body as CreateInstructionDto); + + expect(result).toEqual({ + result: mockInstruction, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockPolymeshApi.settlements.addInstruction, + expect.objectContaining({ + endAfterLock: true, + mediators: ['mediatorDid'], + venueId, + }), + expect.objectContaining({ signer }) + ); + }); }); describe('createVenue', () => { From ed6f14bd384c63c4544a1a8d85272d87fa754f6d Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:55:34 +0530 Subject: [PATCH 26/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20bump=20SDK=20to?= =?UTF-8?q?=20v31.0.0=20(stable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 31.0.0-beta.8 was promoted to the stable v31.0.0 release with no code changes beyond the version bump itself (confirmed by diffing the two published packages). No other dependency versions changed. --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0a0e55f6..480767ba 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^3.0.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^4.1.0", "@polymeshassociation/local-signing-manager": "^4.1.1", - "@polymeshassociation/polymesh-sdk": "31.0.0-beta.7", + "@polymeshassociation/polymesh-sdk": "31.0.0", "@polymeshassociation/polymesh-types": "^7.5.0", "@polymeshassociation/signing-manager-types": "^3.7.1", "class-transformer": "0.5.1", diff --git a/yarn.lock b/yarn.lock index d00d0e45..c2dbc78d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2797,9 +2797,9 @@ __metadata: languageName: node linkType: hard -"@polymeshassociation/polymesh-sdk@npm:31.0.0-beta.7": - version: 31.0.0-beta.7 - resolution: "@polymeshassociation/polymesh-sdk@npm:31.0.0-beta.7" +"@polymeshassociation/polymesh-sdk@npm:31.0.0": + version: 31.0.0 + resolution: "@polymeshassociation/polymesh-sdk@npm:31.0.0" dependencies: "@apollo/client": "npm:^3.8.1" "@polkadot/api": "npm:16.5.2" @@ -2817,7 +2817,7 @@ __metadata: semver: "npm:^7.5.4" ts-morph: "npm:^25.0.1" ws: "npm:^8.18.3" - checksum: 10c0/15239455c18e2af83ee4e5b01ece76a36905d705713cfad0d8674fb8ba56163144d274f696538c2be9a1ad8d58f7cd2bfe1b92facd2cfcce3f00ede0a32d2e4b + checksum: 10c0/9c74bb0a6be7e0c528edb4d4d35f8841d3e754e1fdae694749ee461d9b833e200c62c2563baf3c549cb0434588f91079afc3a53c8ea60e6c009b18b265b91815 languageName: node linkType: hard @@ -11409,7 +11409,7 @@ __metadata: "@polymeshassociation/fireblocks-signing-manager": "npm:^3.0.0" "@polymeshassociation/hashicorp-vault-signing-manager": "npm:^4.1.0" "@polymeshassociation/local-signing-manager": "npm:^4.1.1" - "@polymeshassociation/polymesh-sdk": "npm:31.0.0-beta.7" + "@polymeshassociation/polymesh-sdk": "npm:31.0.0" "@polymeshassociation/polymesh-types": "npm:^7.5.0" "@polymeshassociation/signing-manager-types": "npm:^3.7.1" "@semantic-release/changelog": "npm:^6.0.3" From e5079c88d2501acc464b81ae0293eb084c35cacf Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:58:11 +0530 Subject: [PATCH 27/34] =?UTF-8?q?fix:=20=F0=9F=90=9B=20require=20expiresAt?= =?UTF-8?q?=20on=20off-chain=20affirmation=20receipts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Between beta.7 and the stable v31.0.0 release, the SDK made expiresAt required (previously optional) on Instruction.generateOffChainAffirmationReceipt and OffChainAffirmationReceipt, for both the server-generated and the already-signed passthrough case. OffChainAffirmationReceiptDto now mirrors that. BREAKING CHANGE: expiresAt on a receipt in AffirmInstructionDto.receipts is now required; previously it could be omitted. --- src/settlements/dto/offchain-affirmation-receipt.dto.ts | 9 ++++----- src/settlements/settlements.service.spec.ts | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/settlements/dto/offchain-affirmation-receipt.dto.ts b/src/settlements/dto/offchain-affirmation-receipt.dto.ts index 219f70ed..6dcc7c44 100644 --- a/src/settlements/dto/offchain-affirmation-receipt.dto.ts +++ b/src/settlements/dto/offchain-affirmation-receipt.dto.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { Type } from 'class-transformer'; import { IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; @@ -54,14 +54,13 @@ export class OffChainAffirmationReceiptDto { @IsString() readonly metadata?: string; - @ApiPropertyOptional({ - description: 'Expiration date for the off-chain affirmation receipt. Required when generating receipts', + @ApiProperty({ + description: 'Expiration date for the off-chain affirmation receipt', type: 'string', example: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), }) - @IsOptional() @IsDate() - readonly expiresAt?: Date; + readonly expiresAt: Date; constructor(dto: OffChainAffirmationReceiptDto) { Object.assign(this, dto); diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index 30ebfd35..62a3aaaa 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -510,6 +510,7 @@ describe('SettlementsService', () => { type: SignerKeyRingType.Sr25519, value: '0xsomesignature', }, + expiresAt: new Date('2026-01-01T00:00:00.000Z'), }); const receipt2 = new OffChainAffirmationReceiptDto({ @@ -521,6 +522,7 @@ describe('SettlementsService', () => { value: '0xsomesignature2', }, metadata: 'random metadata', + expiresAt: new Date('2026-01-01T00:00:00.000Z'), }); mockInstruction.generateOffChainAffirmationReceipt.mockResolvedValue(receipt2); From 0a5f89a9fbf775ade920cba58205ea950a25ab82 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:07:33 +0530 Subject: [PATCH 28/34] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20dedupe=20swagg?= =?UTF-8?q?er=20decorators=20on=20portfolio=20pre-approval=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SonarCloud flagged a 31-line duplicate block between preApproveAsset and removeAssetPreApproval (PR #369) - their ApiParam/ApiTransactionResponse/ ApiTransactionFailedResponse decorators were identical apart from two short strings. Extracted into ApiPortfolioAssetPreApprovalResponses, parameterized by the id description and the unprocessable entity message, following the applyDecorators pattern already used elsewhere in this codebase (e.g. ApiTransactionResponse itself). No behavior change: same decorators applied with the same arguments, just composed rather than stacked inline. Full suite still 1071/1071 passing with unchanged coverage. --- src/portfolios/decorators/swagger.ts | 46 ++++++++++++++++++++++ src/portfolios/portfolios.controller.ts | 51 ++++--------------------- 2 files changed, 53 insertions(+), 44 deletions(-) create mode 100644 src/portfolios/decorators/swagger.ts diff --git a/src/portfolios/decorators/swagger.ts b/src/portfolios/decorators/swagger.ts new file mode 100644 index 00000000..ee2b8012 --- /dev/null +++ b/src/portfolios/decorators/swagger.ts @@ -0,0 +1,46 @@ +/* istanbul ignore file */ + +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { ApiParam } from '@nestjs/swagger'; + +import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; + +/** + * Shared swagger docs for the Portfolio Asset pre-approval transaction endpoints + * (`preApproveAsset` / `removeAssetPreApproval`), which differ only in their + * `id` param description and unprocessable entity message + */ +export function ApiPortfolioAssetPreApprovalResponses({ + idDescription, + unprocessableEntityMessage, +}: { + idDescription: string; + unprocessableEntityMessage: string; +}): ReturnType { + return applyDecorators( + ApiParam({ + name: 'did', + description: 'The DID of the Portfolio owner', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }), + ApiParam({ + name: 'id', + description: idDescription, + type: 'string', + example: '1', + }), + ApiTransactionResponse({ + description: 'Information about the transaction', + type: TransactionQueueModel, + }), + ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: [ + 'The Portfolio with provided ID was not found', + 'The Identity with provided DID was not found', + ], + [HttpStatus.UNPROCESSABLE_ENTITY]: [unprocessableEntityMessage], + }) + ); +} diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index d7f0d003..742c4cfc 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -34,6 +34,7 @@ import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { PreApprovedModel } from '~/identities/models/pre-approved.model'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { ApiPortfolioAssetPreApprovalResponses } from '~/portfolios/decorators/swagger'; import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; import { CreatePortfolioDto } from '~/portfolios/dto/create-portfolio.dto'; import { GetTransactionsDto } from '~/portfolios/dto/get-transactions.dto'; @@ -430,28 +431,9 @@ export class PortfoliosController { description: 'This endpoint pre-approves receiving an Asset for the given Portfolio, so incoming transfers of it auto-affirm without a manual affirm step', }) - @ApiParam({ - name: 'did', - description: 'The DID of the Portfolio owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Portfolio for which to pre-approve the Asset', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['The Portfolio has already pre-approved the Asset'], + @ApiPortfolioAssetPreApprovalResponses({ + idDescription: 'The ID of the Portfolio for which to pre-approve the Asset', + unprocessableEntityMessage: 'The Portfolio has already pre-approved the Asset', }) @Post('/identities/:did/portfolios/:id/pre-approve-asset') public async preApproveAsset( @@ -468,28 +450,9 @@ export class PortfoliosController { description: 'This endpoint disables automatic affirmation when the Portfolio receives the Asset', }) - @ApiParam({ - name: 'did', - description: 'The DID of the Portfolio owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Portfolio for which to remove the Asset pre-approval', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['The Asset is not pre-approved for the Portfolio'], + @ApiPortfolioAssetPreApprovalResponses({ + idDescription: 'The ID of the Portfolio for which to remove the Asset pre-approval', + unprocessableEntityMessage: 'The Asset is not pre-approved for the Portfolio', }) @Post('/identities/:did/portfolios/:id/remove-pre-approval') public async removeAssetPreApproval( From a6515b0f26bd5ff6f0dbd68fcb57792dd1ff7d88 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:13:01 +0530 Subject: [PATCH 29/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20throw=20NotFoundE?= =?UTF-8?q?xception=20from=20getNextCheckpoint=20instead=20of=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Returning null from a Nest controller produces a zero-length response body, not the JSON literal "null" - any client doing response.json() or JSON.parse() on it throws. --- src/checkpoints/checkpoints.controller.spec.ts | 9 +++++---- src/checkpoints/checkpoints.controller.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/checkpoints/checkpoints.controller.spec.ts b/src/checkpoints/checkpoints.controller.spec.ts index 0d1a136f..28edc901 100644 --- a/src/checkpoints/checkpoints.controller.spec.ts +++ b/src/checkpoints/checkpoints.controller.spec.ts @@ -1,3 +1,4 @@ +import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; @@ -176,12 +177,12 @@ describe('CheckpointsController', () => { expect(result).toEqual(new NextCheckpointModel(mockNextCheckpoint)); }); - it('should return null if the Asset has no active Schedules', async () => { + it('should throw NotFoundException if the Asset has no active Schedules', async () => { mockCheckpointsService.getNextCheckpoint.mockResolvedValue(null); - const result = await controller.getNextCheckpoint({ asset: assetId }); - - expect(result).toBeNull(); + await expect(controller.getNextCheckpoint({ asset: assetId })).rejects.toThrow( + NotFoundException + ); }); }); diff --git a/src/checkpoints/checkpoints.controller.ts b/src/checkpoints/checkpoints.controller.ts index 20a39d0e..d6f2b012 100644 --- a/src/checkpoints/checkpoints.controller.ts +++ b/src/checkpoints/checkpoints.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, NotFoundException, Param, Post, Query } from '@nestjs/common'; import { ApiBadRequestResponse, ApiNotFoundResponse, @@ -189,18 +189,18 @@ export class CheckpointsController { example: '3616b82e-8e10-80ae-dc95-2ea28b9db8b3', }) @ApiOkResponse({ - description: - 'The next Checkpoint details, or null if the Asset has no active Schedules', + description: 'The next Checkpoint details', type: NextCheckpointModel, }) + @ApiNotFoundResponse({ + description: 'The Asset has no active Schedules', + }) @Get('schedules/next') - public async getNextCheckpoint( - @Param() { asset }: AssetParamsDto - ): Promise { + public async getNextCheckpoint(@Param() { asset }: AssetParamsDto): Promise { const nextCheckpoint = await this.checkpointsService.getNextCheckpoint(asset); if (!nextCheckpoint) { - return null; + throw new NotFoundException('The Asset has no active Schedules'); } return new NextCheckpointModel(nextCheckpoint); From 1a8e6f8d85c76fb9768dc246ef2c0037a9306033 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:13:40 +0530 Subject: [PATCH 30/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20preserve=20null?= =?UTF-8?q?=20on=20TickerRegistrationConfigModel.registrationLength?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared @FromBigNumber() decorator does value?.toString(), which turns null into undefined - and undefined-valued properties are dropped by JSON serialization entirely. That silently defeats the `nullable: true` swagger annotation: clients can no longer distinguish "registrations never expire" (null) from "field not supported" (absent). Replaced with a dedicated transform that preserves null, verified through the actual class-transformer serialization pipeline rather than plain object construction, which wouldn't have caught this. --- .../ticker-registration-config.model.spec.ts | 32 +++++++++++++++++++ .../ticker-registration-config.model.ts | 5 +-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/ticker-reservations/models/ticker-registration-config.model.spec.ts diff --git a/src/ticker-reservations/models/ticker-registration-config.model.spec.ts b/src/ticker-reservations/models/ticker-registration-config.model.spec.ts new file mode 100644 index 00000000..78239450 --- /dev/null +++ b/src/ticker-reservations/models/ticker-registration-config.model.spec.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ + +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { instanceToPlain } from 'class-transformer'; + +import { TickerRegistrationConfigModel } from '~/ticker-reservations/models/ticker-registration-config.model'; + +describe('TickerRegistrationConfigModel', () => { + describe('serialization', () => { + it('should serialize registrationLength as null, not drop the field, when reservations never expire', () => { + const model = new TickerRegistrationConfigModel({ + maxTickerLength: new BigNumber(12), + registrationLength: null, + }); + + const plain = instanceToPlain(model); + + expect(plain).toHaveProperty('registrationLength', null); + }); + + it('should serialize registrationLength as a string when set', () => { + const model = new TickerRegistrationConfigModel({ + maxTickerLength: new BigNumber(12), + registrationLength: new BigNumber(5184000000), + }); + + const plain = instanceToPlain(model); + + expect(plain.registrationLength).toBe('5184000000'); + }); + }); +}); diff --git a/src/ticker-reservations/models/ticker-registration-config.model.ts b/src/ticker-reservations/models/ticker-registration-config.model.ts index 2e91ba25..b5838468 100644 --- a/src/ticker-reservations/models/ticker-registration-config.model.ts +++ b/src/ticker-reservations/models/ticker-registration-config.model.ts @@ -2,6 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { Transform } from 'class-transformer'; import { FromBigNumber } from '~/common/decorators/transformation'; @@ -16,12 +17,12 @@ export class TickerRegistrationConfigModel { @ApiPropertyOptional({ description: - 'Amount of time (in milliseconds) a ticker reservation is valid for before it expires, starting from the moment it is reserved. Absent if ticker reservations never expire', + 'Amount of time (in milliseconds) a ticker reservation is valid for before it expires, starting from the moment it is reserved. Null if ticker reservations never expire', type: 'string', nullable: true, example: '5184000000', }) - @FromBigNumber() + @Transform(({ value }: { value: BigNumber | null }) => (value ? value.toString() : null)) readonly registrationLength: BigNumber | null; constructor(model: TickerRegistrationConfigModel) { From 9bbf11bcfbc563da976b3197186aa11f5e9213cd Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:14:28 +0530 Subject: [PATCH 31/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20restore=20missing?= =?UTF-8?q?=20400=20doc=20for=20registerIdentity=20expiry=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/identities/identities.controller.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index 044d250a..5815bc8e 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; import { ApiBadRequestResponse, ApiInternalServerErrorResponse, @@ -37,7 +37,11 @@ import { ClaimsService } from '~/claims/claims.service'; import { ClaimsFilterDto } from '~/claims/dto/claims-filter.dto'; import { ClaimModel } from '~/claims/models/claim.model'; import { ClaimScopeModel } from '~/claims/models/claim-scope.model'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/'; +import { + ApiArrayResponse, + ApiTransactionFailedResponse, + ApiTransactionResponse, +} from '~/common/decorators/'; import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; import { DidDto, IncludeExpiredFilterDto } from '~/common/dto/params.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; @@ -106,6 +110,9 @@ export class IdentitiesController { description: 'Newly created Authorization Request along with transaction details', type: CreatedIdentityModel, }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: ['Expiry cannot be set unless a CDD claim is being created'], + }) async registerIdentity( @Body() registerIdentityDto: RegisterIdentityDto ): Promise { @@ -125,9 +132,7 @@ export class IdentitiesController { description: 'Newly created Identity along with transaction details', type: CreatedIdentityModel, }) - async registerDid( - @Body() registerDidDto: RegisterDidDto - ): Promise { + async registerDid(@Body() registerDidDto: RegisterDidDto): Promise { this.logger.debug('Registering new DID'); const serviceResult = await this.identitiesService.registerDidAsRegistrar(registerDidDto); From 35cda7a6821ab5baf1ab4b94a36d1fcbb4c9f699 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:16:42 +0530 Subject: [PATCH 32/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20remove=20dead=20c?= =?UTF-8?q?ode=20left=20over=20from=20CDD=20claims=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/claims/models/cdd-claim.model.ts | 24 --------- src/common/decorators/swagger.ts | 81 ---------------------------- 2 files changed, 105 deletions(-) delete mode 100644 src/claims/models/cdd-claim.model.ts diff --git a/src/claims/models/cdd-claim.model.ts b/src/claims/models/cdd-claim.model.ts deleted file mode 100644 index 0a7a616a..00000000 --- a/src/claims/models/cdd-claim.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -export class CddClaimModel { - @ApiProperty({ - type: 'string', - description: 'Claim type', - example: 'CustomerDueDiligence', - }) - readonly type: ClaimType.CustomerDueDiligence; - - @ApiProperty({ - type: 'string', - description: 'ID of the Claim', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly id: string; - - constructor(model: CddClaimModel) { - Object.assign(this, model); - } -} diff --git a/src/common/decorators/swagger.ts b/src/common/decorators/swagger.ts index 0c2fd5b6..900dedae 100644 --- a/src/common/decorators/swagger.ts +++ b/src/common/decorators/swagger.ts @@ -11,7 +11,6 @@ import { ApiUnauthorizedResponse, ApiUnprocessableEntityResponse, getSchemaPath, - OmitType, } from '@nestjs/swagger'; import { ReferenceObject, @@ -20,7 +19,6 @@ import { import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; -import { Class } from '~/common/types'; export const ApiArrayResponse = ( model: TModel, @@ -71,85 +69,6 @@ export const ApiArrayResponse = ( ); }; -export const ApiArrayResponseReplaceModelProperties = ( - Model: Type, - { - paginated, - example, - examples, - description, - }: { - paginated: boolean; - example?: unknown; - examples?: unknown[] | Record; - description?: string; - } = { - paginated: true, - }, - extendItems: Record -): ReturnType => { - const extraModels = []; - const items: SchemaObject = {}; - const keys = Object.keys(extendItems) as K[]; - - const obj = new Model() as unknown as Type; - const name = `${obj.constructor.name}-Omit-${keys.join('-')}`; - - const intermediary = { - [name]: class extends OmitType( - Model as unknown as Class, - keys as unknown as readonly never[] - ) {}, - }; - - items.allOf = [{ $ref: getSchemaPath(intermediary[name]) }]; - extraModels.push(intermediary[name]); - - for (const [key, value] of Object.entries(extendItems)) { - if (typeof value === 'function') { - extraModels.push(value); - items.allOf.push({ - type: 'object', - properties: { - [key]: { $ref: getSchemaPath(value) }, - }, - }); - } - - if (typeof value === 'string') { - items.allOf.push({ - type: 'object', - properties: { - [key]: { type: value }, - }, - }); - } - } - - return applyDecorators( - ApiOkResponse({ - description, - schema: { - allOf: [ - { $ref: getSchemaPath(paginated ? PaginatedResultsModel : ResultsModel) }, - { - properties: { - results: { - type: 'array', - items, - example, - examples, - description, - }, - }, - }, - ], - }, - }), - ApiExtraModels(PaginatedResultsModel, ResultsModel, ...extraModels) - ); -}; - type ApiPropertyOneOfOptions = Omit< ApiPropertyOptions, 'oneOf' | 'type' | 'additionalProperties' | 'selfRequired' | 'properties' | 'required' From 659df7ef39498b17b36e94b08e3329036f1f00bb Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:17:45 +0530 Subject: [PATCH 33/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20apply=20prettier?= =?UTF-8?q?=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/portfolios/portfolios.service.spec.ts | 5 ++++- src/ticker-reservations/ticker-reservations.controller.ts | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/portfolios/portfolios.service.spec.ts b/src/portfolios/portfolios.service.spec.ts index e1d1cbfd..9704f804 100644 --- a/src/portfolios/portfolios.service.spec.ts +++ b/src/portfolios/portfolios.service.spec.ts @@ -614,7 +614,10 @@ describe('PortfoliosService', () => { mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); mockPortfolio.isAssetPreApproved.mockResolvedValue(true); - const result = await service.isAssetPreApproved(new PortfolioDto({ did, id: mockPortfolio.id }), assetId); + const result = await service.isAssetPreApproved( + new PortfolioDto({ did, id: mockPortfolio.id }), + assetId + ); expect(result).toBe(true); expect(mockPortfolio.isAssetPreApproved).toHaveBeenCalledWith(assetId); diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts index ec82427c..b62160d4 100644 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ b/src/ticker-reservations/ticker-reservations.controller.ts @@ -50,8 +50,7 @@ export class TickerReservationsController { @ApiOperation({ summary: 'Get ticker registration configuration', - description: - 'This endpoint returns the chain-wide rules used to validate ticker registrations', + description: 'This endpoint returns the chain-wide rules used to validate ticker registrations', }) @ApiOkResponse({ description: 'Chain-wide ticker registration configuration', From d83b48857961bf60b991ca86ceb03b1f610f032b Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:05:37 +0530 Subject: [PATCH 34/34] =?UTF-8?q?chore:=20=F0=9F=A4=96=20exclude=20dist/?= =?UTF-8?q?=20from=20docker=20build=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The final image stage's `COPY --chown=node:node . /home/node` copies the whole build context over the top of the fresh `./dist` the builder stage just produced. `dist` is gitignored but wasn't dockerignored, so a stale local `dist/` (e.g. from a prior `yarn build`) silently wins over the actual build with no warning - same image digest whether cached or --no-cache, since the COPY step's content hash matches. --- .dockerignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 45229ed5..ca78daec 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ .git .env* -node_modules/ \ No newline at end of file +node_modules/ +/dist \ No newline at end of file