Skip to content

fix(sdk-coin-ton): add dual ESM/CJS build for browser WASM support - #9346

Merged
0xPrabh merged 1 commit into
masterfrom
prabhsharansingh540/BTC-3216-ton-browser-wasm-fix
Jul 30, 2026
Merged

fix(sdk-coin-ton): add dual ESM/CJS build for browser WASM support#9346
0xPrabh merged 1 commit into
masterfrom
prabhsharansingh540/BTC-3216-ton-browser-wasm-fix

Conversation

@0xPrabh

@0xPrabh 0xPrabh commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Withdrawing TON (tton) from a wallet in the staging/testnet web UI fails client-side with:

TypeError: Cannot read properties of undefined (reading 'fromBytes')
    at explainTonTransaction
    at Tton.verifyTransaction
    at Wallet.prebuildAndSignTransaction
    at async Wallet.sendManyTxRequests

sdk-coin-ton imports @bitgo/wasm-ton (a wasm-pack "bundler" target package whose .wasm binary loads via an async ESM import) but only ever 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 — plus an explicit webpack alias (webpack doesn't use the browser/module package.json fields by default when resolving from CJS code).

@bitgo/sdk-coin-ton (introduced in 0387a236be, ticket BTC-3216) never got this treatment, so bitgo-ui's browser bundle resolved it via CJS, leaving @bitgo/wasm-ton's Transaction export 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's coinFactory, and without breaking any supported Node version.

Fix

  • package.json: add module/browser/exports fields and build:cjs/build:esm scripts, mirroring abstract-utxo/utxo-core/utxo-staging/utxo-ord.
  • tsconfig.json: output CJS to dist/cjs instead of dist.
  • tsconfig.esm.json (new): ES2020/bundler-resolution build to dist/esm.
  • webpack/bitgojs.config.js: add a @bitgo/sdk-coin-ton alias pointing at its ESM build, mirroring the existing @bitgo/utxo-ord alias.
  • ton.ts / lib/explainTransactionWasm.ts: load @bitgo/wasm-ton lazily via require() inside the async methods that need it (getSignablePayload, verifyTransaction, explainTransaction), instead of a static top-level import in ton.ts.

Two problems surfaced iterating on this, both caught by CI rather than by me locally first:

  1. Aliasing sdk-coin-ton's own package to ESM broke bitgo's eager coin registration. All ~150 coins are registered synchronously at module load in coinFactory.ts. Once sdk-coin-ton resolved via its ESM build, its module joined the same async-WASM chain as @bitgo/wasm-ton itself, so Ton/Tton came back undefinedexport * from './ton' (and the package barrel) couldn't finish resolving before the WASM init settled. Caught by bitgo's own browser-test (karma) CI job. Fixed by moving the WASM touch out of ton.ts's top-level imports into an isolated helper module, loaded lazily only when the async methods that need it are actually called.
  2. Dynamic import() broke Node 20 unit tests. Node's import() always resolves a package's "import" export condition (the ESM build), and that build's raw ESM import of the .wasm binary throws ERR_UNKNOWN_FILE_EXTENSION on Node 20 without an experimental flag (it worked fine locally on Node 24, masking this). Fixed by using require() instead — it 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 correctly routes to the ESM build there.

Testing

  • modules/sdk-coin-ton unit tests: 133 passing, verified under both the local default Node and Node 20.20.1 directly (matching CI's unit-test (20.x) job that initially failed).
  • bitgo's browser-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-ton as its own separate async chunk rather than blocking the main bundle or eager coin registration.
  • Follow-up filed for the underlying error-handling gap: Tton.verifyTransaction's WASM branch had no try/catch fallback, unlike the sibling explainTransaction/tton branch (tracked in the same ticket, lower severity, not addressed in this PR to keep scope tight).

Ticket: BTC-3216

@0xPrabh
0xPrabh force-pushed the prabhsharansingh540/BTC-3216-ton-browser-wasm-fix branch from 6a55dee to b9fd3cf Compare July 24, 2026 19:34
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
0xPrabh force-pushed the prabhsharansingh540/BTC-3216-ton-browser-wasm-fix branch from b9fd3cf to 5dd9d3f Compare July 24, 2026 19:53
@0xPrabh
0xPrabh marked this pull request as ready for review July 30, 2026 06:10
@0xPrabh
0xPrabh requested review from a team as code owners July 30, 2026 06:10
@0xPrabh
0xPrabh requested a review from Charan-Ratlan July 30, 2026 06:10
@Venkat-Annavazzala

Copy link
Copy Markdown
Contributor

@claude

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Venkat-Annavazzala's task —— View job


PR Analysis and Review

Comprehensive 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 (package.json, tsconfig.*)

  • Perfect implementation following established patterns from other WASM packages
  • Proper conditional exports with separate type declarations for ESM/CJS
  • Clean separation: CJS to dist/cjs, ESM to dist/esm

2. Brilliant WASM Loading Strategy (explainTransactionWasm.ts:14-30)

  • The lazy loading approach is exceptionally well-engineered
  • Using require() instead of dynamic import() elegantly solves Node 20 compatibility
  • Excellent documentation explaining the rationale and trade-offs
  • Prevents blocking eager coin registration - critical for browser performance

3. Webpack Integration (bitgojs.config.js:28)

  • Consistent with existing WASM package patterns
  • Proper ESM alias ensures browser bundles get the right build

4. Code Quality

  • Test file updates show no behavioral changes, just import adjustments
  • Clean refactoring with WASM calls moved from top-level imports to async methods
  • Maintains full backward compatibility

Problem Analysis:

The root cause analysis is spot-on:

  • Browser webpack resolved sdk-coin-ton via CJS build
  • @bitgo/wasm-ton exports were undefined due to async WASM initialization
  • This broke verifyTransaction with TypeError: Cannot read properties of undefined (reading 'fromBytes')

Solution Assessment:

The fix addresses both immediate and systemic issues:

  • Immediate: Dual build enables proper ESM resolution in browsers
  • Systemic: Lazy loading prevents future WASM-related registration issues
  • Robust: Works across Node 20+ and all browser environments

Edge Cases Handled:

  • ✅ Node 20 ERR_UNKNOWN_FILE_EXTENSION via require() strategy
  • ✅ Browser webpack alias resolution regardless of require/import syntax
  • ✅ Eager coin registration preserved via lazy WASM loading
  • ✅ Maintains server-side behavior unchanged

Minor Observations:

  • The require() vs import() strategy is well-documented and justified
  • Test coverage appears comprehensive for the refactored functionality
  • Commit message provides excellent technical context

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

@0xPrabh
0xPrabh merged commit 66a83ea into master Jul 30, 2026
25 checks passed
@linear-code

linear-code Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

COINS-1288

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants