WalletConnect, owned since 2023

Cake LabsSenior Mobile Engineer2023 to now15 merged PRs

dApp connectivity across EVM and Solana, from the first Ethereum build in 2023 to the Reown migration and the signing-flow redesign.

Context

Cake Wallet is an open-source, non-custodial wallet. In 2023 it held Monero, Bitcoin and Ethereum wallets, and none of them could talk to a dApp. WalletConnect is the protocol a dApp uses to ask a wallet to sign something.

I built it into the app in October 2023 and I have owned it since. Every merged change to it in the public repo is mine: 15 pull requests over 35 months, about 10,100 lines added and 4,000 removed, across 386 file touches. The links at the end go to each one.

Problem

A dApp connection is a private-key path. Every request that arrives has to reach the right person, for the right wallet, with enough detail to decide, and then be signed with a key the app never holds longer than the signature takes.

And it has to keep working while everything around it moves. The SDK, the app’s own dashboard, the list of chains, and the dApps themselves, which do not all send the same shapes.

Constraints

Non-custodial. Keys are read per request inside the handler and never cached. The first version got this wrong in a small way, the key service held one wallet’s keys, and the fix in December 2023 deleted more code than it added.

Several wallets in one app. A person can have five wallets on five chains. Each must see only its own connections, and a request meant for one must never be signed by another.

Phone and desktop. Desktop uses a separate navigator per route, so a listener mounted on the dashboard was disposed when the route popped. On desktop it lives on the settings route for that reason.

Open source. Everything below is in the public repo, review threads included.

Decisions and trade-offs

Approval is a queued bottom sheet, not a route (October 2023). The chain service awaits the user’s decision as a plain future, and a listener on the dashboard shows the sheet. That shape survived every rewrite since.

Connections per wallet, without a migration (April 2024). Only a list of pairing topics is stored per wallet, keyed by the wallet’s own public key, and the SDK stays the source of truth. Existing wallets started empty and repaired themselves on the next connection. The cost came later, twice. Disconnecting had to learn to remove its topic too (May 2026). And an index alone does not prove ownership, so in August 2026 I added a check against the session’s own accounts and made that one check drive the list, the events and the request handling together.

Migrate and restructure in one change (April 2025). The SDK was deprecated under the feature. I could have ported like for like and tidied afterwards. I did both at once: 85 files, new folders, method enums, extracted decoders, per-chain construction. One very large diff and an iOS 13 floor, in exchange for the structure that every later change fits inside.

Fees: take the higher of the dApp’s and the wallet’s (April 2026). A dApp quotes fees when it builds the transaction. By signing time the base fee has moved. Trust the dApp and the transaction stalls. Use the wallet’s own quote and you silently change what the person was shown. So I compare the two and use the larger of each field, clamp the priority fee under max fee minus base fee, and fall back to the network price if the quote fails. The wallet never lowers what a dApp asked for, and never signs something its own estimate says is underpriced.

Show what you do not recognise (June 2026). The permissions list on the connection sheet groups known methods into plain lines: approve transactions, sign messages, switch chains, view balance. Anything it does not know is listed by name. Less tidy than hiding it. It never presents an unknown request as a familiar one.

Match the request by topic and method, not “most recent” (August 2026). Reading the last pending request is simple and works until two dApps are connected. Filtering by session topic and method means a handler can find nothing, which needed an explicit no-op and explicit error responses. It is the only version that is correct.

What was hard

Getting a private-key path through review. The first PR ran 46 days and 22 review events. The asks were structural, and they were right: inject the key service instead of reaching for it, keep the project id out of source, never leave an error path silent.

Real dApps disagree with each other. Nearly every change in 2025 and 2026 fixes something only visible against a live dApp. Responses sent twice. Typed data in a shape the Permit2 reader could not read. dApp-supplied gas being overwritten. A Solana mainnet chain id that older dApps still send and newer ones do not, so I accept both.

State that outlives a wallet switch. A cached key list, a static SDK handle, session lists not cleared, setup fired twice by two quick switches. The same bug shape four times over three years, each fix narrower than the last. The last one serialises wallet switches on a single chained future and tears the relay connection down with a timeout.

Result

From an EVM or Solana wallet, on phone or desktop, you connect to a dApp by scanning a QR code, pasting a URI or tapping a wc: link, and each wallet sees only its own connections.

A request arrives as a sheet that names the dApp, shows which wallet and address will sign, says in plain language what is being asked, flags a dApp the SDK marks as a scam, and shows the total network fee in the chain’s own currency with a fiat estimate. Confirming is a swipe, not a tap. A request meant for a different wallet is refused with a message.

Expired connections disappear from the list, disconnecting cleans up after itself, and a fee that has gone stale is refreshed before signing.

Two more pieces are open as I write this, not shipped: decoding contract calldata so the sheet can say what a transaction does, with about 2,800 lines of tests behind it, and a third namespace, Tron.

All case studies