From 5bfc5ae6bf51e47fde7a0e954e82ae38266f95e5 Mon Sep 17 00:00:00 2001 From: Ravi Hegde Date: Mon, 19 Oct 2026 13:56:20 +0530 Subject: [PATCH] feat(sdk-coin-canton): add participant allow-list command builders Ticket: CHALO-1850 --- .../src/lib/cantonCommandBuilder.ts | 54 ++++++++++++- modules/sdk-coin-canton/src/lib/iface.ts | 10 +++ .../cantonCommand/cantonCommandBuilder.ts | 76 ++++++++++++++++++- 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts b/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts index c267d0fbf0d..ca34f4fe9fb 100644 --- a/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts +++ b/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts @@ -4,9 +4,15 @@ import { TransactionType, CantonCommand, CantonCommandResolveContractSpec, + CantonExerciseCommand, } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { CANTON_COMMAND_KEYS, CantonCommandRequest, CantonPrepareCommandResponse } from './iface'; +import { + CANTON_COMMAND_KEYS, + CantonAllowListCommandParams, + CantonCommandRequest, + CantonPrepareCommandResponse, +} from './iface'; import { TransactionBuilder } from './transactionBuilder'; import { Transaction } from './transaction/transaction'; import utils from './utils'; @@ -127,6 +133,52 @@ export class CantonCommandBuilder extends TransactionBuilder { return this; } + /** + * Sets the V3 participant allow-list add choice. + * + * The DAML choice is exercised on ParticipantService, while allowListCid and + * credentialCids are supplied as the choice argument. + */ + addWalletsToAllowList(params: CantonAllowListCommandParams): this { + return this.setAllowListCommand(params, 'ParticipantService_AddWalletsToAllowList'); + } + + /** Sets the V3 participant allow-list remove choice. */ + removeWalletsFromAllowList(params: CantonAllowListCommandParams): this { + return this.setAllowListCommand(params, 'ParticipantService_RemoveWalletsFromAllowList'); + } + + private setAllowListCommand( + params: CantonAllowListCommandParams, + choice: 'ParticipantService_AddWalletsToAllowList' | 'ParticipantService_RemoveWalletsFromAllowList' + ): this { + const templateId = params?.templateId?.trim(); + const participantServiceCid = params?.participantServiceCid?.trim(); + const allowListCid = params?.allowListCid?.trim(); + if (!templateId) throw new Error('templateId must be a non-empty string'); + if (!participantServiceCid) throw new Error('participantServiceCid must be a non-empty string'); + if (!allowListCid) throw new Error('allowListCid must be a non-empty string'); + if (!Array.isArray(params.credentialCids)) throw new Error('credentialCids must be an array'); + + const credentialCids = params.credentialCids.map((credentialCid) => credentialCid?.trim()); + if (credentialCids.some((credentialCid) => !credentialCid)) { + throw new Error('credentialCids must contain only non-empty strings'); + } + if (new Set(credentialCids).size !== credentialCids.length) { + throw new Error('credentialCids must not contain duplicates'); + } + + const command: CantonExerciseCommand = { + ExerciseCommand: { + templateId, + contractId: participantServiceCid, + choice, + choiceArgument: { allowListCid, credentialCids }, + }, + }; + return this.command(command); + } + /** * Sets the list of ACS contract resolution specs that IMS will resolve before prepare. * diff --git a/modules/sdk-coin-canton/src/lib/iface.ts b/modules/sdk-coin-canton/src/lib/iface.ts index 845c11711b4..e824629fd43 100644 --- a/modules/sdk-coin-canton/src/lib/iface.ts +++ b/modules/sdk-coin-canton/src/lib/iface.ts @@ -278,6 +278,16 @@ export interface CantonCommandRequest { resolveContracts?: CantonCommandResolveContractSpec[]; token?: string; } +export interface CantonAllowListCommandParams { + /** Fully-qualified V3 ParticipantService template ID, including package reference. */ + templateId: string; + /** ParticipantService contract being exercised. */ + participantServiceCid: string; + /** Participant-owned AllowList contract passed to the choice. */ + allowListCid: string; + /** End-investor Credential contracts. Empty is valid and preserves DAR no-op semantics. */ + credentialCids: string[]; +} // Root command decoded from the prepared Canton transaction protobuf, used during verifyTransaction. export interface CantonCommandInfo { diff --git a/modules/sdk-coin-canton/test/unit/builder/cantonCommand/cantonCommandBuilder.ts b/modules/sdk-coin-canton/test/unit/builder/cantonCommand/cantonCommandBuilder.ts index 1fbc5796508..4422f1768df 100644 --- a/modules/sdk-coin-canton/test/unit/builder/cantonCommand/cantonCommandBuilder.ts +++ b/modules/sdk-coin-canton/test/unit/builder/cantonCommand/cantonCommandBuilder.ts @@ -7,7 +7,7 @@ import { coins } from '@bitgo/statics'; import { CantonCommandBuilder } from '../../../../src/lib/cantonCommandBuilder'; import { TransactionBuilderFactory } from '../../../../src/lib/transactionBuilderFactory'; import { Transaction } from '../../../../src/lib/transaction/transaction'; -import { CantonCommandRequest } from '../../../../src/lib/iface'; +import { CantonAllowListCommandParams, CantonCommandRequest } from '../../../../src/lib/iface'; import { CantonExerciseCommandPrepareResponse } from '../../../resources'; const PARTY_A = 'RegRegistrar::1220508e54fb709dcb1a9b4096408ad415d2217d8881ad9034a9850752933ba88ad5'; @@ -137,7 +137,81 @@ describe('CantonCommandBuilder', () => { assert.throws(() => builder.command(null as unknown as CantonExerciseCommand), /command must be a plain object/); }); }); + describe('participant allow-list choices', () => { + const allowListParams: CantonAllowListCommandParams = { + templateId: TEMPLATE_ID, + participantServiceCid: CONTRACT_ID, + allowListCid: 'allow-list-cid', + credentialCids: ['credential-a', 'credential-b'], + }; + + it('builds the V3 add choice with the ParticipantService contract', () => { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + builder.commandId('cmd-allow-add').actAs([PARTY_A]).addWalletsToAllowList(allowListParams); + + const request = builder.toRequestObject(); + assert.deepEqual(request.command, { + ExerciseCommand: { + templateId: TEMPLATE_ID, + contractId: CONTRACT_ID, + choice: 'ParticipantService_AddWalletsToAllowList', + choiceArgument: { + allowListCid: 'allow-list-cid', + credentialCids: ['credential-a', 'credential-b'], + }, + }, + }); + }); + + it('builds the V3 remove choice and preserves an empty credential list', () => { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + builder + .commandId('cmd-allow-remove') + .actAs([PARTY_A]) + .removeWalletsFromAllowList({ + ...allowListParams, + credentialCids: [], + }); + + const request = builder.toRequestObject(); + assert.equal( + (request.command as CantonExerciseCommand).ExerciseCommand.choice, + 'ParticipantService_RemoveWalletsFromAllowList' + ); + assert.deepEqual((request.command as CantonExerciseCommand).ExerciseCommand.choiceArgument, { + allowListCid: 'allow-list-cid', + credentialCids: [], + }); + }); + it('rejects duplicate credential IDs', () => { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + assert.throws( + () => + builder.addWalletsToAllowList({ + ...allowListParams, + credentialCids: ['credential-a', 'credential-a'], + }), + /credentialCids must not contain duplicates/ + ); + }); + + it('rejects missing contract identifiers', () => { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + assert.throws( + () => builder.addWalletsToAllowList({ ...allowListParams, allowListCid: ' ' }), + /allowListCid must be a non-empty string/ + ); + }); + }); describe('token()', () => { it('should set the token', function () { const builder = new CantonCommandBuilder(coins.get('tcanton'));