> ## Documentation Index
> Fetch the complete documentation index at: https://docs.passage.coinlist.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallets: the EvmWallet seam

> Implement EvmWallet once and every Passage on-chain flow works with your wallet stack. Prove ownership, allowlist addresses, and hand wallets to CheckoutContainer.

The SDK ships **no wallet stack**. It is not coupled to Privy, Turnkey, AppKit, wagmi, or anything else, and it never asks a user to connect. Wallet discovery, connection, and disconnection are your app's job.

That is deliberate. A partner may be a wallet provider themselves, and every partner already has a wallet story before they reach Passage. So the SDK inverts it: you implement one small interface, `EvmWallet`, and the SDK drives it. Do that once and every on-chain flow works - Superstate swaps, Ondo buys, token-sale approvals, ownership proofs.

<Note>
  The SDK uses and ships [viem](https://viem.sh/) types, the de-facto TypeScript standard for Ethereum, so the interface lines up with whatever you are already using. A `SolanaWallet` seam will follow the same shape when Solana lands.
</Note>

## Two shapes, one seam

There are two interfaces, and the smaller one is a subset of the larger.

| Interface   | Members                                                          | Needed for                                                     |
| ----------- | ---------------------------------------------------------------- | -------------------------------------------------------------- |
| `EvmSigner` | `address`, `signMessage`                                         | Proving wallet ownership. No gas, no transaction.              |
| `EvmWallet` | the two above, plus `writeContract`, `broadcastRawTx`, `awaitTx` | Everything on-chain: approvals, swaps, allowlist transactions. |

`EvmWallet extends EvmSigner`, so implementing the full interface satisfies both. If all you need is the requirements checklist binding a wallet to an offer option, `EvmSigner` is enough and your users never see a gas prompt.

## Implement EvmWallet

```ts theme={null}
import type { EvmWallet } from "@coinlist-co/react";
```

| Member           | Signature                                                           | What the SDK does with it                                                                                                           |
| ---------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `address`        | `EvmWalletAddress`                                                  | Reads balances and allowances, and names the sender.                                                                                |
| `signMessage`    | `(message: string) => Promise<Hex>`                                 | Ownership challenges and allowlist authorization.                                                                                   |
| `writeContract`  | `(params: WriteContractParams) => Promise<Hash>`                    | ERC-20 `approve`, and any contract call the SDK encodes itself.                                                                     |
| `broadcastRawTx` | `(params: BroadcastTxParams) => Promise<Hash>`                      | Sends backend-encoded calldata **verbatim**. Never re-encode it: the contract verifies a signature over the exact arguments inside. |
| `awaitTx`        | `(hash: Hash, chain: EthereumChain) => Promise<TransactionReceipt>` | Waits for confirmation before advancing a flow.                                                                                     |

Every method that takes a chain gets it as an explicit parameter. Switch the wallet to that chain before acting - the SDK does not assume your wallet is already pointed at the right one.

## Adapter: wagmi and AppKit

This is the canonical adapter, and it is the one the [partner demo](https://github.com/coinlist/partner-demo) runs. Any stack that can sign a message and send a transaction works the same way.

```ts wagmiEvmWallet.ts theme={null}
import type { EvmWallet } from "@coinlist-co/react";
import type {
  EthereumChain,
  EvmWalletAddress,
} from "@coinlist-co/react/shared";
import {
  type Config,
  getChainId,
  switchChain,
  waitForTransactionReceipt,
} from "@wagmi/core";
import { type Chain, encodeFunctionData, type WalletClient } from "viem";
import { mainnet, sepolia } from "viem/chains";

const CHAINS: Record<EthereumChain, Chain> = {
  ethereum_mainnet: mainnet,
  ethereum_sepolia: sepolia,
};

export function buildEvmWallet({
  address,
  walletClient,
  config,
}: {
  address: EvmWalletAddress;
  walletClient: WalletClient;
  config: Config;
}): EvmWallet {
  const account = address as `0x${string}`;

  // Each call passes the target chain; switch the wallet there first.
  const ensureChain = async (chain: EthereumChain): Promise<Chain> => {
    const target = CHAINS[chain];
    if (getChainId(config) !== target.id) {
      await switchChain(config, { chainId: target.id });
    }
    return target;
  };

  return {
    address,
    signMessage: (message) => walletClient.signMessage({ account, message }),
    async writeContract({ abi, address: to, functionName, args, value, chain }) {
      const targetChain = await ensureChain(chain);
      const data = encodeFunctionData({ abi, functionName, args });
      return walletClient.sendTransaction({
        account,
        chain: targetChain,
        to,
        data,
        value,
      });
    },
    async broadcastRawTx({ to, data, chain }) {
      const targetChain = await ensureChain(chain);
      return walletClient.sendTransaction({ account, chain: targetChain, to, data });
    },
    awaitTx: (hash, chain) =>
      waitForTransactionReceipt(config, { hash, chainId: CHAINS[chain].id }),
  };
}
```

```bash theme={null}
npm install @coinlist-co/react wagmi @wagmi/core viem @reown/appkit @reown/appkit-adapter-wagmi @tanstack/react-query
```

## Let wallet errors propagate

**Do not catch and translate your wallet library's errors.** Let them throw. The SDK catches them and classifies them into a typed `WalletError`, so you never parse a wallet error string yourself:

| `WalletError`        | Cause                                                   |
| -------------------- | ------------------------------------------------------- |
| `user_rejected`      | The user dismissed the wallet prompt.                   |
| `insufficient_funds` | Not enough native token for gas, or not enough balance. |
| `contract_reverted`  | The transaction was mined and reverted.                 |
| `timeout`            | Confirmation did not arrive in time.                    |
| `unknown`            | Anything the SDK could not classify.                    |

Flows surface it as the `cause` on the step that failed, so you can tell a user rejection apart from an infrastructure problem. See [Errors and edge cases](/sdk/errors) for the full breakdown.

## Proving wallet ownership

Two identities are involved in every Passage purchase, and they are not the same thing:

* The **CoinList session** (from [OAuth](/sdk/oauth-authentication)) identifies *who* is investing.
* The **wallet** is *where* the assets are delivered.

An ownership proof is what links them, which is why the user signs a message before they can transact. Some offers go further: the issuer only settles to wallets they have allowlisted. Superstate assets work this way, and the same allowlists are what make permissioned DeFi pools and RWA lending markets reachable.

Ownership proof is provider-agnostic, so it lives on the platform namespace rather than being duplicated per provider:

```ts theme={null}
const challenge = await coinlist.wallets.createOwnershipChallenge({
  challengeType: "siwe",          // or "plain"
  walletAddress: wallet.address,
  chain: "ethereum_mainnet",
  domain: window.location.host,
  uri: window.location.origin,
  statement: "Sign to prove you own this wallet.",
});

const signature = await wallet.signMessage(challenge.message);

const binding = await coinlist.wallets.connectExternal({
  offerId,
  offerOptionId,
  walletAddress: wallet.address,
  chain: "ethereum_mainnet",
  signature,
});
```

`coinlist.wallets` also reads and removes those bindings: `list({ offerId, offerOptionId })` returns the wallets bound to an option, and `remove({ offerId, addressId })` unbinds one.

<Tip>
  You rarely need to write the sequence above. [`RequirementsChecklistContainer`](/sdk/requirements) runs it for you when an offer has an `external_wallet` or `whitelisted_wallet` requirement, and the [Superstate Swap](/sdk/swap-flow) allowlist flow wraps it in one idempotent call.
</Tip>

## CheckoutWalletSelection: what checkout takes

`EvmWallet` is what the SDK *drives*. `CheckoutWalletSelection` is what you *hand* [`CheckoutContainer`](/sdk/checkout): the wallets the buyer may spend from, plus the two lambdas that connect and disconnect an external one.

```ts theme={null}
import type { CheckoutWalletSelection } from "@coinlist-co/react";

const wallets: CheckoutWalletSelection = {
  // The user's embedded (custodial) wallets, in display order.
  // Empty is a normal state, not an error.
  embedded: embeddedWallets,
  // The connected external wallet, or null while none is connected.
  external: externalWallet,
  // Opens your wallet connector. The SDK reads the outcome from `external`
  // on the next render, not from a return value, so a redirect-based
  // connector works. Reject to report a declined or failed connection.
  connectExternal: () => openConnectModal(),
  disconnectExternal: () => disconnect(),
};
```

There is deliberately no separate wallet model here: `EvmWallet` already carries the `address`, so an `{ address, signer }` pair would be a second wallet type to keep in sync with the first. Everything a wallet row displays beyond the address - the network name - comes from the checkout's execution chain rather than from the wallet, because that is the chain the transaction goes to whatever the wallet is currently pointed at.

## Next step

<Card title="Building the Checkout flow" icon="cart-shopping" href="/sdk/checkout" horizontal>
  Hand your wallets to `CheckoutContainer` and let it render whichever provider the offer belongs to.
</Card>
