diff --git a/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts b/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts index 193f662f2f..c267d0fbf0 100644 --- a/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts +++ b/modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts @@ -17,6 +17,7 @@ export class CantonCommandBuilder extends TransactionBuilder { private _readAs: string[] = []; private _command: CantonCommand; private _resolveContracts: CantonCommandResolveContractSpec[] = []; + private _token?: string; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -137,6 +138,21 @@ export class CantonCommandBuilder extends TransactionBuilder { return this; } + /** + * Sets the Canton token identifier (e.g. 'tcanton:stgusd1') forwarded to IMS for + * choice-context resolution on token-specific commands such as mint and burn. + * + * @param name - Registered BitGo canton token name + * @returns The current builder instance for chaining. + */ + token(name: string): this { + if (typeof name !== 'string' || !name.trim()) { + throw new Error('token must be a non-empty string'); + } + this._token = name.trim(); + return this; + } + /** * Builds and returns the CantonCommandRequest from the builder's internal state. * @@ -146,13 +162,17 @@ export class CantonCommandBuilder extends TransactionBuilder { toRequestObject(): CantonCommandRequest { this.validate(); - return { + const req: CantonCommandRequest = { commandId: this._commandId, actAs: this._actAs, readAs: this._readAs ?? [], command: this._command, resolveContracts: this._resolveContracts ?? [], }; + if (this._token) { + req.token = this._token; + } + return req; } private validate(): void { diff --git a/modules/sdk-coin-canton/src/lib/iface.ts b/modules/sdk-coin-canton/src/lib/iface.ts index 39d875c231..4a178b5d8c 100644 --- a/modules/sdk-coin-canton/src/lib/iface.ts +++ b/modules/sdk-coin-canton/src/lib/iface.ts @@ -231,6 +231,7 @@ export interface CantonCommandRequest { readAs?: string[]; command: CantonCommand; resolveContracts?: CantonCommandResolveContractSpec[]; + token?: string; } // Root command decoded from the prepared Canton transaction protobuf, used during verifyTransaction. 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 5af34acbf0..1fbc579650 100644 --- a/modules/sdk-coin-canton/test/unit/builder/cantonCommand/cantonCommandBuilder.ts +++ b/modules/sdk-coin-canton/test/unit/builder/cantonCommand/cantonCommandBuilder.ts @@ -138,6 +138,38 @@ describe('CantonCommandBuilder', () => { }); }); + describe('token()', () => { + it('should set the token', function () { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + builder.commandId('cmd-tok-1').actAs([PARTY_A]).command(sampleExerciseCommand).token('tcanton:testtoken'); + assert.equal(builder.toRequestObject().token, 'tcanton:testtoken'); + }); + + it('should trim whitespace', function () { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + builder.commandId('cmd-tok-2').actAs([PARTY_A]).command(sampleExerciseCommand).token(' tcanton:testtoken '); + assert.equal(builder.toRequestObject().token, 'tcanton:testtoken'); + }); + + it('should throw on empty string', function () { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + assert.throws(() => builder.token(''), /token must be a non-empty string/); + }); + + it('should throw on whitespace-only string', function () { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + assert.throws(() => builder.token(' '), /token must be a non-empty string/); + }); + }); + describe('resolveContracts()', () => { it('should set the spec array', function () { const spec = [{ templateId: TEMPLATE_ID, actAs: [PARTY_A], injectAs: 'command.ExerciseCommand.contractId' }]; @@ -206,6 +238,24 @@ describe('CantonCommandBuilder', () => { const req = builder.toRequestObject(); assert.deepEqual(req.resolveContracts, []); }); + + it('should include token when set', function () { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + builder.commandId('cmd-003').actAs([PARTY_A]).command(sampleExerciseCommand).token('tcanton:testtoken'); + const req = builder.toRequestObject(); + assert.equal(req.token, 'tcanton:testtoken'); + }); + + it('should not include token key when not set', function () { + const builder = new CantonCommandBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + builder.initBuilder(tx); + builder.commandId('cmd-004').actAs([PARTY_A]).command(sampleExerciseCommand); + const req = builder.toRequestObject(); + assert.ok(!('token' in req)); + }); }); describe('initBuilder()', () => { diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts index 19682197f4..e5c131603c 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts @@ -93,7 +93,7 @@ export interface CantonExerciseCommand { templateId: string; contractId?: string; choice: string; - choiceArgument: Record; + choiceArgument?: Record; }; } diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 80b0133f8f..bd8e5ed909 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -4301,6 +4301,7 @@ export class Wallet implements IWallet { reqId, intentType: 'cantonCommand', cantonCommandParams: params.cantonCommandParams, + tokenName: params.tokenName, sequenceId: params.sequenceId, comment: params.comment, },