fix(sdk-coin-ton): add dual ESM/CJS build for browser WASM support - #9346
Conversation
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
b9fd3cf to
5dd9d3f
Compare
|
Claude finished @Venkat-Annavazzala's task —— View job PR Analysis and ReviewComprehensive analysis of this PR that fixes TON browser WASM support by implementing dual ESM/CJS builds. Technical Review Summary✅ Excellent Technical Solution This PR demonstrates a sophisticated understanding of modern JavaScript module systems and provides an elegant solution to a complex browser/Node.js compatibility issue. Key Strengths:1. Dual Build Architecture (
2. Brilliant WASM Loading Strategy (
3. Webpack Integration (
4. Code Quality
Problem Analysis:The root cause analysis is spot-on:
Solution Assessment:The fix addresses both immediate and systemic issues:
Edge Cases Handled:
Minor Observations:
Overall Assessment: LGTM ✅ This is a high-quality fix that demonstrates deep understanding of the JavaScript ecosystem's complexity. The solution is both elegant and robust, addressing the immediate browser issue while preventing similar future problems. Job Run: Link | Branch: prabhsharansingh540/BTC-3216-ton-browser-wasm-fix |
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