Skip to content

Commit 1fde476

Browse files
committed
feat(sdk-coin-canton): forward token and make choiceArgument optional
Ticket: SCAAS-9624
1 parent 142d72d commit 1fde476

5 files changed

Lines changed: 116 additions & 3 deletions

File tree

modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
CantonCommand,
66
CantonCommandResolveContractSpec,
77
} from '@bitgo/sdk-core';
8-
import { BaseCoin as CoinConfig } from '@bitgo/statics';
8+
import { BaseCoin as CoinConfig, coins } from '@bitgo/statics';
99
import { CANTON_COMMAND_KEYS, CantonCommandRequest, CantonPrepareCommandResponse } from './iface';
1010
import { TransactionBuilder } from './transactionBuilder';
1111
import { Transaction } from './transaction/transaction';
@@ -17,6 +17,7 @@ export class CantonCommandBuilder extends TransactionBuilder {
1717
private _readAs: string[] = [];
1818
private _command: CantonCommand;
1919
private _resolveContracts: CantonCommandResolveContractSpec[] = [];
20+
private _token?: string;
2021

2122
constructor(_coinConfig: Readonly<CoinConfig>) {
2223
super(_coinConfig);
@@ -137,6 +138,34 @@ export class CantonCommandBuilder extends TransactionBuilder {
137138
return this;
138139
}
139140

141+
/**
142+
* Sets the Canton token identifier (e.g. 'tcanton:stgusd1') forwarded to IMS for
143+
* choice-context resolution on token-specific commands such as mint and burn.
144+
*
145+
* @param name - Registered BitGo canton token name
146+
* @returns The current builder instance for chaining.
147+
*/
148+
token(name: string): this {
149+
if (typeof name !== 'string' || !name.trim()) {
150+
throw new Error('token must be a non-empty string');
151+
}
152+
const tokenName = name.trim();
153+
let coinConfig: ReturnType<typeof coins.get>;
154+
try {
155+
coinConfig = coins.get(tokenName);
156+
} catch {
157+
throw new Error(`token is not a registered coin: ${tokenName}`);
158+
}
159+
if (coinConfig.network.type !== this._coinConfig.network.type) {
160+
throw new Error(`token network must match builder network: ${tokenName}`);
161+
}
162+
if (coinConfig.family !== 'canton' || !coinConfig.isToken) {
163+
throw new Error(`token must be a registered canton token: ${tokenName}`);
164+
}
165+
this._token = tokenName;
166+
return this;
167+
}
168+
140169
/**
141170
* Builds and returns the CantonCommandRequest from the builder's internal state.
142171
*
@@ -146,13 +175,17 @@ export class CantonCommandBuilder extends TransactionBuilder {
146175
toRequestObject(): CantonCommandRequest {
147176
this.validate();
148177

149-
return {
178+
const req: CantonCommandRequest = {
150179
commandId: this._commandId,
151180
actAs: this._actAs,
152181
readAs: this._readAs ?? [],
153182
command: this._command,
154183
resolveContracts: this._resolveContracts ?? [],
155184
};
185+
if (this._token) {
186+
req.token = this._token;
187+
}
188+
return req;
156189
}
157190

158191
private validate(): void {

modules/sdk-coin-canton/src/lib/iface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export interface CantonCommandRequest {
231231
readAs?: string[];
232232
command: CantonCommand;
233233
resolveContracts?: CantonCommandResolveContractSpec[];
234+
token?: string;
234235
}
235236

236237
// Root command decoded from the prepared Canton transaction protobuf, used during verifyTransaction.

modules/sdk-coin-canton/test/unit/builder/cantonCommand/cantonCommandBuilder.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,66 @@ describe('CantonCommandBuilder', () => {
138138
});
139139
});
140140

141+
describe('token()', () => {
142+
it('should set the token', function () {
143+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
144+
const tx = new Transaction(coins.get('tcanton'));
145+
builder.initBuilder(tx);
146+
builder.commandId('cmd-tok-1').actAs([PARTY_A]).command(sampleExerciseCommand).token('tcanton:testtoken');
147+
assert.equal(builder.toRequestObject().token, 'tcanton:testtoken');
148+
});
149+
150+
it('should trim whitespace', function () {
151+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
152+
const tx = new Transaction(coins.get('tcanton'));
153+
builder.initBuilder(tx);
154+
builder.commandId('cmd-tok-2').actAs([PARTY_A]).command(sampleExerciseCommand).token(' tcanton:testtoken ');
155+
assert.equal(builder.toRequestObject().token, 'tcanton:testtoken');
156+
});
157+
158+
it('should throw on empty string', function () {
159+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
160+
const tx = new Transaction(coins.get('tcanton'));
161+
builder.initBuilder(tx);
162+
assert.throws(() => builder.token(''), /token must be a non-empty string/);
163+
});
164+
165+
it('should throw on whitespace-only string', function () {
166+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
167+
const tx = new Transaction(coins.get('tcanton'));
168+
builder.initBuilder(tx);
169+
assert.throws(() => builder.token(' '), /token must be a non-empty string/);
170+
});
171+
172+
it('should throw on an unregistered coin name', function () {
173+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
174+
const tx = new Transaction(coins.get('tcanton'));
175+
builder.initBuilder(tx);
176+
assert.throws(() => builder.token('tcanton:fakecoin'), /token is not a registered coin/);
177+
});
178+
179+
it('should throw when token is not a canton family token', function () {
180+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
181+
const tx = new Transaction(coins.get('tcanton'));
182+
builder.initBuilder(tx);
183+
assert.throws(() => builder.token('teth'), /token must be a registered canton token/);
184+
});
185+
186+
it('should throw when token network does not match builder network', function () {
187+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
188+
const tx = new Transaction(coins.get('tcanton'));
189+
builder.initBuilder(tx);
190+
assert.throws(() => builder.token('canton:usd1'), /token network must match builder network/);
191+
});
192+
193+
it('should throw when token is the base canton coin (not a token)', function () {
194+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
195+
const tx = new Transaction(coins.get('tcanton'));
196+
builder.initBuilder(tx);
197+
assert.throws(() => builder.token('tcanton'), /token must be a registered canton token/);
198+
});
199+
});
200+
141201
describe('resolveContracts()', () => {
142202
it('should set the spec array', function () {
143203
const spec = [{ templateId: TEMPLATE_ID, actAs: [PARTY_A], injectAs: 'command.ExerciseCommand.contractId' }];
@@ -206,6 +266,24 @@ describe('CantonCommandBuilder', () => {
206266
const req = builder.toRequestObject();
207267
assert.deepEqual(req.resolveContracts, []);
208268
});
269+
270+
it('should include token when set', function () {
271+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
272+
const tx = new Transaction(coins.get('tcanton'));
273+
builder.initBuilder(tx);
274+
builder.commandId('cmd-003').actAs([PARTY_A]).command(sampleExerciseCommand).token('tcanton:testtoken');
275+
const req = builder.toRequestObject();
276+
assert.equal(req.token, 'tcanton:testtoken');
277+
});
278+
279+
it('should not include token key when not set', function () {
280+
const builder = new CantonCommandBuilder(coins.get('tcanton'));
281+
const tx = new Transaction(coins.get('tcanton'));
282+
builder.initBuilder(tx);
283+
builder.commandId('cmd-004').actAs([PARTY_A]).command(sampleExerciseCommand);
284+
const req = builder.toRequestObject();
285+
assert.ok(!('token' in req));
286+
});
209287
});
210288

211289
describe('initBuilder()', () => {

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export interface CantonExerciseCommand {
9393
templateId: string;
9494
contractId?: string;
9595
choice: string;
96-
choiceArgument: Record<string, unknown>;
96+
choiceArgument?: Record<string, unknown>;
9797
};
9898
}
9999

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4301,6 +4301,7 @@ export class Wallet implements IWallet {
43014301
reqId,
43024302
intentType: 'cantonCommand',
43034303
cantonCommandParams: params.cantonCommandParams,
4304+
tokenName: params.tokenName,
43044305
sequenceId: params.sequenceId,
43054306
comment: params.comment,
43064307
},

0 commit comments

Comments
 (0)