Skip to content

fix(express): use wallet multisigType instead of coin.supportsTss() for TRX consolidation routing#8811

Merged
bhuvanr159 merged 1 commit into
masterfrom
CHALO-473-fix
May 20, 2026
Merged

fix(express): use wallet multisigType instead of coin.supportsTss() for TRX consolidation routing#8811
bhuvanr159 merged 1 commit into
masterfrom
CHALO-473-fix

Conversation

@bhuvanr159
Copy link
Copy Markdown
Contributor

@bhuvanr159 bhuvanr159 commented May 20, 2026

On-chain TRX wallets with an external signer configured were failing consolidation with "TxRequestId required to sign TSS transactions with External Signer." because handleV2ConsolidateAccount used coin.supportsTss() to decide signing params. For TRX, supportsTss() is always true at the coin level regardless of wallet type, causing TSS MPC generators to be injected for on-chain wallets that have no txRequestId in their build response. Align with handleV2SendMany (fixed in BG-65857) by switching to wallet._wallet.multisigType === 'tss', which reflects the actual wallet configuration rather than coin capability.

TICKET: CHALO-473

Problem

Clients using BitGo Express with an external signer (externalSignerUrl) are unable to consolidate on-chain TRX wallets. Every consolidation attempt returns:

{"success":[],"failure":[{"message":"TxRequestId required to sign TSS transactions with External Signer.","name":"Error"}],"message":"All transactions failed"}

Root Cause

In handleV2ConsolidateAccount, the choice between TSS and on-chain signing params is gated on coin.supportsTss() — a coin-level capability flag:

  // Before                                                                                                                                                       
  if (coin.supportsTss()) {                                                                                                                                       
    result = await wallet.sendAccountConsolidations(createTSSSendParams(req, wallet));
  } else {                                                                                                                                                        
    result = await wallet.sendAccountConsolidations(createSendParams(req));                                                                                     
  }     

For TRX, supportsTss() unconditionally returns true because TRX supports both on-chain multisig and TSS wallets at the coin level. This causes every TRX
consolidation request — including plain on-chain wallets — to go through createTSSSendParams, which injects ECDSA MPC share generator functions into the signing
params.

When the microservices build endpoint is called for an on-chain TRX wallet, it correctly returns { txHex, consolidateId } with no txRequestId (since it's not a
TSS wallet). Later, signTransaction detects the injected ECDSA generators and routes to signTransactionTssExternalSignerECDSA, which immediately requires a
txRequestId — throws, and the consolidation fails.

The analogous handleV2SendMany handler was already fixed (BG-65857) to use wallet._wallet.multisigType === 'tss' — a wallet-instance level check that reflects
the actual wallet configuration. handleV2ConsolidateAccount was never updated to match.

Why This Did Not Surface Before

The bug has two hard prerequisites that must both be true simultaneously:

  1. The wallet must be an on-chain TRX wallet (multisigType: "onchain") — TSS TRX wallets are unaffected because their build response does include a txRequestId
  2. Express must have externalSignerUrl configured — without it, createTSSSendParams and createSendParams return plain req.body unchanged (the generator
    functions are only injected when an external signer URL is present), so the wrong routing is completely masked and signing falls through to passphrase-based
    signing successfully

Most TRX deployments historically used passphrase-based signing. The external signer pattern is an enterprise/custody setup where keys are never decrypted
inside Express. As more clients have migrated to external signer configurations while still operating on-chain TRX wallets — rather than migrating those wallets
to TSS — this code path started getting exercised in production and the latent bug became visible.

Additionally, TRX is unique among all consolidation-supporting coins in having supportsTss() === true at the coin level while defaulting new wallets to
multisigType: "onchain". Every other TSS-capable consolidation coin (SOL, NEAR, DOT, SUI, ETH, BSC, etc.) defaults new wallets to TSS, so their entire live
wallet population is TSS and coin.supportsTss() happened to be a valid proxy for them. TRX is the only coin where the two diverge.

Fix

Switch from the coin-level flag to the wallet-instance check, consistent with handleV2SendMany:

  // After                                                                                                                                                        
  if (wallet._wallet.multisigType === 'tss') {                                                                                                                  
    result = await wallet.sendAccountConsolidations(createTSSSendParams(req, wallet));
  } else {
    result = await wallet.sendAccountConsolidations(createSendParams(req));                                                                                       
  }

File changed: modules/express/src/clientRoutes.ts

This ensures:

  • On-chain TRX wallets → createSendParams → customSigningFunction → external signer receives POST /api/v2/ttrx/sign with txHex ✅
  • TSS TRX wallets → createTSSSendParams → ECDSA MPC generators → txRequestId present in build response ✅
  • All other consolidation coins → behaviour unchanged ✅

Unit tests added:

  • On-chain TRX wallet routes to createSendParams even though coin.supportsTss() is true
  • TSS TRX wallet routes to createTSSSendParams

References

  • sendMany fix for same pattern: BG-65857
  • Original consolidation bug: BG-62277
  • wallet.sendAccountConsolidation already uses this._wallet.multisigType === 'tss' correctly — the bug was only at the Express routing layer

@bhuvanr159 bhuvanr159 requested review from a team as code owners May 20, 2026 05:27
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 20, 2026

CHALO-473

bhavidhingra
bhavidhingra previously approved these changes May 20, 2026
bhavidhingra
bhavidhingra previously approved these changes May 20, 2026
Marzooqa
Marzooqa previously approved these changes May 20, 2026
Comment thread modules/express/src/clientRoutes.ts Outdated
…or TRX consolidation routing

On-chain TRX wallets with an external signer configured were failing
consolidation with "TxRequestId required to sign TSS transactions with
External Signer." because handleV2ConsolidateAccount used coin.supportsTss()
to decide signing params. For TRX, supportsTss() is always true at the coin
level regardless of wallet type, causing TSS MPC generators to be injected
for on-chain wallets that have no txRequestId in their build response.
Align with handleV2SendMany (fixed in BG-65857) by switching to
wallet._wallet.multisigType === 'tss', which reflects the actual wallet
configuration rather than coin capability.

TICKET: CHALO-473
@bhuvanr159 bhuvanr159 merged commit 9f85628 into master May 20, 2026
22 checks passed
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.

3 participants