Skip to content

AppProposeService unconditionally empties requiredNamespaces — dApps cannot request a guaranteed chain grant (no opt-out) #347

Description

@carsonfarmer

Describe the bug

Since 1.6.7 (#156), AppProposeService.propose routes every proposal through NamespaceMerger.mergeRequiredIntoOptional and always sends requiredNamespaces: [:] on the wire, regardless of what the dApp passes via SessionParams.requiredNamespaces or the (deprecated) SignClient.connect(requiredNamespaces:) overload. There is no public opt-out.

For dApps that transact exclusively on a specific chain, required namespaces are the only mechanism that guarantees the settled session includes an account on that chain. With all-optional proposals, wallets that do not have the chain installed legally drop it and settle without it — MetaMask being the common case (WalletConnect/WalletConnectSwiftV2#1257, MetaMask/metamask-mobile#6703). The dApp then cannot send any request pinned to that chain (rejected with invalidPermissions), cannot expand the settled session, and is forced into either a disconnect/reconnect loop or a permanently depinned request envelope (routing chain-management and signing requests over a different approved chain).

Our iOS app transacts exclusively on Arbitrum One (production) and Arbitrum Sepolia (demo/testnet), proposing the environment's chain as required and the remaining supported chains as optional. On 1.6.7+ the required chain is silently optional, so MetaMask sessions routinely settle without the only chain the app can use.

For what it's worth, the CAIP-25 implementation guidance this change implements retains defined requiredNamespaces as a supported pattern (wallet handling cases #2 and #3 in the post), and frames all-optional as a recommendation for apps ("provide the most flexibility they can afford"), not a prohibition. The equivalent JS-side change (WalletConnect/walletconnect-monorepo#6667) is likewise a silent remap.

SDK Version

To Reproduce

AppKit.set(sessionParams: SessionParams(
    requiredNamespaces: [
        "eip155": ProposalNamespace(
            chains: [Blockchain("eip155:421614")!],
            methods: ["eth_signTypedData_v4", "wallet_switchEthereumChain", "wallet_addEthereumChain"],
            events: ["chainChanged", "accountsChanged"]
        )
    ]
))
// then present the modal and connect to a wallet that does NOT have chain 421614 installed
  1. Physical device with MetaMask; Arbitrum Sepolia (421614) not added.
  2. Connect and approve.
  3. Observed: the session settles without any eip155:421614 account — MetaMask's approval UI only offered networks it already had installed, so the app's only transacting chain is missing from the settled namespaces. (Source inspection confirms why: the wc_sessionPropose payload carries requiredNamespaces: {} with everything merged into optionalNamespaces.)
  4. Expected: the required namespace passes through (pre-1.6.7 behavior), so the wallet either grants/adds the chain or rejects at connect time.

Expected behavior

Keep the current merge as the default for compatibility, but provide an opt-in that passes requiredNamespaces through to SessionProposal unchanged (restoring the pre-1.6.7 code path) — e.g. a SessionParams flag or a non-merging connect variant. We would be happy to contribute a PR if maintainers are open to the shape.

Screenshots

n/a — wire-level behavior; happy to attach a wc_sessionPropose payload capture if useful.

Device (please complete the following information):

  • Device: iPhone (physical)
  • OS: iOS
  • Version: reown-swift 2.3.0, MetaMask Mobile (current)

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions