Skip to content

Support WalletConnect Bitcoin proof of ownership - #6193

Open
j0ntz wants to merge 5 commits into
developfrom
jon/wc-bitcoin-proof-of-ownership
Open

Support WalletConnect Bitcoin proof of ownership#6193
j0ntz wants to merge 5 commits into
developfrom
jon/wc-bitcoin-proof-of-ownership

Conversation

@j0ntz

@j0ntz j0ntz commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

Bitcoin was never registered for WalletConnect in Edge, so a bip122 session
proposal (Bringin's Proof of Ownership flow) matched no plugin. Because Bringin
lists its chains as OPTIONAL namespaces, the existing "no wallets meet dapp
requirements" guard never fired, and WcConnectionsScene handed the wallet
picker an empty allowedAssets array. That filtered out every wallet the user
owns while leaving the create-wallet rows visible, which is why the reporter saw
Edge steer them into Create Wallet instead of listing their BTC wallets.

What this branch does:

  • Registers bitcoin with walletConnectV2ChainId bip122:000000000019d6689c085ae165831e93,
    and widens WalletConnectChainId.namespace to accept bip122.
  • Advertises the bip122 methods Edge can actually serve, signMessage and
    getAccountAddresses, plus the bip122_addressesChanged event. sendTransfer
    and signPsbt are deliberately absent: the UTXO plugin has no WalletConnect
    payload parser, so advertising them would accept spend requests Edge cannot
    fulfill, and a dapp requiring them is now rejected cleanly instead.
  • Routes bip122 session_requests to their own handler. signMessage opens a
    new WcSignMessageModal (dApp, wallet, address, message, Sign / Reject) and
    signs with signatureFormat: 'bip137', the encoding SegWit verifiers such as
    Bringin require; getAccountAddresses returns the address the session already
    disclosed. Signing always uses the address the session advertised, since that
    is the one the dapp verifies against.
  • Throws NO_WALLETS_DAPP_REQUIREMENTS when the proposal matches no wallet at
    all, so an unsupported dapp gets a toast rather than a create-only picker.
  • Keeps the UTXO copy on the split-wallet scene, which previously inferred "UTXO"
    from the absence of a walletConnectV2ChainId.

Review-round changes on top of that:

  • The session advertises the wallet's native SegWit address where the chain offers
    one, matching the receive address Edge shows the user, rather than the wrapped or
    legacy address getReceiveAddress returns. A wallet that reports no address is
    skipped instead of throwing, which would have taken down session listing for every
    other wallet.
  • The wallet that approved a session is remembered by topic in account.dataStore,
    so an incoming request still resolves after a UTXO receive address rotates. The
    address on the session stays the fallback.
  • A request Edge cannot serve is rejected rather than dropped: an unknown wallet, an
    unsupported method, an account the session does not hold, or a payload the cleaners
    reject all send a WalletConnect rejection instead of leaving the dapp waiting.
  • The signature modal renders the full message and address instead of ellipsizing at
    three lines, since the message is what the user is authorizing, and it ignores a
    dismissal while a signature is in flight so the request cannot be answered twice.

No dependency change is needed: BIP137 message signing already shipped in
edge-currency-plugins v3.12.0 (#454), and signMessage is a core
EdgeCurrencyWallet method.

Asana: https://app.asana.com/0/1215088146871429/1216274035296616

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

@j0ntz

j0ntz commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence

wallet picker

wallet picker

connect scene

connect scene

sign modal

sign modal

signature sent

signature sent

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz force-pushed the jon/wc-bitcoin-proof-of-ownership branch from b735b50 to 9662a95 Compare September 3, 2026 23:13
@j0ntz
j0ntz marked this pull request as ready for review September 3, 2026 23:13
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Security verification (icon URI / debug logging)

{
  "is_real_finding": false,
  "severity": "none",
  "false_positive_risk": "low",
  "exploitability": "None at medium+. Icon URI is loaded client-side via FastImage for display only; same pre-existing WC pattern as WcSmartContractModal. No SSRF, no RN XSS via javascript:, no secrets logged.",
  "affected_lines": "WalletConnectService.tsx:80-83 (new bip122 path); mirrors existing :140-143 → WcSmartContractModal; WcSignMessageModal EdgeCard icon={dAppIcon}; useWalletConnect console.log typing-only",
  "rationale": "PR adds bip122 signMessage UI and reuses the established peer.metadata.icons → SVG fallback → EdgeCard/FastImage path already used for smart-contract WC requests. No new console logging of signatures, keys, seeds, or WC URIs; no TLS bypass or CSP changes. A dapp-controlled icon URL can at most show a misleading logo (pre-existing WC trust model), which is not a medium+ exploit in React Native FastImage."
}
Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread src/components/modals/WcSignMessageModal.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

WalletConnect bip122 binding review (A–F)

Read-only verification of address/message binding for Bitcoin WC PoO. No MEDIUM+ exploitable findings in the six hypothesized issues.

A) Optional address ignored → always session accounts[0]

Exploitable: no · Discard (compat gap, not a bind break)

asBip122SignMessageParams only cleans message + optional account. Signing and the modal both use session.namespaces.bip122.accounts[0]split(':')[2]. The optional spec address (and protocol) are ignored, so Edge cannot be steered onto a change/ordinals/other key. Shown address = signed address = response address. Worst case is incomplete spec support, not UI/signature mismatch.

B) requestedAccount !== publicAddress CAIP-10 vs raw

Exploitable: no · Discard (fail-closed)

publicAddress is the raw CAIP-10 account suffix. A full CAIP-10 account fails inequality and is rejected. Omitted account proceeds with the session address (still bound). No fail-open wrong bind.

C) getWalletIdFromSessionNamespace + multi-namespace

Exploitable: no · Discard

approveSessiongetSupportedNamespaces returns one namespace for the selected wallet only, so Edge-created sessions are single-namespace. First-match wallet binding is fine for bip122-only sessions. Hypothetical multi-namespace confusion is not reachable via this approve path.

D) getAccountAddresses auto-approve

Exploitable: no · Discard

Returns only { address: publicAddress } already advertised at session approval. No extra addresses/keys.

E) sendTransfer / signPsbt if only PoO methods advertised

Exploitable: no · Discard

Advertised methods are getAccountAddresses + signMessage only. Required unsupported methods throw at approve. Unknown bip122 methods hit defaultrejectRequest. bip122 is handled before any EVM/Cosmos payload parser.

F) chainId reference (mainnet genesis vs other bip122)

Exploitable: no · Informational / Low at most — discard as MEDIUM+

Request routing checks namespace bip122 only, not genesis reference vs session chains. Only bitcoin registers bip122 (mainnet genesis). Signature still uses the session mainnet account; wrong-reference requests do not change the key or enable spends. Defense-in-depth chain equality would be nice, not a MEDIUM exploit.


Summary: Binding is session-account-centric and fail-closed on account mismatch. Optional address/protocol incompleteness and missing chain-reference check are non-exploitable relative to PoO goals.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread src/hooks/useWalletConnect.tsx
Comment thread src/hooks/useWalletConnect.tsx
Comment thread src/components/services/WalletConnectService.tsx
Comment thread src/components/modals/WcSignMessageModal.tsx Outdated
@j0ntz
j0ntz force-pushed the jon/wc-bitcoin-proof-of-ownership branch from 9662a95 to 584cbb8 Compare September 3, 2026 23:42
@j0ntz

j0ntz commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence (after review fixes)

wallet picker

wallet picker

connect scene

connect scene

sign modal

sign modal

signature sent

signature sent

Captured by the agent's in-app test run (build-and-test).

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Security verification (platform patterns)

Read-only check of CHANGED code in WalletConnectService.tsx, WcSignMessageModal.tsx, and useWalletConnect.tsx for TLS bypass, sensitive logging, unsafe HTML/icon sinks, and unauthenticated identity.

[
  {
    "exploitable": false,
    "category": "TLS validation bypass",
    "evidence": "No rejectUnauthorized, insecure SSL/TLS agents, or certificate-validation bypass in the changed WC files. Network setup remains Web3Wallet/Core init only."
  },
  {
    "exploitable": false,
    "category": "Sensitive debug/diagnostic logging",
    "evidence": "No new logs of WC messages, signatures, addresses, or credentials. Pre-existing WC error paths still use console.log(..., String(e)) / console.log(e) with type-only edits. The unrecognized-session console.log remains a static string (pre-existing). Message/signature stay in UI or approveRequest payloads only."
  },
  {
    "exploitable": false,
    "category": "CSP / Trusted Types / unsafe HTML sinks",
    "evidence": "React Native path: dAppIcon from session.peer.metadata.icons uses the same svg→fallback then EdgeCard/FastImage uri pattern as existing EVM WcSmartContractModal in WalletConnectService.tsx. No WebView HTML, dangerouslySetInnerHTML, or Trusted Types sinks introduced."
  },
  {
    "exploitable": false,
    "category": "Unauthenticated routes / identity from headers/JWT",
    "evidence": "No HTTP routes, header-based identity, or JWT handling in these files. Wallet binding uses WC session topic store / session namespace accounts only."
  }
]

Verdict: No NEW production security/privacy risks in these platform-pattern categories from this PR.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Security verification: remembered walletId vs session address

Verdict: discard — not a medium+ exploitable issue.

Checked resolveSessionWalletId / lookupSessionWallet / rememberSessionWallet / getWalletIdFromSessionNamespace / handleBip122Request and the UTXO signMessage path.

  1. Missing GUI ownership check is real, but not exploitable for bad signatures. resolveSessionWalletId does return a remembered topic→walletId without comparing it to session.namespaces.*.accounts[0]. Signing still passes publicAddress from the session into wallet.signMessage.

  2. edge-currency-plugins refuses foreign addresses. UtxoEngine.signMessage resolves publicAddress → scriptPubkey, loads it from the wallet data layer, and throws AddressNotOwnedError when path is missing (or the string is not this chain’s address). A mismatched remembered wallet cannot produce a valid proof for the session address; the modal’s approve path rejects the WC request on that error.

  3. walletId reuse / cross-wallet mixup lacks a concrete path. Edge wallet ids are hmacSha256-derived from the wallet key material, not recycled counters. A deleted wallet makes currencyWallets[walletId] undefined and the request is rejected. A stale mapping to a different live wallet still hits AddressNotOwnedError unless that wallet actually owns the address (same keys).

  4. dataStore is not an external write surface. account.dataStore is the unlocked account’s encrypted storage-wallet disklet. Writing it already implies account access, which already implies the ability to sign with owned wallets. A WC dapp cannot plant topic→walletId entries.

No medium+ security/privacy finding filed from this hypothesis. Defense-in-depth (verify remembered wallet still matches the session CAIP-10 account before opening the modal) would be optional hardening only, not a required fix for exploitability.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Security verification: WcSignMessageModal message truncation (re-check)

Read-only check of current code (not prior review state).

{
  "still_truncates": false,
  "severity_if_true": null,
  "evidence": "Message EdgeRow now uses maximumHeight=\"large\" → EdgeRow textHeights.large=0 → EdgeText gets numberOfLines={0}; {...rest} keeps 0 (overrides newline bump). RN treats 0 as unlimited, so no ellipsis of unseen suffix/prefix; ScrollView can show full payload before approve.",
  "confidence": 0.92
}

Path: WcSignMessageModal message row → EdgeRow (large→0) → EdgeText (numberOfLines + {...rest}) → RN Text unlimited lines. Prior MEDIUM finding (default medium/3-line ellipsis) is addressed.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Verification: message body truncation via maximumHeight="large"

Verdict: not truncated. A user cannot approve a WalletConnect signMessage while an ellipsized/unseen suffix is hidden by EdgeRow/EdgeText/Text line limiting.

{
  "verified_truncation": false,
  "reason": "EdgeRow maximumHeight=\"large\" sets numberOfLines=0. EdgeText's {...rest} after the explicit numberOfLines prop overwrites any newline-bump back to 0, so RN <Text> receives numberOfLines={0}. Per RN docs and Android native (ReactBaseTextShadowNode / ReactTextView), 0 unsets the line cap (unlimited). ellipsizeMode=\"tail\" is inert without a positive line limit; Android updateView even nulls TruncateAt when unlimited. The modal ScrollView can reveal the full wrapped body.",
  "actual_props_to_Text": {
    "numberOfLines": 0,
    "ellipsizeMode": "tail",
    "adjustsFontSizeToFit": true,
    "allowFontScaling": false,
    "minimumFontScale": 0.65
  },
  "exploit_with_newlines": false,
  "exploit_with_long_single_line": false
}

Trace (concrete)

  1. WcSignMessageModal message row: maximumHeight="large" + body={message}
  2. EdgeRow: textHeights.large = 0 → passes numberOfLines={0} and ellipsizeMode="tail" to EdgeText
  3. EdgeText: default is 1, then optional bump, but {...rest} is spread after numberOfLines={numberOfLines}, and rest still contains the original numberOfLines: 0 → final value on <Text> is 0 (bump never sticks when caller passed 0)
  4. RN 0.79 semantics for numberOfLines={0}: unlimited (docs: “unsetting this value… no lines restriction”). Android: 0 → ReactConstants.UNSET / ViewDefaults.NUMBER_OF_LINES; updateView sets setEllipsize(null) when unlimited

Prior MEDIUM finding (default medium → 3-line ellipsis) is addressed by the current maximumHeight="large" on the message row.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread src/components/modals/WcSignMessageModal.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 584cbb8. Configure here.

Comment thread src/hooks/useWalletConnect.tsx
@j0ntz
j0ntz force-pushed the jon/wc-bitcoin-proof-of-ownership branch from 584cbb8 to 0ca67bd Compare September 4, 2026 00:14
@j0ntz

j0ntz commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence (final)

wallet picker

wallet picker

connect scene

connect scene

sign modal

sign modal

signature sent

signature sent

Captured by the agent's in-app test run (build-and-test).

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