# A crash-course in Provider Authorization Negotiations If you're reading this, you're probably trying to get "fancy" with your "Connect Wallet" flow in one way or another. Here are a few use-cases supported by our powerful, but dauntingly flexible new dapp-wallet negotiation: 1. Dapp wants to check for support of a chain-specific method ([skip ahead](#Use-case-1-chain-specific-extension-to-a-namespace) to see the code snippets) 2. Dapp wants to know, with as few clicks and user-interactions as possible, which exact signing methods a wallet supports ([skip ahead](#Use-Case-2-Find-out-which-signing-methods-a-wallet-supports)) 3. Dapp wants to take advantage of L2-specific functions, without awkward failure modes if the wallet can't or the user won't approve it to ([skip ahead](#Use-case-1-chain-specific-extension-to-a-namespace) to see the code snippets) 4. Dashboard dapp wants to know which kinds of blockchain to query, by which chains the wallet supports. ([skip ahead](#Use-Case-2-Find-out-which-signing-methods-a-wallet-supports)) But first, let's look at how CAIP-25 messages are structured: ## The Basics: Syntax and Semantics The JSON-RPC call is named ["provider_authorization"][^1] and contains, at the top level: - `requiredNamespaces` object [MANDATORY][^2] - `optionalNamespaces` object [OPTIONAL] - `sessionProperties` object [OPTIONAL] #### sessionProperties The `sessionProperties` object is pretty self-explanatory: the persistent wallet-dapp session uses it to synchronise on shared values that wallet and dapp have to persist, which should look familiar if you've done traditional web development where the browser itself custodies these shared values in a session object that both can access. Here, instead, WalletConnect relays that object back and forth, and any update to this virtual "session object", and any change to the authorizations, require updates over new CAIP-25 calls. To put it another way, whether session properties change, they need to be verbosely communicated in CAIP-25 update requests about authorizations, and vice versa. To date, very few of these properties are being used in WalletConnect, so none are defined yet as standards. One useful exception is the `expiry` property in `sessionProperties`, which we recommend setting initially to test if the wallet supports it-- this UNIX timestamp is when the dapp can expect the wallet to drop the session and require a new wallet connection. A helpful wallet will keep bumping this forward into the future upon each successive CAIP-25 call. A wallet that does not expire sessions will drop the property in each response, which a careful dapp may take as a trigger to drop the session on its side if it has security concerns about sessions staying open. #### Namespaces, required or optional The first two objects contain one or more scope-keyed objects, each representing a rich authorization that applies to the scope expressed in a string as the object's key. For example, an object that applies to all EVM chains would be keyed to `eip155` ([EIP-155][] defines the chainID system), while an object listing additional methods for just Ethereum main-net would be keyed to `eip155:1`. Each of these scoped objects, called "namespaces"[^3], contain the following: - `chains` array [OPTIONAL - only needed if scope is not chain-specific] - "chainIDs" as strings (see [CAIP-2][] for chainID syntax and [namespaces][] for valid values outside of ethereum chainID system) - `methods` array [REQUIRED - even if empty] - RPC method names as strings - `events`[^4] array [REQUIRED - even if empty] - RPC notification names (no response) as strings Note: These key strings need to be unique *within* `requiredNamespaces` or `optionalNamespaces`. If you would like to request some optional chains/methods/events as well as the ones you need, the same key can appear in *both* (and very often does, as you'll see below). ### Response Parsing The wallet goes through the following steps to authorize a connection and initiate a session: 1. Confirm that it can authorize all the required chains/methods/events for each scope in the `requiredNamespaces`. This object can be renamed `sessionNamespaces`[^5], as this will be the main payload returned in a succesful response. 2. Parse the rest to see that it is well-formed. (Return error code if not). 3. Decide how many of the chains, methods, and events to add to `sessionNamespaces`. Think of this as a "selective merge", folding in additional properties one-by-one into the same place in the (scope-keyed) heirarchy. Any scope object that doesn't appear in `sessionNamespaces` should be added as a new object in `sessionNamespaces` rather than modifying existing ones. 4. Set an `expiry` if desired (best practice=yes, expire all sessions!), which can be sooner or later than what is requested by the dapp. 5. Accept, change, or drop any other `sessionProperties` according to policy. Drop any that the wallet does not understand. 6. Assign a sufficiently random[^7] UUID, Hex integer, etc. and use it to track the `sessionNamespaces` and `sessionProperties` as they evolve with successive calls. 7. Save a copy of the authorization `sessionNamespaces` to check against later, in whatever local storage/memory the wallet uses to manage sessions. 8. Return the `sessionIdentifier`, `sessionNamespaces`, and `sessionProperties` (if non-empty). Note: Any chain added to a `chains` array in step 3 will authorize on that chain ALL the methods and events in that object, so be careful with those![^6] ### Session Model Note that all successive calls to update the `sessionNamespaces` or `sessionProperties`, or even just to keep the session alive and push forward `sessionProperties.expiry`, will carry the same `sessionIdentifier`. This is like a "topic" or channel kept alive between wallet and dapp, which makes sure they share a session and are both updating it as the wallet adds or subtracts authorizations. Metamask, Brave, and other in-browser wallets are exploring a CAIP-25 approach that would use the browser's native "session" object for storing all this data, thus the name; WalletConnect stores this same session information in the live "connection" that it routes via topics. ## Use case 1: chain-specific extension to a namespace A simple example will show how namespace objects with the same "key" are merged in responses "additively". request: ``` { "requiredScopes": { "eip155": { "chains": ["1", "6"], "methods": ["personal_sign", "get_balance"], "events": ["accountsChanged"] } }, "optionalScopes": { "eip155": { "chains": ["10"], "methods": ["eth_signTypedData_v4"], "events": [] } } } ``` response: ``` { "sessionIdentifier": "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb", "sessionScopes": { "eip155": { "chains": ["1", "6", "10"], "methods": ["personal_sign", "get_balance", "eth_signTypedData_v4"], "events": ["accountsChanged"] } } } ``` There are now **3 chains with 3 methods authorized** on each (3x3 = 9 authorizations), **not** 2 chains with 2 methods and 1 chain with 1 method (2x2+1x1 = 5 authorizations). The exchange above is a great example of how **NOT** to request a chain-specific method, because if any of those methods were chain- specific, some of those 9 combinations would be invalid and open to the door to error cases. ### Best Practice: All Chain-specific requests in a Chain-scoped namespace So far so good-- but what if you had a FOURTH chain that implemented its own special methods unique to that L2, not available on the first three? What if your dapp wants to know whether a wallet supports RPC methods specific to one or two Layer-2 chains? Because chains and methods are additive, as we saw above, we want to avoid wallets sending L2-specific methods to the wrong L2, or worse, to mainnet! The best way to avoid this is a **separate scope object**. So, adding to the example above: request: ``` { "requiredScopes": { "eip155": { "chains": ["1", "6"], "methods": ["personal_sign", "get_balance"], "events": ["accountsChanged"] } }, "optionalScopes": { "eip155": { "chains": ["10"], "methods": ["eth_signTypedData_v4"], "events": [] }, "eip155:280": { //ZKSync testnet "chains": [], "methods": ["zks_getBlockDetails"], "events": [] } } } ``` response #1 (if the wallet supports the requested chain-specific methods): ``` { "sessionIdentifier": "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb", "sessionScopes": { "eip155": { "chains": ["1", "6", "10"], "methods": ["personal_sign", "get_balance", "eth_signTypedData_v4"], "events": ["accountsChanged"] }, "eip155:280": { //ZKSync testnet "chains": [], "methods": ["zks_getBlockDetails"], "events": [] } } } ``` response #2 (if it isn't, or the user hits "no" on a modal asking for permission): ``` { "sessionIdentifier": "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb", "sessionScopes": { "eip155": { "chains": ["1", "6", "10"], "methods": ["personal_sign", "get_balance", "eth_signTypedData_v4"], "events": ["accountsChanged"] } } } ``` ## Use Case 2: Find out which signing methods a wallet supports Wallet signing, particularly off-chain signing, is less standardized than many developers outside of the dApp world [realize](https://docs.metamask.io/guide/signing-data.html#a-brief-history)-- there are many variations of ethereum signing, "compatibility-mode" signing that non-EVM systems use to create ethereum-ish signatures with other key types, etc etc. Imagine you have an off-chain application, which just needs to enable "Sign in with X" (aka [CAIP-122][] Authentication) for any wallet that has something close enough to `personal_sign` to produce a valid proof-of-control signature to produce an [authentication receipt][CAIP-74]. Ideally, you'd like to know this the minute the end-user clicks "connect wallet", so that you can present them right away with an off-chain transaction to sign, tailored to their wallet's signing capabilities. Here's how CAIP-25 enables this: request: ``` { "requiredScopes": { "eip155": { "chains": ["1"], "methods": ["eth_accounts"], "events": ["accountsChanged"] } }, "optionalScopes": { "eip155": { "chains": [], "methods": [ "personal_sign", "eth_sign", "eth_signTypedData", "eth_signTypedData_v2", "eth_signTypedData_v3", "eth_signTypedData_v4", ], "events": [] }, "hedera": { "methods": ["hedera_signTransaction"] }, "solana": { "methods": ["getSignaturesForAddress"] } //...etc etc } } ``` A response with basically ANY of those methods authorized tells the dapp exactly what message to present their user on first sign-in-- and a response with none at least tells the dapp how to route that user to instructions on what kind of wallet to try connecting with next! ## Use Case 3: Omnichain Dapp ## Advanced Topics: UX & Security Best Practices Some high-level guidance: 1. Don't ask for everything up front-- **progressive consent is better UX** 2. Parse responses carefully: `sessionProperties.expiry` might change, for example ## Advanced Topics: ## Advanced Topics: Custom RPC methods and dictionaries ## References [CAIP-2]: https://chainagnostic.org/CAIPs/CAIP-2 [CAIP-74]: https://chainagnostic.org/CAIPs/caip-74 [CAIP-122]: https://chainagnostic.org/CAIPs/caip-122 [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 [namespaces]: https://namespaces.chainagnostic.org/ [^1]: May be deprecated in favor of `provider_authorize` in a future major version. [^2]: May be deprecated in favor of `requiredScopes` and `optionalScopes` in a future major version [^3]: May be deprecated in favor of "scope objects" in a future major version [^4]: May be deprecated in favor of `notifications` in a future major version (technically what every other programming contexts calls "events" are "notifications" in JSON-RPC, since they don't await responses) [^5]: May be deprecated in favor of `sessionScopes` in a future major version [^6]: Corner case: authorizations are recombinatory and additive. See examples in Use Case #1. [^7]: See #5 in the [requirements section](https://chainagnostic.org/CAIPs/caip-171#definition) of CAIP-171, which defines the `sessionIdentifier` property.