Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/sdk-coin-ton/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
},
"dependencies": {
"@bitgo/sdk-core": "^36.35.0",
"@bitgo/wasm-ton": "*",
"@bitgo/sdk-lib-mpc": "^10.9.0",
"@bitgo/statics": "^58.31.0",
"bignumber.js": "^9.0.0",
Expand Down
138 changes: 138 additions & 0 deletions modules/sdk-coin-ton/src/lib/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* WASM-based TON transaction explanation.
*
* Built on @bitgo/wasm-ton's parseTransaction(). Derives transaction types,
* extracts outputs/inputs, and maps to BitGoJS TransactionExplanation format.
* This is BitGo-specific business logic that lives outside the wasm package.
*/

import { Transaction as WasmTonTransaction, parseTransaction } from '@bitgo/wasm-ton';
import type { TonTransactionType } from '@bitgo/wasm-ton';
import type { ParsedTransaction as WasmParsedTransaction } from '@bitgo/wasm-ton';
import { TransactionType } from '@bitgo/sdk-core';
import { TransactionExplanation } from './iface';

export interface ExplainTonTransactionWasmOptions {
txBase64: string;
}

// =============================================================================
// Transaction type mapping
// =============================================================================

function mapTransactionType(wasmType: TonTransactionType): TransactionType {
switch (wasmType) {
case 'Transfer':
return TransactionType.Send;
case 'TokenTransfer':
return TransactionType.SendToken;
case 'WhalesDeposit':
return TransactionType.TonWhalesDeposit;
case 'WhalesVestingDeposit':
return TransactionType.TonWhalesVestingDeposit;
case 'WhalesWithdraw':
return TransactionType.TonWhalesWithdrawal;
case 'WhalesVestingWithdraw':
return TransactionType.TonWhalesVestingWithdrawal;
case 'SingleNominatorWithdraw':
return TransactionType.SingleNominatorWithdraw;
case 'Unknown':
return TransactionType.Send;
default:
return TransactionType.Send;
}
}

// =============================================================================
// Output/input extraction
// =============================================================================

interface InternalOutput {
address: string;
amount: string;
}

interface InternalInput {
address: string;
value: string;
}

function extractOutputsAndInputs(parsed: WasmParsedTransaction): {
outputs: InternalOutput[];
inputs: InternalInput[];
outputAmount: string;
withdrawAmount: string | undefined;
} {
const outputs: InternalOutput[] = [];
const inputs: InternalInput[] = [];
let withdrawAmount: string | undefined;

if (parsed.recipient && parsed.amount !== undefined) {
const amountStr = String(parsed.amount);
outputs.push({ address: parsed.recipient, amount: amountStr });
inputs.push({ address: parsed.sender, value: amountStr });
}

if (parsed.withdrawAmount !== undefined) {
withdrawAmount = String(parsed.withdrawAmount);
}

const outputAmount = outputs.reduce((sum, o) => sum + BigInt(o.amount), 0n);

return {
outputs,
inputs,
outputAmount: String(outputAmount),
withdrawAmount,
};
}

// =============================================================================
// Main explain function
// =============================================================================

/**
* Standalone WASM-based transaction explanation for TON.
*
* Parses the transaction via `parseTransaction(tx)` from @bitgo/wasm-ton,
* then derives the transaction type, extracts outputs/inputs, and maps
* to BitGoJS TransactionExplanation format.
*/
export function explainTonTransaction(params: ExplainTonTransactionWasmOptions): TransactionExplanation & {
type: TransactionType;
sender: string;
memo?: string;
seqno: number;
expireTime: number;
isSigned: boolean;
} {
const tx = WasmTonTransaction.fromBytes(Buffer.from(params.txBase64, 'base64'));
const parsed: WasmParsedTransaction = parseTransaction(tx);

const type = mapTransactionType(parsed.type);
const id = tx.id;
const { outputs, inputs, outputAmount, withdrawAmount } = extractOutputsAndInputs(parsed);

// Convert bigint to string at serialization boundary
const resolvedOutputs = outputs.map((o) => ({
address: o.address,
amount: o.amount,
}));

return {
displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'],
id,
type,
outputs: resolvedOutputs,
outputAmount,
changeOutputs: [],
changeAmount: '0',
fee: { fee: 'UNKNOWN' },
withdrawAmount,
sender: parsed.sender,
memo: parsed.memo,
seqno: parsed.seqno,
expireTime: parsed.expireTime,
isSigned: parsed.isSigned,
};
}
1 change: 1 addition & 0 deletions modules/sdk-coin-ton/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export { TransferBuilder } from './transferBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { TonWhalesVestingDepositBuilder } from './tonWhalesVestingDepositBuilder';
export { TonWhalesVestingWithdrawBuilder } from './tonWhalesVestingWithdrawBuilder';
export { explainTonTransaction } from './explainTransactionWasm';
export { Interface, Utils };
3 changes: 2 additions & 1 deletion modules/sdk-coin-ton/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export class Utils implements BaseUtils {
wc: 0,
});
const address = await wallet.getAddress();
return address.toString(isUserFriendly, true, bounceable);
const legacyAddress = address.toString(isUserFriendly, true, bounceable);
return legacyAddress;
}

getAddress(address: string, bounceable = true): string {
Expand Down
18 changes: 18 additions & 0 deletions modules/sdk-coin-ton/src/ton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import {
} from '@bitgo/sdk-core';
import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
import { Transaction as WasmTonTransaction } from '@bitgo/wasm-ton';
import { KeyPair as TonKeyPair } from './lib/keyPair';
import { TransactionBuilderFactory, Utils, TransferBuilder, TokenTransferBuilder, TransactionBuilder } from './lib';
import { getFeeEstimate } from './lib/utils';
import { explainTonTransaction } from './lib/explainTransactionWasm';

export interface TonParseTransactionOptions extends ParseTransactionOptions {
txHex: string;
Expand Down Expand Up @@ -235,13 +237,29 @@ export class Ton extends BaseCoin {

/** @inheritDoc */
async getSignablePayload(serializedTx: string): Promise<Buffer> {
// WASM-based signable payload: Transaction.fromBytes -> signablePayload()
try {
const tx = WasmTonTransaction.fromBytes(Buffer.from(serializedTx, 'base64'));
return Buffer.from(tx.signablePayload());
} catch {
// Fallback to legacy path
}

const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
const rebuiltTransaction = await factory.from(serializedTx).build();
return rebuiltTransaction.signablePayload;
}

/** @inheritDoc */
async explainTransaction(params: Record<string, any>): Promise<TransactionExplanation> {
// WASM-based explain path: parse via @bitgo/wasm-ton
try {
const txBase64 = Buffer.from(params.txHex, 'hex').toString('base64');
return explainTonTransaction({ txBase64 });
} catch {
// Fallback to legacy path
}

try {
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
const transactionBuilder = factory.from(Buffer.from(params.txHex, 'hex').toString('base64'));
Expand Down
149 changes: 149 additions & 0 deletions modules/sdk-coin-ton/test/unit/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import assert from 'assert';
import should from 'should';
import { Transaction as WasmTonTransaction, parseTransaction } from '@bitgo/wasm-ton';
import { explainTonTransaction } from '../../src/lib/explainTransactionWasm';
import { TransactionType } from '@bitgo/sdk-core';
import * as testData from '../resources/ton';

describe('TON WASM explainTransaction', function () {
describe('explainTonTransaction', function () {
it('should explain a signed send transaction', function () {
const txBase64 = testData.signedSendTransaction.tx;
const explained = explainTonTransaction({ txBase64 });

explained.type.should.equal(TransactionType.Send);
explained.outputs.length.should.be.greaterThan(0);
explained.outputs[0].amount.should.equal(testData.signedSendTransaction.recipient.amount);
explained.changeOutputs.should.be.an.Array();
explained.changeAmount.should.equal('0');
should.exist(explained.id);
should.exist(explained.sender);
explained.isSigned.should.be.true();
});

it('should explain a signed token send transaction', function () {
const txBase64 = testData.signedTokenSendTransaction.tx;
const explained = explainTonTransaction({ txBase64 });

explained.type.should.equal(TransactionType.SendToken);
explained.outputs.length.should.be.greaterThan(0);
should.exist(explained.id);
should.exist(explained.sender);
});

it('should explain a single nominator withdraw transaction', function () {
const txBase64 = testData.signedSingleNominatorWithdrawTransaction.tx;
const explained = explainTonTransaction({ txBase64 });

explained.type.should.equal(TransactionType.SingleNominatorWithdraw);
should.exist(explained.id);
should.exist(explained.sender);
});

it('should explain a Ton Whales deposit transaction', function () {
const txBase64 = testData.signedTonWhalesDepositTransaction.tx;
const explained = explainTonTransaction({ txBase64 });

explained.type.should.equal(TransactionType.TonWhalesDeposit);
should.exist(explained.id);
should.exist(explained.sender);
});

it('should explain a Ton Whales withdrawal transaction', function () {
const txBase64 = testData.signedTonWhalesWithdrawalTransaction.tx;
const explained = explainTonTransaction({ txBase64 });

explained.type.should.equal(TransactionType.TonWhalesWithdrawal);
should.exist(explained.id);
should.exist(explained.sender);
should.exist(explained.withdrawAmount);
});

it('should explain a Ton Whales full withdrawal transaction', function () {
const txBase64 = testData.signedTonWhalesFullWithdrawalTransaction.tx;
const explained = explainTonTransaction({ txBase64 });

explained.type.should.equal(TransactionType.TonWhalesWithdrawal);
should.exist(explained.id);
should.exist(explained.sender);
});
});

describe('WASM Transaction signing flow', function () {
it('should produce correct signable payload from WASM Transaction', function () {
const txBase64 = testData.signedSendTransaction.tx;
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));
const signablePayload = tx.signablePayload();

signablePayload.should.be.instanceOf(Uint8Array);
signablePayload.length.should.equal(32);

// Compare against known signable from test fixtures
const expectedSignable = Buffer.from(testData.signedSendTransaction.signable, 'base64');
Buffer.from(signablePayload).toString('base64').should.equal(expectedSignable.toString('base64'));
});

it('should parse transaction and preserve bigint amounts', function () {
const txBase64 = testData.signedSendTransaction.tx;
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));
const parsed = parseTransaction(tx);

parsed.type.should.equal('Transfer');
should.exist(parsed.amount);
(typeof parsed.amount).should.equal('bigint');
parsed.seqno.should.be.a.Number();
parsed.expireTime.should.be.a.Number();
});

it('should get transaction id', function () {
const txBase64 = testData.signedSendTransaction.tx;
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));
const id = tx.id;

should.exist(id);
id.should.be.a.String();
id.length.should.be.greaterThan(0);
});

it('should report isSigned correctly', function () {
const txBase64 = testData.signedSendTransaction.tx;
const tx = WasmTonTransaction.fromBytes(Buffer.from(txBase64, 'base64'));

tx.isSigned.should.be.true();
});
});

describe('WASM parseTransaction types', function () {
it('should parse Transfer type', function () {
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedSendTransaction.tx, 'base64'));
const parsed = parseTransaction(tx);
parsed.type.should.equal('Transfer');
});

it('should parse TokenTransfer type', function () {
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedTokenSendTransaction.tx, 'base64'));
const parsed = parseTransaction(tx);
parsed.type.should.equal('TokenTransfer');
});

it('should parse SingleNominatorWithdraw type', function () {
const tx = WasmTonTransaction.fromBytes(
Buffer.from(testData.signedSingleNominatorWithdrawTransaction.tx, 'base64')
);
const parsed = parseTransaction(tx);
parsed.type.should.equal('SingleNominatorWithdraw');
});

it('should parse WhalesDeposit type', function () {
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedTonWhalesDepositTransaction.tx, 'base64'));
const parsed = parseTransaction(tx);
parsed.type.should.equal('WhalesDeposit');
});

it('should parse WhalesWithdraw type', function () {
const tx = WasmTonTransaction.fromBytes(Buffer.from(testData.signedTonWhalesWithdrawalTransaction.tx, 'base64'));
const parsed = parseTransaction(tx);
parsed.type.should.equal('WhalesWithdraw');
});
});
});
Loading