feat(dapp): Tron injection + functional MetaMask masking#46
Merged
BitHighlander merged 5 commits intodevelopfrom Apr 24, 2026
Merged
feat(dapp): Tron injection + functional MetaMask masking#46BitHighlander merged 5 commits intodevelopfrom
BitHighlander merged 5 commits intodevelopfrom
Conversation
This was referenced Apr 24, 2026
80995fd to
2c69b54
Compare
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]>
9963092 to
38a2629
Compare
6 tasks
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.
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):Background handler (
tronHandler.tsextended):tron_requestAccounts,tron_sign,tron_signMessage(stub — vault endpoint pending)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.
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):
Requires on-device verification before release:
🤖 Generated with Claude Code