Back to experience

Conflux · DApp Developer Infrastructure

use-wallet× dapp-omnibus

Separate wallet, chain, and UI ownership, then let every DApp assemble only what it needs

I built use-wallet for DApps that needed both Conflux Core and EVM wallets with a custom connection interface. It preserves protocol differences and exposes state and commands without binding them to UI. dapp-omnibus extends that approach to shared wallet selection, approvals, and transaction decoding. This case starts with my design decisions, then follows the capabilities and the project’s later evolution.

2022
year of the first use-wallet commit
2 families
Conflux Core and Ethereum RPC
3 models
injected, WalletConnect, and EIP-6963
5 packages
current dapp-omnibus release boundary

9 chapters · 2m 33s

01

Building it in 2022 was not about recreating a ConnectButton

use-wallet already had an implementation in early 2022: the old branch contains January wallet code and a February release record; the current-line initialization has a May 13 author date. I built it to support Conflux Core and EVM, stay lightweight, and leave connection UI to each DApp. For a versioned comparison, RainbowKit 0.1.0 imported base styles, configured wagmi and ethers, nested WagmiProvider and RainbowKitProvider, then rendered ConnectButton, with visibility props and theme variables for customization. This work also needed cfx_* methods, Base32 addresses, latest_state, and CIP-23, so use-wallet wrapped the injected provider and connection state without fixed UI. The current-line initial README recorded a target of roughly 3.7 KB gzip for source, or about 20 KB with decimal.js.

  • Not a rejection of wagmi or RainbowKit; the problem boundary was different
  • Chain differences remain explicit: Conflux Core is not another EVM chainId
  • UI ownership stays with the product: hooks and commands come before ConnectButton

02

The hard part of use-wallet is the lifecycle, not four hooks

A provider may not be injected yet, may be impersonated by another wallet, may time out, may return no account, or may resolve account and chain while the balance RPC stalls. Emitter contains those cases in five states: in-detecting, not-installed, not-active, in-activating, and active. Once detected it binds accountsChanged and chainChanged, while batchGetInfo reads chain and accounts together. A 1.25-second balance guard still commits account and chain if the node balance call fails. State emits only when values really change, and active is derived from account presence.

03

Heterogeneous chains keep their differences inside adapters

ConfluxRPCMethod and EthereumRPCMethod expose the same product actions: connect, sendTransaction, personalSign, typedSign, addChain, switchChain, and watchAsset. Internally they preserve the protocol: cfx_requestAccounts versus eth_requestAccounts, cfx_getBalance(..., latest_state) versus eth_getBalance(..., latest), CIP23Domain versus EIP712Domain, and wallet_addConfluxChain versus wallet_addEthereumChain. Fluent can also request cross-network account permissions and move between Core and EVM address views. Product code gets one mental model without purchasing that simplicity by erasing chain semantics.

04

Fine-grained hooks solve both rendering cost and UI ownership

useStatus, useAccount, useChainId, and useBalance subscribe to independent slices, so a header showing only the account does not rerender for balance polling. useBalance maintains a module-level reference count: the first subscriber starts the tracker and the last unmount stops it, tying continuous RPC cost to real UI demand. React uses Zustand and Vue uses shallowReactive and computed, but both are thin adapters over one Emitter. Because the library renders no connection page, a product can use any design system, modal, account card, and responsive layout.

05

AccountManage solves the point where several good wallet stores become chaos

One use-wallet store can describe one wallet. A real DApp must list Fluent Core, Fluent EVM, MetaMask, OKX, TokenPocket, WalletConnect, and dynamically announced EIP-6963 wallets together. AccountManage creates an independent Zustand store for each WalletProvider, while currentWalletName selects which account, accounts, chainId, status, and balance project into the global store. Switching first releases old subscriptions, then the new store takes over with fireImmediately. The wallet name and persistable state survive reloads while balance explicitly does not. useRegisteredWallets also deduplicates and sorts by rdns or name. The film illustrates the result with a desktop wallet list and a mobile bottom sheet. Both read one registry, call the same connect(name), and show the same active wallet. A different layout does not require another connection implementation.

06

WalletConnect is hardest where a CIP session crosses an EIP channel

The WalletConnect plugin first adapts SignClient, QR flow, approval, session_update, and session_delete into the WalletProvider lifecycle. Conflux Core adds another boundary. The compatibility layer encodes a cip155 chain inside an eip155:201029... placeholder namespace, converts Base32 addresses to hex, maps cfx_ methods to eth_ at the transport boundary, then reconstructs CIP data from the session. WalletConnect continues using the eip155 session shape it understands, while AccountManage still receives the same account, chainId, and status model. This is an explicit protocol envelope, not a claim that both chains are identical.

07

Approval management turns the most duplicated pre-transaction logic into a machine

Swap, Stake, and Bridge flows involving ERC20 approval often repeat allowance reads, input debounce, disabled-button logic, approval writes, receipt waiting, and another read. useApproveStatus reduces the UI to checking-approve, need-approve, approving, and approved. useAuthERC20Token refreshes immediately before the write; with an existing allowance it calls increaseAllowance for the delta, otherwise it approves the delta, waits for the receipt, then refreshes. The existing-allowance path requires the token contract to support increaseAllowance, so it does not cover every ERC20. The gain is not one fewer button. Flows using compatible tokens share the same loop of read fact, decide write, and confirm new fact.

08

Decode Action turns hexadecimal transactions into product semantics

Take the illustrative transaction in the film: transfer(to, 25000000), combined with the example token DEMO and its 6 decimals, becomes “Send 25 DEMO to 0x7a00…0042”. A wallet activity feed can show one sentence; an explorer can separate the recipient and amount. These illustrate decoding and presentation, not a recording of a real transaction. Underneath, decodeData starts from a method-selector whitelist and ERC20, ERC721, and ERC1155 ABIs, then combines token metadata to decide whether transferFrom describes fungible tokens or an NFT. When receipt events exist, event facts take precedence, with filters and aggregation for mints, burns, TransferSingle, and TransferBatch. It does not emit a fixed component. It dispatches action types such as ERC20_Transfer, ERC721_Approved, and ERC1155_BatchMint into customUI renderers. Wallet activity, transaction confirmation, and explorer products can share decoding rules while rendering completely different interfaces. The cost remains explicit: this is a whitelist decoder, and unknown protocols require deliberate extension.

09

Omnibus gains value from boundaries, not from putting everything in one package

@cfx-kit/utils, dapp-utils, react-utils, ui-components, and dapp-components form the current release boundary. dapp-utils covers address validation and conversion, typed contract helpers, JSON-RPC batch and Multicall, NFT metadata fallback from server to contract to IPFS, deterministic account avatars, and Decode Action. react-utils owns AccountManage, approvals, and React helpers. ui-components wraps Zag state machines and exposes trigger, content props, markers, and render props so products can own DOM and styles. The evidence boundary matters: dapp-components still contains obvious placeholders beyond AccountAvatar, and the current source lacks tests covering AccountManage, so this page presents evolving infrastructure rather than a fully mature design system.

Project evolution, including later maintenance

Jan–May 2022

Early implementation and current-line initialization

The old branch already had wallet code in January and an npm first release record in February. The current-line root has a May 13 author date and separates Conflux and Ethereum RPC, React and Vue adapters, and fine-grained hooks.

Jul 2022

Balance logic reaches transaction flows

A max-available-balance tracker moved gas estimation and balance changes out of page components and into reusable logic.

Feb–Jun 2023

The wallet matrix expands

TokenPocket, Halo, and other Ethereum adapters arrived without changing the hooks and commands used above them.

Jul 11–12, 2023

dapp-omnibus and AccountManage

Right after the monorepo began, AccountManage moved from Recoil to Zustand: one store per wallet and one switchable projection for the active wallet.

Nov 2023

Approval becomes infrastructure

Allowance reads, four UI states, incremental approval, receipt waiting, and refresh became one reusable flow.

Jan 2024

WalletConnect joins the same account model

Sessions, QR flow, account, and chain changes became a WalletProvider, with a compatibility layer between CIP and EIP namespaces.

Jul–Sep 2024

AccountManage becomes an enumerable registry

Status, balance, registered-wallet listing, and ordering made it possible for any custom connection UI to consume only data and commands.

May–Jun 2025

Dynamic EIP-6963 discovery

Multiple injected wallets in one browser can register dynamically, then deduplicate and sort by rdns or name.

Nov 2025–Jun 2026

Subsequent project maintenance

The repository continued improving addChain, IPFS gateways, addresses, and error handling across five packages.

Evidence boundary

This page is based on the local use-wallet and dapp-omnibus source, CodeGraph call paths, Git history for both repositories, the dapp-omnibus changelogs, and the historical RainbowKit 0.1.0 README and package manifest. The local dapp-omnibus checkout is a shallow clone containing one grafted commit, so its timeline is completed with public GitHub commit records. The page claims only architecture, APIs, and evolution supported by source; it does not claim external adoption, revenue, or untested runtime reliability.