Skip to main content

Wallet and network

Certichain has no accounts and no passwords. A person's identity in the app is the address of their Ethereum wallet, and every action that changes the chain or proves who someone is needs that wallet: a signature (a free, off-chain message signed with the wallet's key) or a transaction (an on-chain write that costs gas). Reading and verifying never need a wallet.

The app supports an injected EIP-1193 wallet, the window.ethereum object that MetaMask and the in-app browsers of wallet apps provide. There is no WalletConnect integration.

WalletService​

WalletService (src/services/wallet/wallet-service.ts) is the only code that talks to the wallet. It looks the wallet up on every call, because extensions can inject window.ethereum after the page has loaded.

MethodWhat it doesWallet request
isAvailable()True when a wallet object existsnone
connect()Asks for accounts, keeps a BrowserProvider and signer, returns { address }eth_requestAccounts
signer()The current ethers signer, or null before connect()none
address()The current account; throws "Signer not available" before connect()none
signMessage(message)Personal-sign message; used for the server sign-in (SIWE) messagepersonal_sign through ethers
signTypedData(domain, types, value)EIP-712 typed data signature: issuer signatures, invitations, disclosures, badge key attestations, and the seed signatures that wallet-derived keys come frometh_signTypedData_v4
chainId()The wallet's current chain as a numbereth_chainId
switchChain(chainId)Asks the wallet to switchwallet_switchEthereumChain with the hex chain id
network()A small provider whose getNetwork() asks the wallet for eth_chainId each timeeth_chainId
onChange(listener)Subscribes to accountsChanged and chainChanged; returns an unsubscribe functionevent listeners

Two details matter for correctness:

  • The provider is created with new BrowserProvider(wallet, "any") (createWalletProvider in src/lib/chain.ts). The "any" network lets ethers follow a chain switch instead of failing with a "network changed" error.
  • network() and the chain guard read eth_chainId straight from the wallet (walletNetwork), because a provider's cached network can lag behind a switch. A write is therefore always checked against the chain the wallet is on at that moment.

When the wallet reports new accounts, onChange rebuilds the signer from the same provider before it tells listeners. An empty account list clears the connection.

Errors​

Error (src/services/wallet/errors.ts)MessageWhen
WalletUnavailableError"No wallet detected. Install MetaMask to continue."Any call that needs the wallet when window.ethereum is missing
WalletRejectedError"The wallet request was rejected."connect() when the user refuses

isUserRejection(error) recognises a refusal by the EIP-1193 code 4001, the ethers code "ACTION_REJECTED", or a nested info.error.code of 4001. Pages translate these errors into their own words; for example /login shows "MetaMask is not installed. Please install it to continue." and "Connection cancelled. Please try again."

Methods that need a connection throw a plain Error ("Wallet not connected" or "Signer not available") when called before connect().

The wallet session hook​

Pages that depend on the account use useWalletSession() (src/services/react/use-wallet-session.ts). It calls wallet.connect() once when the component mounts and then follows accountsChanged.

StatusMeaningTypical page reaction
connectingThe first connect() is in flightShow a skeleton, do not redirect yet
connectedaddress holds the accountCheck the role and render
disconnectedThe wallet has no accountsGated pages redirect to /login
unavailableNo wallet. The hook logs "No wallet in this browser; pages that only read still work." with console.infoGated pages redirect; read-only pages work
rejectedThe user refused the connectionGated pages redirect
failedAnother error, logged with console.errorGated pages redirect

The hook does not react to chainChanged; the network banner and the chain guard handle the chain.

Network guard​

A wallet on the wrong chain would sign and send transactions to the wrong network. The app prevents this in two places.

The write guard​

The composition root builds one chainGuard and gives it to every gateway (src/services/index.ts):

  1. If NEXT_PUBLIC_NETWORK_ID is not a number, it throws ChainConfigError ("Blockchain service is not configured").
  2. Otherwise assertCorrectChain(wallet.network(), expected) reads eth_chainId from the wallet and throws WrongNetworkError ("Switch your wallet to Sepolia to continue", with the network name from the chain id) when the two differ.

Every gateway writer() (v0.2 contract, v0.3 registries, access gate) awaits the guard before it returns a contract bound to the signer, so no transaction is ever built for the wrong chain. Reads are not affected: they never use the wallet's chain (see Reading the chain).

Network names come from src/lib/chain.ts: 1 is "Ethereum mainnet", 11155111 is "Sepolia", 31337 is "Hardhat local", and any other id is "chain N".

The network banner​

NetworkBanner (src/components/layout/NetworkBanner.tsx) sits under the header on every page.

  • When a wallet exists and the expected chain can be read, it compares wallet.chainId() with NEXT_PUBLIC_NETWORK_ID and listens for chainChanged.
  • On a mismatch it shows a non-dismissable role="alert" strip: "Your wallet is on the wrong network. Actions that write to the blockchain are disabled until you switch to Sepolia." (with the configured network's name) and a Switch network button.
  • Switch network calls wallet.switchChain(expected). If the wallet refuses or does not know the chain, the banner adds "Could not switch network. Please switch in your wallet."
  • When NEXT_PUBLIC_NETWORK_ID cannot be read, the banner disables itself and logs "Network guard disabled: ..." with console.warn. The write guard still refuses writes in that case.

What prompts the user sees​

A wallet prompt is either a signature or a transaction. The tables in each flow page list the prompts of that flow in order; the kinds are:

PromptKindExample
ConnectAccount access/login, any gated page on first visit
Sign-in messagePersonal signature (SIWE)First upload or document lookup in a session; see Sign-in and sessions
Typed dataEIP-712 signatureIssuer signature over a credential, an invitation, a disclosure attestation
Seed signatureEIP-712 signature, asked for twice and required to matchDeriving the wallet's document keys for the page session (src/services/documents/wallet-key-session.ts) or an access identity (AccessRequesterService.walletSeeds)
TransactionOn-chain write, costs gasIssue, revoke, redeem an invitation, register a record type, pause
Switch chainWallet settingSwitch network
Contract wallets

A smart contract wallet cannot produce the repeatable signatures some features derive keys from. For example /access refuses with "A smart contract wallet cannot make an access identity from its signature. Make a new identity and keep its file instead." Issuer signatures by contract wallets are checked on chain through ERC-1271, but issuing through the site with a contract wallet is not supported today. See Signatures.

Source files​

  • src/services/wallet/wallet-service.ts, src/services/wallet/errors.ts, src/services/wallet/types.ts
  • src/lib/chain.ts
  • src/services/react/use-wallet-session.ts
  • src/components/layout/NetworkBanner.tsx
  • src/services/index.ts (the chainGuard)
  • src/app/login/page.tsx

Tests​

  • src/services/wallet/wallet-service.test.ts
  • src/lib/chain.test.ts
  • src/services/react/use-wallet-session.test.tsx
  • src/components/layout/NetworkBanner.test.tsx
  • e2e/support/ provides the injected test wallet the browser journeys use; see End-to-end tests.