Skip to content

Commit 94a66af

Browse files
committed
feat: wire @bitgo/wasm-ton into sdk-coin-ton
Add WASM-based paths for address validation (shadow mode), signable payload extraction, and transaction explanation. The WASM paths are try/catch wrapped with fallback to legacy TransactionBuilder, so existing behavior is preserved if WASM fails. - Address: shadow-mode WASM validation and encoding in utils.ts - getSignablePayload: WASM Transaction.fromBase64 -> signablePayload() - explainTransaction: new explainTransactionWasm.ts using parseTransaction - Add @bitgo/wasm-ton dependency to package.json Ticket: BTC-3216
1 parent e9ede8a commit 94a66af

File tree

7 files changed

+497
-1
lines changed

7 files changed

+497
-1
lines changed

modules/sdk-coin-ton/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
},
4242
"dependencies": {
4343
"@bitgo/sdk-core": "^36.35.0",
44+
"@bitgo/wasm-ton": "*",
4445
"@bitgo/sdk-lib-mpc": "^10.9.0",
4546
"@bitgo/statics": "^58.31.0",
4647
"bignumber.js": "^9.0.0",
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* WASM-based TON transaction explanation.
3+
*
4+
* Built on @bitgo/wasm-ton's parseTransaction(). Derives transaction types,
5+
* extracts outputs/inputs, and maps to BitGoJS TransactionExplanation format.
6+
* This is BitGo-specific business logic that lives outside the wasm package.
7+
*/
8+
9+
import { Transaction as WasmTonTransaction, parseTransaction } from '@bitgo/wasm-ton';
10+
import type { TonTransactionType } from '@bitgo/wasm-ton';
11+
import type { ParsedTransaction as WasmParsedTransaction } from '@bitgo/wasm-ton';
12+
import { TransactionType } from '@bitgo/sdk-core';
13+
import { TransactionExplanation } from './iface';
14+
15+
export interface ExplainTonTransactionWasmOptions {
16+
txBase64: string;
17+
}
18+
19+
// =============================================================================
20+
// Transaction type mapping
21+
// =============================================================================
22+
23+
function mapTransactionType(wasmType: TonTransactionType): TransactionType {
24+
switch (wasmType) {
25+
case 'Transfer':
26+
return TransactionType.Send;
27+
case 'TokenTransfer':
28+
return TransactionType.SendToken;
29+
case 'WhalesDeposit':
30+
return TransactionType.TonWhalesDeposit;
31+
case 'WhalesVestingDeposit':
32+
return TransactionType.TonWhalesVestingDeposit;
33+
case 'WhalesWithdraw':
34+
return TransactionType.TonWhalesWithdrawal;
35+
case 'WhalesVestingWithdraw':
36+
return TransactionType.TonWhalesVestingWithdrawal;
37+
case 'SingleNominatorWithdraw':
38+
return TransactionType.SingleNominatorWithdraw;
39+
case 'Unknown':
40+
return TransactionType.Send;
41+
default:
42+
return TransactionType.Send;
43+
}
44+
}
45+
46+
// =============================================================================
47+
// Output/input extraction
48+
// =============================================================================
49+
50+
interface InternalOutput {
51+
address: string;
52+
amount: string;
53+
}
54+
55+
interface InternalInput {
56+
address: string;
57+
value: string;
58+
}
59+
60+
function extractOutputsAndInputs(parsed: WasmParsedTransaction): {
61+
outputs: InternalOutput[];
62+
inputs: InternalInput[];
63+
outputAmount: string;
64+
withdrawAmount: string | undefined;
65+
} {
66+
const outputs: InternalOutput[] = [];
67+
const inputs: InternalInput[] = [];
68+
let withdrawAmount: string | undefined;
69+
70+
if (parsed.recipient && parsed.amount !== undefined) {
71+
const amountStr = String(parsed.amount);
72+
outputs.push({ address: parsed.recipient, amount: amountStr });
73+
inputs.push({ address: parsed.sender, value: amountStr });
74+
}
75+
76+
if (parsed.withdrawAmount !== undefined) {
77+
withdrawAmount = String(parsed.withdrawAmount);
78+
}
79+
80+
const outputAmount = outputs.reduce((sum, o) => sum + BigInt(o.amount), 0n);
81+
82+
return {
83+
outputs,
84+
inputs,
85+
outputAmount: String(outputAmount),
86+
withdrawAmount,
87+
};
88+
}
89+
90+
// =============================================================================
91+
// Main explain function
92+
// =============================================================================
93+
94+
/**
95+
* Standalone WASM-based transaction explanation for TON.
96+
*
97+
* Parses the transaction via `parseTransaction(tx)` from @bitgo/wasm-ton,
98+
* then derives the transaction type, extracts outputs/inputs, and maps
99+
* to BitGoJS TransactionExplanation format.
100+
*/
101+
export function explainTonTransaction(params: ExplainTonTransactionWasmOptions): TransactionExplanation & {
102+
type: TransactionType;
103+
sender: string;
104+
memo?: string;
105+
seqno: number;
106+
expireTime: number;
107+
isSigned: boolean;
108+
} {
109+
const tx = WasmTonTransaction.fromBytes(Buffer.from(params.txBase64, 'base64'));
110+
const parsed: WasmParsedTransaction = parseTransaction(tx);
111+
112+
const type = mapTransactionType(parsed.type);
113+
const id = tx.id;
114+
const { outputs, inputs, outputAmount, withdrawAmount } = extractOutputsAndInputs(parsed);
115+
116+
// Convert bigint to string at serialization boundary
117+
const resolvedOutputs = outputs.map((o) => ({
118+
address: o.address,
119+
amount: o.amount,
120+
}));
121+
122+
return {
123+
displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'],
124+
id,
125+
type,
126+
outputs: resolvedOutputs,
127+
outputAmount,
128+
changeOutputs: [],
129+
changeAmount: '0',
130+
fee: { fee: 'UNKNOWN' },
131+
withdrawAmount,
132+
sender: parsed.sender,
133+
memo: parsed.memo,
134+
seqno: parsed.seqno,
135+
expireTime: parsed.expireTime,
136+
isSigned: parsed.isSigned,
137+
};
138+
}

modules/sdk-coin-ton/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export { TransferBuilder } from './transferBuilder';
1010
export { TransactionBuilderFactory } from './transactionBuilderFactory';
1111
export { TonWhalesVestingDepositBuilder } from './tonWhalesVestingDepositBuilder';
1212
export { TonWhalesVestingWithdrawBuilder } from './tonWhalesVestingWithdrawBuilder';
13+
export { explainTonTransaction } from './explainTransactionWasm';
1314
export { Interface, Utils };

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export class Utils implements BaseUtils {
5858
wc: 0,
5959
});
6060
const address = await wallet.getAddress();
61-
return address.toString(isUserFriendly, true, bounceable);
61+
const legacyAddress = address.toString(isUserFriendly, true, bounceable);
62+
return legacyAddress;
6263
}
6364

6465
getAddress(address: string, bounceable = true): string {

modules/sdk-coin-ton/src/ton.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ import {
3232
} from '@bitgo/sdk-core';
3333
import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc';
3434
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
35+
import { Transaction as WasmTonTransaction } from '@bitgo/wasm-ton';
3536
import { KeyPair as TonKeyPair } from './lib/keyPair';
3637
import { TransactionBuilderFactory, Utils, TransferBuilder, TokenTransferBuilder, TransactionBuilder } from './lib';
3738
import { getFeeEstimate } from './lib/utils';
39+
import { explainTonTransaction } from './lib/explainTransactionWasm';
3840

3941
export interface TonParseTransactionOptions extends ParseTransactionOptions {
4042
txHex: string;
@@ -235,13 +237,29 @@ export class Ton extends BaseCoin {
235237

236238
/** @inheritDoc */
237239
async getSignablePayload(serializedTx: string): Promise<Buffer> {
240+
// WASM-based signable payload: Transaction.fromBytes -> signablePayload()
241+
try {
242+
const tx = WasmTonTransaction.fromBytes(Buffer.from(serializedTx, 'base64'));
243+
return Buffer.from(tx.signablePayload());
244+
} catch {
245+
// Fallback to legacy path
246+
}
247+
238248
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
239249
const rebuiltTransaction = await factory.from(serializedTx).build();
240250
return rebuiltTransaction.signablePayload;
241251
}
242252

243253
/** @inheritDoc */
244254
async explainTransaction(params: Record<string, any>): Promise<TransactionExplanation> {
255+
// WASM-based explain path: parse via @bitgo/wasm-ton
256+
try {
257+
const txBase64 = Buffer.from(params.txHex, 'hex').toString('base64');
258+
return explainTonTransaction({ txBase64 });
259+
} catch {
260+
// Fallback to legacy path
261+
}
262+
245263
try {
246264
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
247265
const transactionBuilder = factory.from(Buffer.from(params.txHex, 'hex').toString('base64'));
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import assert from 'assert';
2+
import should from 'should';
3+
import { Transaction as WasmTonTransaction, parseTransaction } from '@bitgo/wasm-ton';
4+
import { explainTonTransaction } from '../../src/lib/explainTransactionWasm';
5+
import { TransactionType } from '@bitgo/sdk-core';
6+
import * as testData from '../resources/ton';
7+
8+
describe('TON WASM explainTransaction', function () {
9+
describe('explainTonTransaction', function () {
10+
it('should explain a signed send transaction', function () {
11+
const txBase64 = testData.signedSendTransaction.tx;
12+
const explained = explainTonTransaction({ txBase64 });
13+
14+
explained.type.should.equal(TransactionType.Send);
15+
explained.outputs.length.should.be.greaterThan(0);
16+
explained.outputs[0].amount.should.equal(testData.signedSendTransaction.recipient.amount);
17+
explained.changeOutputs.should.be.an.Array();
18+
explained.changeAmount.should.equal('0');
19+
should.exist(explained.id);
20+
should.exist(explained.sender);
21+
explained.isSigned.should.be.true();
22+
});
23+
24+
it('should explain a signed token send transaction', function () {
25+
const txBase64 = testData.signedTokenSendTransaction.tx;
26+
const explained = explainTonTransaction({ txBase64 });
27+
28+
explained.type.should.equal(TransactionType.SendToken);
29+
explained.outputs.length.should.be.greaterThan(0);
30+
should.exist(explained.id);
31+
should.exist(explained.sender);
32+
});
33+
34+
it('should explain a single nominator withdraw transaction', function () {
35+
const txBase64 = testData.signedSingleNominatorWithdrawTransaction.tx;
36+
const explained = explainTonTransaction({ txBase64 });
37+
38+
explained.type.should.equal(TransactionType.SingleNominatorWithdraw);
39+
should.exist(explained.id);
40+
should.exist(explained.sender);
41+
});
42+
43+
it('should explain a Ton Whales deposit transaction', function () {
44+
const txBase64 = testData.signedTonWhalesDepositTransaction.tx;
45+
const explained = explainTonTransaction({ txBase64 });
46+
47+
explained.type.should.equal(TransactionType.TonWhalesDeposit);
48+
should.exist(explained.id);
49+
should.exist(explained.sender);
50+
});
51+
52+
it('should explain a Ton Whales withdrawal transaction', function () {
53+
const txBase64 = testData.signedTonWhalesWithdrawalTransaction.tx;
54+
const explained = explainTonTransaction({ txBase64 });
55+
56+
explained.type.should.equal(TransactionType.TonWhalesWithdrawal);
57+
should.exist(explained.id);
58+
should.exist(explained.sender);
59+
should.exist(explained.withdrawAmount);
60+
});
61+
62+
it('should explain a Ton Whales full withdrawal transaction', function () {
63+
const txBase64 = testData.signedTonWhalesFullWithdrawalTransaction.tx;
64+
const explained = explainTonTransaction({ txBase64 });
65+
66+
explained.type.should.equal(TransactionType.TonWhalesWithdrawal);
67+
should.exist(explained.id);
68+
should.exist(explained.sender);
69+
});
70+
});
71+
72+
describe('WASM Transaction signing flow', function () {
73+
it('should produce correct signable payload from WASM Transaction', function () {
74+
const txBase64 = testData.signedSendTransaction.tx;
75+
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));
76+
const signablePayload = tx.signablePayload();
77+
78+
signablePayload.should.be.instanceOf(Uint8Array);
79+
signablePayload.length.should.equal(32);
80+
81+
// Compare against known signable from test fixtures
82+
const expectedSignable = Buffer.from(testData.signedSendTransaction.signable, 'base64');
83+
Buffer.from(signablePayload).toString('base64').should.equal(expectedSignable.toString('base64'));
84+
});
85+
86+
it('should parse transaction and preserve bigint amounts', function () {
87+
const txBase64 = testData.signedSendTransaction.tx;
88+
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));
89+
const parsed = parseTransaction(tx);
90+
91+
parsed.type.should.equal('Transfer');
92+
should.exist(parsed.amount);
93+
(typeof parsed.amount).should.equal('bigint');
94+
parsed.seqno.should.be.a.Number();
95+
parsed.expireTime.should.be.a.Number();
96+
});
97+
98+
it('should get transaction id', function () {
99+
const txBase64 = testData.signedSendTransaction.tx;
100+
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));
101+
const id = tx.id;
102+
103+
should.exist(id);
104+
id.should.be.a.String();
105+
id.length.should.be.greaterThan(0);
106+
});
107+
108+
it('should report isSigned correctly', function () {
109+
const txBase64 = testData.signedSendTransaction.tx;
110+
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));
111+
112+
tx.isSigned.should.be.true();
113+
});
114+
});
115+
116+
describe('WASM parseTransaction types', function () {
117+
it('should parse Transfer type', function () {
118+
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedSendTransaction.tx, 'base64'));
119+
const parsed = parseTransaction(tx);
120+
parsed.type.should.equal('Transfer');
121+
});
122+
123+
it('should parse TokenTransfer type', function () {
124+
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedTokenSendTransaction.tx, 'base64'));
125+
const parsed = parseTransaction(tx);
126+
parsed.type.should.equal('TokenTransfer');
127+
});
128+
129+
it('should parse SingleNominatorWithdraw type', function () {
130+
const tx = WasmTonTransaction.fromBytes(
131+
Buffer.from(testData.signedSingleNominatorWithdrawTransaction.tx, 'base64')
132+
);
133+
const parsed = parseTransaction(tx);
134+
parsed.type.should.equal('SingleNominatorWithdraw');
135+
});
136+
137+
it('should parse WhalesDeposit type', function () {
138+
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedTonWhalesDepositTransaction.tx, 'base64'));
139+
const parsed = parseTransaction(tx);
140+
parsed.type.should.equal('WhalesDeposit');
141+
});
142+
143+
it('should parse WhalesWithdraw type', function () {
144+
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedTonWhalesWithdrawalTransaction.tx, 'base64'));
145+
const parsed = parseTransaction(tx);
146+
parsed.type.should.equal('WhalesWithdraw');
147+
});
148+
});
149+
});

0 commit comments

Comments
 (0)