Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions components/snippets/explorer-lists.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use client';

/**
* Searchable lists backed by the WalletConnect Explorer API; clicking an entry copies its ID.
* Searchable lists backed by the Explorer API (wallets) and the web3modal API (chains); clicking an entry copies its ID.
* Ported from Mintlify snippets/chainlist.mdx and snippets/walletlist.mdx.
*/
import { useEffect, useMemo, useState } from 'react';

const EXPLORER = 'https://explorer-api.walletconnect.com/v3';
const PROJECT_ID = '8e998cd112127e42dce5e2bf74122539';
const SOURCES = {
chains: `https://api.web3modal.org/getChains?projectId=${PROJECT_ID}&st=docs&sv=1.0.0`,
wallets: `https://explorer-api.walletconnect.com/v3/wallets?projectId=${PROJECT_ID}`,
};

interface Entry {
id: string;
Expand All @@ -19,11 +22,16 @@ function useExplorer(kind: 'chains' | 'wallets') {
const [entries, setEntries] = useState<Entry[]>([]);
const [error, setError] = useState(false);
useEffect(() => {
fetch(`${EXPLORER}/${kind}?projectId=${PROJECT_ID}`)
fetch(SOURCES[kind])
.then((res) => res.json())
.then((data) => {
if (kind === 'chains') {
setEntries(Object.entries<{ name: string }>(data.chains).map(([id, c]) => ({ id, name: c.name })));
setEntries(
(data.data as { name: string; namespace: string; reference: string }[]).map((c) => ({
id: `${c.namespace}:${c.reference}`,
name: c.name,
})),
);
} else {
setEntries(
Object.values<{ id: string; name: string; image_url: { sm: string } }>(data.listings).map((w) => ({
Expand Down
151 changes: 151 additions & 0 deletions content/docs/wallet-verification/attestations.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: "Wallet Verification Results and Attestations"
description: "Verification states, the verification and attestation objects, how to re-check a signature yourself, and the signed message format."
sidebarTitle: "Results & attestations"
---

When a user signs, Wallet Verification checks the signature and stores an attestation on the verification. The attestation holds everything you need to re-check the proof independently.

## Verification states

| State | Terminal | Meaning |
|-------|----------|---------|
| `pending` | No | Created. Waiting for the user to sign. |
| `verified` | Yes | The signature checked out. `attestation` is set. |
| `failed` | Yes | The signature did not verify. `failure_reason` is set. |
| `expired` | Yes | `expires_at` passed without a signature. |
| `cancelled` | Yes | You cancelled the verification. The link no longer works. |

`failure_reason` is currently always `invalid_signature`.

## Verification object

| Field | Description |
|-------|-------------|
| `id` | Verification ID, `ver_` followed by 32 hex characters. |
| `state` | One of the [states](#verification-states) above. |
| `reference_id` | Your identifier from create. |
| `purpose` | `withdrawal`, `payout`, `onboarding`, or `re_verification`. |
| `chain_id` | CAIP-2 chain ID you pinned, or `null`. |
| `expected_address` | Address you pinned, or `null`. |
| `expires_at` | When the link stops working. |
| `return_url` | Return URL you set, or `null`. |
| `created_at` | When the verification was created. |
| `metadata` | Your metadata object, echoed back. Never signed. |
| `failure_reason` | Set when `state` is `failed`. |
| `attestation` | Set when `state` is `verified`. See below. |

The create response also includes `url`. No other response returns it.

## Attestation object

| Field | Description |
|-------|-------------|
| `address` | Verified address. EIP-55 checksummed for EVM, base58 for Solana. |
| `chain_id` | CAIP-2 chain ID the user signed on. |
| `message` | The exact text the user signed. |
| `signature` | `0x` hex for EVM, base58 for Solana. |
| `wallet_type` | `eoa`, `eoa_7702`, `contract`, or `counterfactual`. |
| `verification_method` | `ecrecover`, `erc1271`, `erc6492`, or `ed25519`. |
| `block_number` | Block the contract check ran at, as a decimal string. `null` for stateless methods. |
| `block_hash` | Hash of that block. `null` for stateless methods. |
| `verified_at` | When the signature was verified. |

`ecrecover` and `ed25519` are stateless. `erc1271` and `erc6492` call the wallet contract, so they record the block.

<Callout type="info">
Attestations are kept for 5 years.
</Callout>

## Verify an attestation yourself

You can re-check any attestation without trusting Wallet Verification. Pick the check that matches `verification_method`.

<Callout type="info">
These examples are illustrative. Use them as a starting point and adapt chain selection, RPC endpoints, and error handling to your stack.
</Callout>

<Tabs items={["ecrecover", "erc1271 / erc6492", "ed25519"]}>
<Tab value="ecrecover">

Recover the EIP-191 `personal_sign` signer over `message` and compare it to `address`.

```typescript title="verify-ecrecover.ts"
import { verifyMessage } from "viem";

const valid = await verifyMessage({
address: attestation.address,
message: attestation.message,
signature: attestation.signature,
});
```

</Tab>
<Tab value="erc1271 / erc6492">

Call the ERC-6492 universal signature validator on the attestation's chain at `block_number`. viem's `publicClient.verifyMessage` handles both ERC-1271 and ERC-6492.

```typescript title="verify-contract.ts"
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains"; // match attestation.chain_id

const client = createPublicClient({ chain: mainnet, transport: http() });

const valid = await client.verifyMessage({
address: attestation.address,
message: attestation.message,
signature: attestation.signature,
blockNumber: BigInt(attestation.block_number),
});
```

Checking at a past block needs an RPC endpoint that serves historical state.

</Tab>
<Tab value="ed25519">

Verify the signature over the UTF-8 bytes of `message` with the address as the public key.

```typescript title="verify-ed25519.ts"
import { verifyAsync } from "@noble/ed25519";
import bs58 from "bs58";

const valid = await verifyAsync(
bs58.decode(attestation.signature),
new TextEncoder().encode(attestation.message),
bs58.decode(attestation.address),
);
```

</Tab>
</Tabs>

## Signed message

EVM wallets sign an [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) (Sign-In with Ethereum) message. Solana wallets sign the equivalent [CAIP-122](https://chainagnostic.org/CAIPs/caip-122) message.

```text title="Signed message format"
{page host} wants you to sign in with your {Ethereum|Solana} account:
{address}

{Requester name} asks you to verify wallet ownership to {purpose text}.

URI: {page origin}/s
Version: 1
Chain ID: {EVM: chain number, e.g. 1 | Solana: full CAIP-2 id}
Nonce: {nonce}
Issued At: {created_at}
Expiration Time: {expires_at}
Request ID: {verification id, ver_…}
```

| Purpose | Purpose text |
|---------|--------------|
| `withdrawal` | authorise a withdrawal to this wallet |
| `payout` | receive payouts at this wallet |
| `onboarding` | complete onboarding |
| `re_verification` | re-confirm ownership of this wallet |

The requester name is the business name from [Customization](/wallet-verification/dashboard#customization), with accents stripped to fit the format. Without one, it reads "The requester". Your `reference_id` and `metadata` are not part of the message.

To fetch only the signed text, call `GET /verifications/{id}/message`. It returns `text/plain`, or `409 not_verified` if the verification is not `verified`.
62 changes: 62 additions & 0 deletions content/docs/wallet-verification/dashboard.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: "Wallet Verification Dashboard and API Keys"
description: "Request access, manage API keys, customize the verification page, register origins, and review verification activity in the WalletConnect Pay merchant dashboard."
sidebarTitle: "Dashboard & API keys"
---

You manage Wallet Verification in the [WalletConnect Pay merchant dashboard](https://merchant.pay.walletconnect.com/en/api-keys).

## Request access

1. Open the **API keys** page and select the **Wallet Verification** tab.
2. Click **Request access**. You can add an optional message.
3. The status shows **Request received** while the WalletConnect team reviews your request. Once access is enabled, you can create API keys.

## API keys

![API keys page on the Wallet verification tab, listing two keys with masked values and creation dates](/images/wallet-verification/dashboard-api-keys.png)

- Give each key a name of 1–64 characters.
- You can have up to 10 active keys.
- The full key is shown only once, at creation. Copy it to a secret manager. The list shows a masked key.
- Keys start with `wv_` and go in the `Authorization: Bearer` header.

Keys can be revoked but not edited. To rotate a key, create a new one, deploy it, then revoke the old one.

<Callout type="warning">
Requests with a revoked key return `401 unauthorized`. Deploy the new key before you revoke the old one.
</Callout>

## Activity

The **Activity** page lists every verification on your account with its date, address, wallet, network, reference, and status. Sort by any column and click **Load more** to see older verifications.

![Activity page listing verifications with date, address, wallet, network, reference and status, and an Export CSV button](/images/wallet-verification/dashboard-activity.png)

### Export CSV

Click **Export CSV** to download verifications from the last 90 days.

- The file is prepared in the background and downloads automatically when ready, usually within seconds.
- If it takes more than 2 minutes, the dashboard shows **Taking longer than usual** with a retry option.
- Download links last 15 minutes. Exported files are deleted after 7 days.

## Customization

The **Customization** page controls how the [hosted page](/wallet-verification/hosted-page) looks to your users.

![Customization page with business name, page title and logo fields, and Preview and Save buttons](/images/wallet-verification/dashboard-customization.png)

| Setting | Limit | Where it appears |
|---------|-------|------------------|
| Business name | Up to 120 characters | Hosted page and the [signed message](/wallet-verification/attestations#signed-message) |
| Page title | Up to 120 characters | Hosted page |
| Logo | Image upload | Hosted page |

<Callout type="idea">
Set your business name before you go live. Users see it in their wallet when they sign, which helps them trust the request.
</Callout>

## Integration settings

Allowed return-URL origins and embedding origins are not in the dashboard yet. Contact your WalletConnect contact to register them. See [Return to your app](/wallet-verification/hosted-page#return-to-your-app) and [Embed in an iframe](/wallet-verification/hosted-page#embed-in-an-iframe).
123 changes: 123 additions & 0 deletions content/docs/wallet-verification/hosted-page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: "Wallet Verification User Experience"
description: "What your users see when they verify a wallet, how to return users to your app or embed the page, and which wallets are supported."
sidebarTitle: "User experience"
---

The verification page is where your user connects a wallet and signs the verification message. You get its link as the `url` when you [create a verification](/wallet-verification/quickstart).

## What your user sees

<figure>
![Wallet Verification page asking the user to connect a wallet, with a searchable wallet list and a QR code to connect on mobile](/images/wallet-verification/connect-wallet.png)

<figcaption>The first screen: your business name at the top, the wallet list, and a QR code for mobile wallets.</figcaption>
</figure>

<Steps>
<Step>
### Open the link
The page shows your business name, page title, and logo from [Customization](/wallet-verification/dashboard#customization).
</Step>
<Step>
### Connect a wallet
The user picks a wallet from the list, or on desktop scans the QR code with a mobile wallet. More than 700 wallets connect through WalletConnect.
</Step>
<Step>
### Review
The user reviews the address and taps **Verify address**. The page names the wallet that will ask for approval and states that no funds move.

![Review screen showing the Solana network and shortened address, the wallet that will ask for approval, and a Verify address button](/images/wallet-verification/review.png)
</Step>
<Step>
### Add personal details
Only shown if your account collects user information. After tapping **Verify address**, the user enters their full name, date of birth, and city and country of birth, then accepts the terms.

![Personal details form asking for full name, date of birth, city of birth and country of birth, with an I agree checkbox and a Confirm button](/images/wallet-verification/personal-details.png)
</Step>
<Step>
### Sign
The user signs the message in their wallet. The [signed message](/wallet-verification/attestations#signed-message) names your business and the purpose.
</Step>
<Step>
### Done
The page shows **Wallet verified** if you set `expected_address`, or **Wallet added** if you didn't. With a `return_url`, it sends the user back to your app after 5 seconds, or right away when they tap **Return to** your business name.

![Success screen titled Wallet verified, counting down 5 seconds before returning the user to Acme Inc, with a Return to Acme Inc button](/images/wallet-verification/verified.png)
</Step>
</Steps>

### Errors the user can see

| Error | What it means |
|-------|---------------|
| Wrong wallet | The connected address does not match `expected_address`. |
| Wrong network | The wallet is on a different network than `chain_id`. |
| Invalid signature | The signature did not verify. This is terminal: the verification ends as `failed`, and the user needs a fresh link from you. |
| Unsupported wallet type | The wallet type is not supported. See [Supported wallets and networks](#supported-wallets-and-networks). |
| Unsupported network | The network is not supported. |
| Chain temporarily unavailable | The page could not reach the chain. The user can retry. |

## Return to your app

Set `return_url` when you create a verification to send the user back to your app. After a successful verification the page returns the user automatically after 5 seconds, unless it is [in an iframe](#embed-in-an-iframe). The **Return to** button (it shows your business name) on the success and error screens, and the **Cancel** button, navigate to `return_url` exactly as you gave it. Nothing is appended, so look up the result by polling.

Without a `return_url`, the success screen tells the user they can close the page, and the other buttons close the tab.

The `return_url` must be HTTPS, and its origin must be on your account's allowed list (exact `https://host[:port]`, up to 20 origins). Otherwise create returns `400 invalid_return_url`. See [Integration settings](/wallet-verification/dashboard#integration-settings) to register origins.

## Embed in an iframe

You can show the verification page inside your app in an iframe. The page can only be framed by origins on your account's embedding list. Without one, the page refuses to be framed (`frame-ancestors 'none'`).

Embedding origins are `https://host` or `https://*.host`, up to 20. Each host must be a registrable domain. IP addresses, `localhost`, and bare platform suffixes such as `*.vercel.app` are not allowed. See [Integration settings](/wallet-verification/dashboard#integration-settings) to register origins.

The page is served from these origins:

| Environment | Verification page origin |
|-------------|--------------------------|
| Production | `https://wallet-verification.walletconnect.com` |
| Staging | `https://staging.wallet-verification.walletconnect.com` |

When embedded:

- **When the user verifies**, the page posts a `verified` message to the parent window right away, without waiting for a click.
- **No automatic return** when framed. The 5-second return only runs when the page is not in an iframe. Close the frame or move on when you get `verified`.
- **With a `return_url`**, the Return and Cancel buttons navigate the top window. If your iframe is sandboxed, add `allow-top-navigation-by-user-activation`. Otherwise only the iframe navigates.
- **Without a `return_url`**, the Return and Cancel buttons post a message to the parent window instead of navigating:

```json title="postMessage payload"
{ "type": "wallet_verification:return", "state": "verified" }
```

`state` is one of `verified`, `cancelled`, `failed`, `expired`, `not_found`, `pending`, or `unknown`. The message is only sent to an origin on your embedding list, never to `*`.

```javascript title="Listen for the return message"
window.addEventListener("message", (event) => {
if (event.origin !== "https://wallet-verification.walletconnect.com") return;
if (event.data?.type !== "wallet_verification:return") return;

// Treat this as a hint. Confirm the result on your backend.
closeVerificationFrame();
refreshVerificationStatus();
});
```

<Callout type="warning">
Browsers that do not expose the parent origin, such as Firefox without a referrer, receive no message. Always confirm the result server-side by polling `GET /verifications/{id}`.
</Callout>

## Supported wallets and networks

| Network | Supported | Not supported |
|---------|-----------|---------------|
| EVM | Regular accounts (EOA), EIP-7702 accounts, smart contract wallets (ERC-1271), smart wallets that are not deployed yet (ERC-6492) | Multi-signature wallets that need several approvers |
| Solana | Regular accounts (ed25519) | Program-derived addresses |

An EVM address is the same on every EVM chain. A verification proves control of the address.

## Limitations

- Multi-signature wallets that need more than one approver, such as a Safe with a threshold above 1, are not supported yet. Today such a verification ends as `failed`. Multisig support is planned.
- Solana program-derived addresses are not supported.
- Allowed return-URL and embedding origins are not self-serve yet. See [Integration settings](/wallet-verification/dashboard#integration-settings).
Loading
Loading