Skip to content

feat(dapp): Tron injection + functional MetaMask masking#46

Merged
BitHighlander merged 5 commits intodevelopfrom
feat/tron-dapp-injection
Apr 24, 2026
Merged

feat(dapp): Tron injection + functional MetaMask masking#46
BitHighlander merged 5 commits intodevelopfrom
feat/tron-dapp-injection

Conversation

@BitHighlander
Copy link
Copy Markdown
Collaborator

Summary

Two related dApp-injection improvements shipped together. Both concern how KeepKey presents itself to in-page Web3 provider detection.

1. Tron dApp support

Mount `window.tronLink` + `window.tronWeb` shims that mirror TronLink's API surface. dApps connect via their standard "Connect TronLink" button; our shim intercepts (`isTronLink: true`), opens the KeepKey approval side panel, signs via the vault, and returns the signed tx for the dApp to broadcast.

Provider surface (new chrome-extension/src/injected/tron-provider.ts):

  • `tronWeb.trx.{sign, sendRawTransaction, getBalance, getAccount, ...}`
  • `tronWeb.transactionBuilder.{sendTrx, triggerSmartContract}`
  • `tronWeb.utils.{isAddress, toSun, fromSun, toHex}`
  • Direct TronGrid for reads/builds/broadcast, extension pipeline for signing only

Background handler (tronHandler.ts extended):

  • tron_requestAccounts, tron_sign, tron_signMessage (stub — vault endpoint pending)
  • Decoder handles `TransferContract` (native TRX), `TriggerSmartContract` `transfer(address,uint256)` (TRC20/USDT), and passes any other TriggerSmartContract through for firmware to validate

2. MetaMask masking pipeline

The Settings → Enable MetaMask Masking toggle was cosmetic — `isMetaMask` and `window.ethereum` mounts were hardcoded on. Now it actually works end-to-end, default off.

  • Content script reads `'masking-settings'` from `chrome.storage` before injection, stamps `data-masking="{...}"` on the `<script>` tag
  • Injected script parses it at startup and branches on each flag:
    • `isMetaMask` reflects `enableMetaMaskMasking`
    • `window.ethereum` / `window.xfi` mounts gated on their respective flags (otherwise EIP-6963 only)
  • When MetaMask masking is ON, also announce via EIP-6963 with `rdns: 'io.metamask'` so SDKs that key off the canonical MetaMask rdns (MetaMask SDK, Dynamic.xyz's MetaMaskConnector, RainbowKit) find us. Same approach Rabby uses.
  • Single diagnostic log per page load: `[KeepKey] masking: metamask=on xfi=off keplr=off`
  • Removed dead, wrong-key-reading `getMaskingSettings` handler from `background/index.ts`

Known ceiling: MetaMask SDK v0.28+ does anti-impersonation probes beyond EIP-6963 that we can't realistically beat without becoming a bug-for-bug clone. Works on simpler legacy sites (plain `isMetaMask` check); SDK-hardened dApps (Stripe via Dynamic.xyz, Coinbase Onramp) need WalletConnect as the proper fix — separate, larger undertaking.

Test plan

Extension-level sanity (automated via bundle simulation during development):

  • Build clean
  • `window.tronLink` / `window.tronWeb` mount with expected surface
  • Both EIP-6963 announces fire (KeepKey + MetaMask) when masking is ON
  • Only KeepKey announces when masking is OFF

Requires on-device verification before release:

  • SunSwap connect via "TronLink" button → address populates in dApp
  • Native TRX send via a Tron donate page / tronscan send UI — device shows correct recipient + amount
  • TRC20 (USDT) send via tronscan — device shows decoded recipient + token amount
  • SunSwap swap (TriggerSmartContract fallback) — signs or rejects cleanly
  • Masking OFF default: `window.ethereum` undefined on a fresh profile; modern dApps find us via EIP-6963 as KeepKey
  • Masking ON: `window.ethereum.isMetaMask === true`; Stripe-style legacy sites connect
  • Masking toggle requires page refresh (documented in UI)

🤖 Generated with Claude Code

BitHighlander and others added 5 commits April 24, 2026 16:24
Two related dApp-injection improvements shipped together:

Tron dApp support
-----------------
Mount window.tronLink + window.tronWeb shims that mirror TronLink's API
surface. dApps connect via the standard "Connect TronLink" button; our
shim intercepts (isTronLink: true), opens the KeepKey approval side
panel, signs via the vault, and returns the signed tx for the dApp to
broadcast.

- New injected provider in chrome-extension/src/injected/tron-provider.ts
  * tronWeb.trx.{sign, sendRawTransaction, getBalance, getAccount}
  * tronWeb.transactionBuilder.{sendTrx, triggerSmartContract}
  * tronWeb.utils.{isAddress, toSun, fromSun, toHex}
  * Direct TronGrid for reads/builds/broadcast, extension pipeline for signing
- Background handler extended with tron_requestAccounts, tron_sign,
  tron_signMessage (throws until vault adds the endpoint)
- tron_sign decoder handles TransferContract (native TRX), TriggerSmartContract
  with transfer(address,uint256) (TRC20/USDT), and passes any other
  TriggerSmartContract through for firmware to validate
- Verified on SunSwap: detection + connect + address populate. Signing
  paths need on-device verification per firmware capability.

MetaMask masking pipeline
-------------------------
The Settings → Enable MetaMask Masking toggle was cosmetic — isMetaMask
and window.ethereum mounts were hardcoded on. Made it actually work
end-to-end, default off.

- Content script reads 'masking-settings' from chrome.storage before
  injection, stamps data-masking="{...}" on the <script> tag
- Injected script parses dataset.masking at startup and branches:
  * isMetaMask reflects enableMetaMaskMasking
  * window.ethereum mount gated on the flag (otherwise EIP-6963 only)
  * window.xfi mount gated on enableXfiMasking
- When MetaMask masking is ON, also announce via EIP-6963 with
  rdns: 'io.metamask' so SDKs that key off the canonical MetaMask rdns
  (MetaMask SDK, Dynamic.xyz's MetaMaskConnector, RainbowKit) find us.
  Same trick Rabby uses.
- Known ceiling: MetaMask SDK v0.28+ does anti-impersonation probes
  beyond EIP-6963 that we can't realistically beat. Works on simpler
  legacy sites; SDK-hardened dApps need WalletConnect (future work).
- Removed dead, wrong-key-reading getMaskingSettings handler from
  background/index.ts.
- Single diagnostic log per page load: "[KeepKey] masking: metamask=on/off ..."

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
decodeTronTx was holding the decoded TRC-20 amount as BigInt, then
casting to Number before stashing it on DecodedTronTx.sunAmount and
passing it to signTronViaRest + the approval event. Any 18-decimal
token (most non-stablecoin TRC-20s) exceeds Number.MAX_SAFE_INTEGER
in base units, so the value silently rounds before the vault sees it
— firmware displays the wrong amount on-device and the signed tx
spends the wrong amount on-chain.

Changes:
- DecodedTronTx.sunAmount (number) → amountRaw (decimal string).
  Renamed to signal "this is base units not native sun".
- TransferContract decode emits String(v.amount); TRC-20 decode emits
  BigInt.toString(); contract-call emits String(callValue). All paths
  preserve precision.
- signTronViaRest signature widened to `string | number | bigint`.
  Internal stringification uses bigint.toString() when applicable so
  nothing re-enters the Number casting path.
- Approval event now carries amountRaw (not sun) so downstream UIs
  that display the raw-unit hint get the untruncated value.

Native TRX amounts never overflow Number (max 90B TRX in sun = 9e16,
fits in 2^53 comfortably), but the change makes that path uniform
and the code easier to audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Previous pass changed side-panel Send to emit type='transfer' +
unsignedTx.payment, but the dApp sign path (tron_sign) was still
emitting type='trc20-transfer' / 'contract-call' and had no payment
block. The shared /approval/other renderer only cases 'transfer' and
reads unsignedTx.payment.*, so dApp TRC-20 + contract-call approvals
surfaced as "Unknown Method" / N/A.

Unify on the same contract:
- type='transfer' for all Tron paths, always.
- decoded.kind now travels on unsignedTx.kind for downstream branches
  that actually care about native vs TRC-20 vs generic contract-call
  (storage/history, txid explorer link selection, etc).
- Populate unsignedTx.payment = { destination, amount, decimals, symbol }
  so RequestDetailsCard renders without a second asset-context
  round-trip.
- amount stays as a raw base-units decimal string (preserved from the
  previous amountRaw fix).
- decimals defaults:
    trx-transfer     → 6 (sun)
    contract-call    → 6 (call_value is TRX attached to the call)
    trc20-transfer   → 0, so the UI shows the raw integer we decoded.
                       We don't have the token's decimals from the
                       raw_data alone and the asset context isn't the
                       dApp's caller — showing a raw integer is less
                       misleading than applying the wrong divisor.
                       Future work: decodeTronTx can call the contract's
                       decimals() view or look up from assetData.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Review follow-up: contract-call events left payment.symbol undefined,
which previously leaned on the UI's asset-context fallback for the
"TRX sent" row. Combined with the UI now gating that fallback on
ctx.caip === event.caip (#48), a dApp-initiated contract call whose
caip doesn't match the side-panel's selected asset would render the
call_value row with no symbol at all.

Two tightenings in one:
- contract-call & trx-transfer both emit symbol='TRX' explicitly —
  the call_value on any TriggerSmartContract is always native TRX,
  so stating it in the handler is accurate and removes the UI's
  need to guess.
- trc20-transfer stays symbol=undefined — we don't know the token's
  symbol without an on-chain symbol() call or an assetData lookup.
  The UI's caip-match gate on asset context will decline to show a
  wrong symbol when the dApp caip differs from whatever the side
  panel had selected.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Review follow-up to the caip-match gate in #48: dApp TRC-20 events
were still emitting caip: TRON_CAIP (tron:27Lqcw/slip44:195 — the
native-TRX caip). The UI's ctx.caip === event.caip guard matched
when the user had TRX selected in the side panel, leaking the TRX
symbol/icon onto a USDT approval — exactly the failure the gate
was supposed to block.

Emit a token-scoped caip for trc20-transfer:

  tron:27Lqcw/token:${decoded.contractAddress}

Now the fallback only fires when the side-panel happened to have
this exact token selected. Any other selection (TRX, a different
TRC-20, or an unrelated chain) leaves symbol/icon empty, which
is the correct behavior — the handler still populates
payment.decimals=0 so amounts render as raw base units.

trx-transfer keeps TRON_CAIP (legitimate native TRX match desired).
contract-call keeps TRON_CAIP too — its renderer owns its own
labels (Contract:, Function:, "TRX sent:") with a hardcoded 'TRX'
fallback on the call_value row, so the caip match affects only
the avatar icon. Not worth the extra complexity.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@BitHighlander BitHighlander force-pushed the feat/tron-dapp-injection branch from 9963092 to 38a2629 Compare April 24, 2026 21:24
@BitHighlander BitHighlander merged commit 79eecf4 into develop Apr 24, 2026
3 of 4 checks passed
@BitHighlander BitHighlander deleted the feat/tron-dapp-injection branch April 24, 2026 21:24
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.

1 participant