fix(sdk-coin-ton): add dual ESM/CJS build for browser WASM support#9346
Draft
0xPrabh wants to merge 1 commit into
Draft
fix(sdk-coin-ton): add dual ESM/CJS build for browser WASM support#93460xPrabh wants to merge 1 commit into
0xPrabh wants to merge 1 commit into
Conversation
0xPrabh
force-pushed
the
prabhsharansingh540/BTC-3216-ton-browser-wasm-fix
branch
from
July 24, 2026 19:34
6a55dee to
b9fd3cf
Compare
sdk-coin-ton imports @bitgo/wasm-ton (a wasm-pack "bundler" target package whose .wasm binary loads via an async ESM import) but only shipped a single CJS build. Per docs/esm.md, any module importing a wasm-* package needs a dual ESM/CJS build so the browser webpack bundle resolves the ESM entry point, where async wasm instantiation is properly synchronized with the module graph. It also needs an explicit webpack alias, same as the other wasm-* packages and utxo-ord, since webpack doesn't use the browser/module fields by default when resolving from CJS code. Without this, bitgo-ui's browser bundle resolved sdk-coin-ton via its CJS build, leaving @bitgo/wasm-ton's Transaction export undefined at call time and breaking every browser-based TON send with: TypeError: Cannot read properties of undefined (reading 'fromBytes') at explainTonTransaction at Tton.verifyTransaction Mirrors the existing dual-build setup in abstract-utxo/utxo-core/ utxo-staging/utxo-ord, plus utxo-ord's webpack alias pattern. Aliasing sdk-coin-ton's own package to its ESM build put its module in the same async-WASM chain as @bitgo/wasm-ton itself, which broke eager coin registration in bitgo's coinFactory (all coins are registered synchronously at module load): Ton/Tton came back undefined because `export * from './ton'` (and its barrel) couldn't finish resolving before @bitgo/wasm-ton's async init settled. Fixed by loading @bitgo/wasm-ton lazily inside the async methods that need it (getSignablePayload, verifyTransaction, explainTransaction) instead of a static top-level import in ton.ts, so the coin classes stay synchronously registrable. That lazy load uses require(), not dynamic import(): Node's import() always resolves the package's "import" condition (the ESM build), and that build's raw ESM .wasm import throws ERR_UNKNOWN_FILE_EXTENSION on Node 20 without an experimental flag. require() resolves the CJS build, which instantiates the same .wasm file synchronously via fs.readFileSync and works on every supported Node version. In the browser, webpack's alias rewrites the resolved path regardless of require/import syntax, so this still routes to the ESM build there. Verified via bitgo's own browser-test (karma) suite (reproduces and confirms the fix for this exact class of bug) and by running sdk-coin-ton's unit tests under Node 20 directly, matching CI. Ticket: BTC-3216
0xPrabh
force-pushed
the
prabhsharansingh540/BTC-3216-ton-browser-wasm-fix
branch
from
July 24, 2026 19:53
b9fd3cf to
5dd9d3f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Withdrawing TON (
tton) from a wallet in the staging/testnet web UI fails client-side with:sdk-coin-tonimports@bitgo/wasm-ton(a wasm-pack "bundler" target package whose.wasmbinary loads via an async ESM import) but only ever shipped a single CJS build. Perdocs/esm.md, any module importing awasm-*package needs a dual ESM/CJS build so the browser webpack bundle resolves the ESM entry point — where async wasm instantiation is properly synchronized with the module graph — plus an explicit webpack alias (webpack doesn't use thebrowser/modulepackage.json fields by default when resolving from CJS code).@bitgo/sdk-coin-ton(introduced in0387a236be, ticket BTC-3216) never got this treatment, so bitgo-ui's browser bundle resolved it via CJS, leaving@bitgo/wasm-ton'sTransactionexport undefined at call time and breaking every browser-based TON send. Server-side (Node/Express) signing is unaffected since Node resolves the CJS build fine.Goal
TON withdrawals work again in the browser, without changing server-side behavior, without breaking eager coin registration in
bitgo'scoinFactory, and without breaking any supported Node version.Fix
package.json: addmodule/browser/exportsfields andbuild:cjs/build:esmscripts, mirroringabstract-utxo/utxo-core/utxo-staging/utxo-ord.tsconfig.json: output CJS todist/cjsinstead ofdist.tsconfig.esm.json(new): ES2020/bundler-resolution build todist/esm.webpack/bitgojs.config.js: add a@bitgo/sdk-coin-tonalias pointing at its ESM build, mirroring the existing@bitgo/utxo-ordalias.ton.ts/lib/explainTransactionWasm.ts: load@bitgo/wasm-tonlazily viarequire()inside the async methods that need it (getSignablePayload,verifyTransaction,explainTransaction), instead of a static top-level import inton.ts.Two problems surfaced iterating on this, both caught by CI rather than by me locally first:
sdk-coin-ton's own package to ESM brokebitgo's eager coin registration. All ~150 coins are registered synchronously at module load incoinFactory.ts. Oncesdk-coin-tonresolved via its ESM build, its module joined the same async-WASM chain as@bitgo/wasm-tonitself, soTon/Ttoncame backundefined—export * from './ton'(and the package barrel) couldn't finish resolving before the WASM init settled. Caught bybitgo's ownbrowser-test(karma) CI job. Fixed by moving the WASM touch out ofton.ts's top-level imports into an isolated helper module, loaded lazily only when the async methods that need it are actually called.import()broke Node 20 unit tests. Node'simport()always resolves a package's"import"export condition (the ESM build), and that build's raw ESM import of the.wasmbinary throwsERR_UNKNOWN_FILE_EXTENSIONon Node 20 without an experimental flag (it worked fine locally on Node 24, masking this). Fixed by usingrequire()instead — it resolves the CJS build, which instantiates the same.wasmfile synchronously viafs.readFileSyncand works on every supported Node version. In the browser, webpack's alias rewrites the resolved path regardless of require/import syntax, so this still correctly routes to the ESM build there.Testing
modules/sdk-coin-tonunit tests: 133 passing, verified under both the local default Node and Node 20.20.1 directly (matching CI'sunit-test (20.x)job that initially failed).bitgo'sbrowser-test(karma, headless Chrome, builds the real webpack browser bundle): reproduced the original CI failure locally at each iteration, verified the final fix resolves it — webpack emits@bitgo/wasm-tonas its own separate async chunk rather than blocking the main bundle or eager coin registration.Tton.verifyTransaction's WASM branch had no try/catch fallback, unlike the siblingexplainTransaction/tton branch (tracked in the same ticket, lower severity, not addressed in this PR to keep scope tight).Ticket: BTC-3216