Skip to content

Commit 85ee08e

Browse files
authored
Merge pull request #8078 from BitGo/COIN-7533
feat: added token in oneStepPreApproval builder
2 parents 98aec6c + 27a9bf0 commit 85ee08e

6 files changed

Lines changed: 88 additions & 1 deletion

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export interface TransactionBroadcastData {
132132

133133
export interface CantonOneStepEnablementRequest extends CantonPrepareCommandRequest {
134134
receiverId: string;
135+
token?: string;
135136
}
136137

137138
export interface CantonTransferAcceptRejectRequest extends CantonPrepareCommandRequest {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import utils from './utils';
88
export class OneStepPreApprovalBuilder extends TransactionBuilder {
99
private _commandId: string;
1010
private _receiverPartyId: string;
11+
private _token: string;
1112
constructor(_coinConfig: Readonly<CoinConfig>) {
1213
super(_coinConfig);
1314
}
@@ -73,6 +74,20 @@ export class OneStepPreApprovalBuilder extends TransactionBuilder {
7374
return this;
7475
}
7576

77+
/**
78+
* Sets the optional token field if present, used for canton token preApproval setup
79+
* @param name - the bitgo name of the token
80+
* @returns The current builder for chaining
81+
* @throws Error if name is invalid
82+
*/
83+
token(name: string): this {
84+
if (!name || !name.trim()) {
85+
throw new Error('token name must be a non-empty string');
86+
}
87+
this._token = name.trim();
88+
return this;
89+
}
90+
7691
/**
7792
* Builds and returns the CantonOneStepEnablementRequest object from the builder's internal state.
7893
*
@@ -91,6 +106,7 @@ export class OneStepPreApprovalBuilder extends TransactionBuilder {
91106
verboseHashing: false,
92107
actAs: [this._receiverPartyId],
93108
readAs: [],
109+
token: this._token,
94110
};
95111
}
96112

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,14 @@ export class Transaction extends BaseTransaction {
244244
case TransactionType.TransferAccept:
245245
case TransactionType.TransferReject: {
246246
const txData = this.toJson();
247-
inputs.push({ address: txData.sender, amount: txData.amount });
247+
const input: ITransactionRecipient = {
248+
address: txData.sender,
249+
amount: txData.amount,
250+
};
251+
if (txData.token) {
252+
input.tokenName = txData.token;
253+
}
254+
inputs.push(input);
248255
inputAmount = txData.amount;
249256
break;
250257
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class Utils implements BaseUtils {
9898
let instrumentAdmin: string | undefined;
9999
let token: string | undefined;
100100
let preApprovalNode: RecordField[] = [];
101+
let tokenPreApprovalNode: RecordField[] = [];
101102
let transferNode: RecordField[] = [];
102103
let transferAcceptRejectNode: RecordField[] = [];
103104
let tokenTransferAcceptRejectNode: RecordField[] = [];
@@ -124,6 +125,13 @@ export class Utils implements BaseUtils {
124125
) {
125126
preApprovalNode = fields;
126127
}
128+
if (
129+
template?.entityName === 'TransferPreapproval' &&
130+
!tokenPreApprovalNode.length &&
131+
txType === TransactionType.OneStepPreApproval
132+
) {
133+
tokenPreApprovalNode = fields;
134+
}
127135
if (
128136
template?.entityName === 'Amulet' &&
129137
!transferAcceptRejectNode.length &&
@@ -182,6 +190,27 @@ export class Utils implements BaseUtils {
182190
const providerData = getField(preApprovalNode, 'provider');
183191
if (providerData?.oneofKind === 'party') sender = providerData.party ?? '';
184192
amount = '0';
193+
} else if (tokenPreApprovalNode.length) {
194+
const receiverData = getField(tokenPreApprovalNode, 'receiver');
195+
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
196+
const operatorData = getField(tokenPreApprovalNode, 'operator');
197+
if (operatorData?.oneofKind === 'party') sender = operatorData.party ?? '';
198+
amount = '0';
199+
const instrumentAdminData = getField(tokenPreApprovalNode, 'instrumentAdmin');
200+
if (instrumentAdminData?.oneofKind === 'party') instrumentAdmin = instrumentAdminData.party ?? '';
201+
const allowancesData = getField(tokenPreApprovalNode, 'instrumentAllowances');
202+
if (allowancesData?.oneofKind === 'list') {
203+
// for the same instrument admin, if multiple tokens are supported then we can enable all of them,
204+
// but we won't be doing that for now
205+
const firstAllowance = allowancesData.list?.elements?.[0]?.sum;
206+
if (firstAllowance?.oneofKind === 'record') {
207+
const allowanceFields = firstAllowance.record?.fields ?? [];
208+
const idData = getField(allowanceFields, 'id');
209+
if (idData?.oneofKind === 'text') {
210+
instrumentId = idData.text ?? '';
211+
}
212+
}
213+
}
185214
} else if (transferNode.length) {
186215
const transferField = transferNode.find((f) => f.label === 'transfer');
187216
const transferSum = transferField?.value?.sum;

modules/sdk-coin-canton/test/resources.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ export const InvalidOneStepPreApprovalPrepareResponse = {
9393
hashingDetails: null,
9494
};
9595

96+
export const CantonTokenPreApprovalPrepareResponse = {
97+
preparedTransaction:
98+
'CoYKCgMyLjESATAa1wkKATDCPtAJCs0JCgMyLjESQjAwYWNhMzZkYzZiMGUxMmI1MDI1YWY3ZjY5YmJlYmE5YjY0MzMzMDVlZTE2ZjhkM2Q3OGU4NTkxMTlkZmQyNmUyOBoXdXRpbGl0eS1yZWdpc3RyeS1hcHAtdjAiigEKQDY2MTE1MTc2OGQ2OTE0MWRmZDZiNzU3YmRjMzQ4MDEyYjJhNjRiMmM5NTZhZDU4Njc4MmY5N2QwNzQwOGYyNjQSMVV0aWxpdHkuUmVnaXN0cnkuQXBwLlYwLk1vZGVsLlRyYW5zZmVyUHJlYXBwcm92YWwaE1RyYW5zZmVyUHJlYXBwcm92YWwqtwVytAUKigEKQDY2MTE1MTc2OGQ2OTE0MWRmZDZiNzU3YmRjMzQ4MDEyYjJhNjRiMmM5NTZhZDU4Njc4MmY5N2QwNzQwOGYyNjQSMVV0aWxpdHkuUmVnaXN0cnkuQXBwLlYwLk1vZGVsLlRyYW5zZmVyUHJlYXBwcm92YWwaE1RyYW5zZmVyUHJlYXBwcm92YWwSdgoIb3BlcmF0b3ISajpoYXV0aDBfMDA3YzY1Zjg1N2YxYzNkNTk5Y2I2ZGY3Mzc3NTo6MTIyMGQyZDczMmQwNDJjMjgxY2VlODBmNDgzYWI4MGYzY2JhYTQ3ODI4NjBlZDVmNGRjMjI4YWIwM2RlZGQyZWU4ZjkSaQoIcmVjZWl2ZXISXTpbcmF2aS0yLXN0ZXAtcGFydHktbmV3OjoxMjIwOTJlN2QzM2FjMTBjMGYzZDU1OTc2MzQyZjM3NTU1ZGYwNWRhNWI3NDI5NTZkNTZhNjJhZTIzNjc3NjkwNzlkMhJ9Cg9pbnN0cnVtZW50QWRtaW4SajpoYXV0aDBfMDA3YzY1Zjg1N2YxYzNkNTk5Y2I2ZGY3Mzc3NTo6MTIyMGQyZDczMmQwNDJjMjgxY2VlODBmNDgzYWI4MGYzY2JhYTQ3ODI4NjBlZDVmNGRjMjI4YWIwM2RlZGQyZWU4ZjkSwgEKFGluc3RydW1lbnRBbGxvd2FuY2VzEqkBWqYBCqMBcqABCooBCkA2NjExNTE3NjhkNjkxNDFkZmQ2Yjc1N2JkYzM0ODAxMmIyYTY0YjJjOTU2YWQ1ODY3ODJmOTdkMDc0MDhmMjY0EjFVdGlsaXR5LlJlZ2lzdHJ5LkFwcC5WMC5Nb2RlbC5UcmFuc2ZlclByZWFwcHJvdmFsGhNJbnN0cnVtZW50QWxsb3dhbmNlEhEKAmlkEgtCCVRlc3RDb2luMTJbcmF2aS0yLXN0ZXAtcGFydHktbmV3OjoxMjIwOTJlN2QzM2FjMTBjMGYzZDU1OTc2MzQyZjM3NTU1ZGYwNWRhNWI3NDI5NTZkNTZhNjJhZTIzNjc3NjkwNzlkMjpoYXV0aDBfMDA3YzY1Zjg1N2YxYzNkNTk5Y2I2ZGY3Mzc3NTo6MTIyMGQyZDczMmQwNDJjMjgxY2VlODBmNDgzYWI4MGYzY2JhYTQ3ODI4NjBlZDVmNGRjMjI4YWIwM2RlZGQyZWU4Zjk6W3JhdmktMi1zdGVwLXBhcnR5LW5ldzo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDIiIhIgonlu8JNr2xJ7NdQe9F9z/79FyBc67vEKrGmbd+r0wtsSigISgwEKW3JhdmktMi1zdGVwLXBhcnR5LW5ldzo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDISJDdkOTk3ODlkLTJmMjItNDllMS04NWNiLTc5ZDJjZTVhNjljMRpTZ2xvYmFsLWRvbWFpbjo6MTIyMGJlNThjMjllNjVkZTQwYmYyNzNiZTFkYzJiMjY2ZDQzYTlhMDAyZWE1YjE4OTU1YWVlZjdhYWM4ODFiYjQ3MWEqJDQ3NzA1ZTk4LWM3NTUtNGM4Ny1hYmI3LTBiOWMyNGM5Mzk3ZjDf4Zy2rMSSAw==',
99+
preparedTransactionHash: 'zl1kTTr96UWEpR0h9MzvLBZItsttVbwBI0JVJOgJZUg=',
100+
hashingSchemeVersion: 'HASHING_SCHEME_VERSION_V2',
101+
hashingDetails: null,
102+
costEstimation: {
103+
estimationTimestamp: '2026-02-06T07:26:30.017983Z',
104+
confirmationRequestTrafficCostEstimation: 3697,
105+
confirmationResponseTrafficCostEstimation: 452,
106+
totalTrafficCostEstimation: 4149,
107+
},
108+
};
109+
96110
export const CANTON_ADDRESSES = {
97111
VALID_ADDRESS: '1220a::1220a43d89dc7d8f85316116aac093667f769fce55411aef6846ccb933b2e1a3b598',
98112
// party hint should be present

modules/sdk-coin-canton/test/unit/builder/oneStepEnablement/oneStepEnablementBuilder.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { OneStepPreApprovalBuilder, Transaction } from '../../../../src';
77
import { CantonOneStepEnablementRequest } from '../../../../src/lib/iface';
88

99
import {
10+
CantonTokenPreApprovalPrepareResponse,
1011
InvalidOneStepPreApprovalPrepareResponse,
1112
OneStepEnablement,
1213
OneStepPreApprovalPrepareResponse,
@@ -29,6 +30,25 @@ describe('Wallet Pre-approval Enablement Builder', () => {
2930
assert.equal(actAs, partyId);
3031
});
3132

33+
it('should get the one step enablement request object for token', function () {
34+
const txBuilder = new OneStepPreApprovalBuilder(coins.get('tcanton'));
35+
const oneStepEnablementTx = new Transaction(coins.get('tcanton'));
36+
txBuilder.initBuilder(oneStepEnablementTx);
37+
txBuilder.setTransaction(CantonTokenPreApprovalPrepareResponse);
38+
const commandId = '7d99789d-2f22-49e1-85cb-79d2ce5a69c1';
39+
const partyId = 'ravi-2-step-party-new::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2';
40+
const token = 'tcanton:testcoin1';
41+
txBuilder.commandId(commandId).receiverPartyId(partyId).token(token);
42+
const requestObj: CantonOneStepEnablementRequest = txBuilder.toRequestObject();
43+
should.exist(requestObj);
44+
assert.equal(requestObj.commandId, commandId);
45+
assert.equal(requestObj.receiverId, partyId);
46+
assert.equal(requestObj.token, token);
47+
assert.equal(requestObj.actAs.length, 1);
48+
const actAs = requestObj.actAs[0];
49+
assert.equal(actAs, partyId);
50+
});
51+
3252
it('should validate raw transaction', function () {
3353
const txBuilder = new OneStepPreApprovalBuilder(coins.get('tcanton'));
3454
const oneStepEnablementTx = new Transaction(coins.get('tcanton'));

0 commit comments

Comments
 (0)