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
25 changes: 21 additions & 4 deletions modules/sdk-coin-ton/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,26 @@
"name": "@bitgo/sdk-coin-ton",
"version": "4.0.4",
"description": "BitGo SDK coin library for Ton",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"main": "./dist/cjs/src/index.js",
"module": "./dist/esm/index.js",
"browser": "./dist/esm/index.js",
"types": "./dist/cjs/src/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/src/index.d.ts",
"default": "./dist/cjs/src/index.js"
}
}
},
"scripts": {
"build": "yarn tsc --build --incremental --verbose .",
"build": "npm run build:cjs && npm run build:esm",
"build:cjs": "yarn tsc --build --incremental --verbose .",
"build:esm": "yarn tsc --project tsconfig.esm.json",
"fmt": "prettier --write .",
"check-fmt": "prettier --check '**/*.{ts,js,json}'",
"clean": "rm -r ./dist",
Expand Down Expand Up @@ -56,6 +72,7 @@
},
"gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c",
"files": [
"dist"
"dist/cjs",
"dist/esm"
]
}
53 changes: 47 additions & 6 deletions modules/sdk-coin-ton/src/lib/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,29 @@
* This is BitGo-specific business logic that lives outside the wasm package.
*/

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

/**
* @bitgo/wasm-ton is loaded lazily (not via a static top-level import) so this
* module doesn't drag the eagerly-registered `Ton`/`Tton` coin classes into
* the same async-WASM-initialization chain in bundlers, which breaks
* synchronous coin registration in the browser.
*
* This uses `require()`, not dynamic `import()`: Node's `import()` always
* resolves the package's "import" condition (the ESM build), and that build
* loads its .wasm binary via a raw ESM import, which throws
* ERR_UNKNOWN_FILE_EXTENSION on Node 20 without an experimental flag. The CJS
* build (resolved by `require()`) instantiates the same .wasm file
* synchronously via `fs.readFileSync`, which works on every supported Node
* version. In the browser, webpack's `@bitgo/sdk-coin-ton`/`@bitgo/wasm-ton`
* aliases rewrite the resolved path regardless of require/import syntax, so
* this still routes to the ESM build there.
*/
async function loadWasmTon() {
return require('@bitgo/wasm-ton') as typeof import('@bitgo/wasm-ton');
}

export interface ExplainTonTransactionWasmOptions {
txBase64: string;
/** When false, use the original bounce-flag-respecting address format. Defaults to true (bounceable EQ...). */
Expand Down Expand Up @@ -63,7 +79,8 @@ function extractOutputs(
* then derives the transaction type, extracts outputs/inputs, and maps
* to BitGoJS TransactionExplanation format.
*/
export function explainTonTransaction(params: ExplainTonTransactionWasmOptions): TransactionExplanation {
export async function explainTonTransaction(params: ExplainTonTransactionWasmOptions): Promise<TransactionExplanation> {
const { Transaction: WasmTonTransaction, parseTransaction } = await loadWasmTon();
const toAddressBounceable = params.toAddressBounceable !== false;
const tx = WasmTonTransaction.fromBytes(Buffer.from(params.txBase64, 'base64'));
const parsed: WasmParsedTransaction = parseTransaction(tx);
Expand All @@ -81,3 +98,27 @@ export function explainTonTransaction(params: ExplainTonTransactionWasmOptions):
withdrawAmount,
};
}

/**
* Get the signable payload (SHA-256 hash of the sign body cell) for a serialized TON transaction.
*
* Isolated behind this module (rather than imported directly in ton.ts) so the
* eagerly-registered `Ton`/`Tton` coin classes don't carry @bitgo/wasm-ton's
* async WASM initialization into their own module's top-level import graph.
*/
export async function getSignablePayloadWasm(serializedTx: string): Promise<Buffer> {
const { Transaction: WasmTonTransaction } = await loadWasmTon();
const tx = WasmTonTransaction.fromBytes(Buffer.from(serializedTx, 'base64'));
return Buffer.from(tx.signablePayload());
}

/**
* Convert an already memoId-stripped TON address to its bounceable (EQ...) form.
*
* See {@link getSignablePayloadWasm} for why this lives here rather than in ton.ts.
*/
export async function toBounceableAddressWasm(strippedAddress: string): Promise<string> {
const { decode: wasmDecode, encode: wasmEncode } = await loadWasmTon();
const decoded = wasmDecode(strippedAddress);
return wasmEncode(decoded.workchainId, decoded.addressHash, true);
}
35 changes: 17 additions & 18 deletions modules/sdk-coin-ton/src/ton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ 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, decode as wasmDecode, encode as wasmEncode } from '@bitgo/wasm-ton';
import { KeyPair as TonKeyPair } from './lib/keyPair';
import { TransactionBuilderFactory, Utils, TransferBuilder, TokenTransferBuilder, TransactionBuilder } from './lib';
import { explainTonTransaction } from './lib/explainTransactionWasm';
import { explainTonTransaction, getSignablePayloadWasm, toBounceableAddressWasm } from './lib/explainTransactionWasm';
import { getFeeEstimate } from './lib/utils';

export interface TonParseTransactionOptions extends ParseTransactionOptions {
Expand Down Expand Up @@ -120,21 +119,22 @@ export class Ton extends BaseCoin {
}

if (this.getChain() === 'tton') {
const toBounceable = (address: string) => {
const decoded = wasmDecode(this.getAddressDetails(address).address);
return wasmEncode(decoded.workchainId, decoded.addressHash, true);
};
const toBounceable = (address: string) => toBounceableAddressWasm(this.getAddressDetails(address).address);
const txBase64 = Buffer.from(rawTx, 'hex').toString('base64');
const explainedTx = explainTonTransaction({ txBase64 });
const explainedTx = await explainTonTransaction({ txBase64 });
if (txParams.recipients !== undefined) {
const filteredRecipients = txParams.recipients.map((recipient) => ({
address: toBounceable(recipient.address),
amount: BigInt(recipient.amount),
}));
const filteredOutputs = explainedTx.outputs.map((output) => ({
address: toBounceable(output.address),
amount: BigInt(output.amount),
}));
const filteredRecipients = await Promise.all(
txParams.recipients.map(async (recipient) => ({
address: await toBounceable(recipient.address),
amount: BigInt(recipient.amount),
}))
);
const filteredOutputs = await Promise.all(
explainedTx.outputs.map(async (output) => ({
address: await toBounceable(output.address),
amount: BigInt(output.amount),
}))
);
if (!_.isEqual(filteredOutputs, filteredRecipients)) {
throw new Error('Tx outputs does not match with expected txParams recipients');
}
Expand Down Expand Up @@ -268,8 +268,7 @@ export class Ton extends BaseCoin {
/** @inheritDoc */
async getSignablePayload(serializedTx: string): Promise<Buffer> {
if (this.getChain() === 'tton') {
const tx = WasmTonTransaction.fromBytes(Buffer.from(serializedTx, 'base64'));
return Buffer.from(tx.signablePayload());
return getSignablePayloadWasm(serializedTx);
}
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
const rebuiltTransaction = await factory.from(serializedTx).build();
Expand All @@ -281,7 +280,7 @@ export class Ton extends BaseCoin {
if (this.getChain() === 'tton') {
try {
const txBase64 = Buffer.from(params.txHex, 'hex').toString('base64');
return explainTonTransaction({ txBase64, toAddressBounceable: params.toAddressBounceable });
return await explainTonTransaction({ txBase64, toAddressBounceable: params.toAddressBounceable });
} catch {
throw new Error('Invalid transaction');
}
Expand Down
26 changes: 13 additions & 13 deletions modules/sdk-coin-ton/test/unit/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import * as testData from '../resources/ton';

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

explained.outputs.length.should.be.greaterThan(0);
explained.outputs[0].amount.should.equal(testData.signedSendTransaction.recipient.amount);
Expand All @@ -17,43 +17,43 @@ describe('TON WASM explainTransaction', function () {
should.exist(explained.id);
});

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

explained.outputs.length.should.be.greaterThan(0);
should.exist(explained.id);
});

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

should.exist(explained.id);
explained.id.should.equal(testData.signedSingleNominatorWithdrawTransaction.txId);
should.exist(explained.withdrawAmount);
explained.withdrawAmount!.should.equal('932178112330000');
});

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

should.exist(explained.id);
should.exist(explained.withdrawAmount);
});

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

should.exist(explained.id);
});

it('should respect toAddressBounceable=false', function () {
it('should respect toAddressBounceable=false', async function () {
const txBase64 = testData.signedSendTransaction.tx;
const bounceable = explainTonTransaction({ txBase64, toAddressBounceable: true });
const nonBounceable = explainTonTransaction({ txBase64, toAddressBounceable: false });
const bounceable = await explainTonTransaction({ txBase64, toAddressBounceable: true });
const nonBounceable = await explainTonTransaction({ txBase64, toAddressBounceable: false });

bounceable.outputs[0].address.should.equal(testData.signedSendTransaction.recipient.address);
nonBounceable.outputs[0].address.should.equal(testData.signedSendTransaction.recipientBounceable.address);
Expand Down
17 changes: 17 additions & 0 deletions modules/sdk-coin-ton/tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/esm",
"rootDir": "./src",
"module": "ES2020",
"target": "ES2020",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"declaration": true,
"declarationMap": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "test", "dist"],
"references": []
}
4 changes: 3 additions & 1 deletion modules/sdk-coin-ton/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"outDir": "./dist/cjs",
"rootDir": "./",
"strictPropertyInitialization": false,
"esModuleInterop": true,
"moduleResolution": "node16",
"module": "node16",
"typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"]
},
"include": ["src/**/*", "test/**/*"],
Expand Down
1 change: 1 addition & 0 deletions webpack/bitgojs.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
'@bitgo/wasm-mps/web': path.resolve('../../node_modules/@bitgo/wasm-mps/dist/web/js/wasm/wasm_mps.js'),
'@bitgo/wasm-mps': path.resolve('../../node_modules/@bitgo/wasm-mps/dist/esm/js/wasm/wasm_mps.js'),
'@bitgo/utxo-ord': path.resolve('../utxo-ord/dist/esm/index.js'),
'@bitgo/sdk-coin-ton': path.resolve('../sdk-coin-ton/dist/esm/index.js'),
},
fallback: {
constants: false,
Expand Down
Loading