diff --git a/modules/sdk-coin-ton/package.json b/modules/sdk-coin-ton/package.json index 7fd6e9a5c8..15eb63e778 100644 --- a/modules/sdk-coin-ton/package.json +++ b/modules/sdk-coin-ton/package.json @@ -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", @@ -56,6 +72,7 @@ }, "gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c", "files": [ - "dist" + "dist/cjs", + "dist/esm" ] } diff --git a/modules/sdk-coin-ton/src/lib/explainTransactionWasm.ts b/modules/sdk-coin-ton/src/lib/explainTransactionWasm.ts index bf0b1a3934..bfaaeb5c1d 100644 --- a/modules/sdk-coin-ton/src/lib/explainTransactionWasm.ts +++ b/modules/sdk-coin-ton/src/lib/explainTransactionWasm.ts @@ -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...). */ @@ -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 { + 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); @@ -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 { + 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 { + const { decode: wasmDecode, encode: wasmEncode } = await loadWasmTon(); + const decoded = wasmDecode(strippedAddress); + return wasmEncode(decoded.workchainId, decoded.addressHash, true); +} diff --git a/modules/sdk-coin-ton/src/ton.ts b/modules/sdk-coin-ton/src/ton.ts index 792790f429..84c531cae3 100644 --- a/modules/sdk-coin-ton/src/ton.ts +++ b/modules/sdk-coin-ton/src/ton.ts @@ -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 { @@ -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'); } @@ -268,8 +268,7 @@ export class Ton extends BaseCoin { /** @inheritDoc */ async getSignablePayload(serializedTx: string): Promise { 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(); @@ -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'); } diff --git a/modules/sdk-coin-ton/test/unit/explainTransactionWasm.ts b/modules/sdk-coin-ton/test/unit/explainTransactionWasm.ts index 1a8ad9a83b..d6ee3145bc 100644 --- a/modules/sdk-coin-ton/test/unit/explainTransactionWasm.ts +++ b/modules/sdk-coin-ton/test/unit/explainTransactionWasm.ts @@ -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); @@ -17,17 +17,17 @@ 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); @@ -35,25 +35,25 @@ describe('TON WASM explainTransaction', function () { 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); diff --git a/modules/sdk-coin-ton/tsconfig.esm.json b/modules/sdk-coin-ton/tsconfig.esm.json new file mode 100644 index 0000000000..17f39ab0f3 --- /dev/null +++ b/modules/sdk-coin-ton/tsconfig.esm.json @@ -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": [] +} diff --git a/modules/sdk-coin-ton/tsconfig.json b/modules/sdk-coin-ton/tsconfig.json index aa8f5a7e66..69acc65fe1 100644 --- a/modules/sdk-coin-ton/tsconfig.json +++ b/modules/sdk-coin-ton/tsconfig.json @@ -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/**/*"], diff --git a/webpack/bitgojs.config.js b/webpack/bitgojs.config.js index b40af33464..72376f8591 100644 --- a/webpack/bitgojs.config.js +++ b/webpack/bitgojs.config.js @@ -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,