diff --git a/components/snippets/explorer-lists.tsx b/components/snippets/explorer-lists.tsx index bcccca6..67d506a 100644 --- a/components/snippets/explorer-lists.tsx +++ b/components/snippets/explorer-lists.tsx @@ -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; @@ -19,11 +22,16 @@ function useExplorer(kind: 'chains' | 'wallets') { const [entries, setEntries] = useState([]); 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) => ({ diff --git a/content/docs/wallet-verification/attestations.mdx b/content/docs/wallet-verification/attestations.mdx new file mode 100644 index 0000000..e3c1f5f --- /dev/null +++ b/content/docs/wallet-verification/attestations.mdx @@ -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. + + +Attestations are kept for 5 years. + + +## Verify an attestation yourself + +You can re-check any attestation without trusting Wallet Verification. Pick the check that matches `verification_method`. + + +These examples are illustrative. Use them as a starting point and adapt chain selection, RPC endpoints, and error handling to your stack. + + + + + +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, +}); +``` + + + + +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. + + + + +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), +); +``` + + + + +## 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`. diff --git a/content/docs/wallet-verification/dashboard.mdx b/content/docs/wallet-verification/dashboard.mdx new file mode 100644 index 0000000..5f368e0 --- /dev/null +++ b/content/docs/wallet-verification/dashboard.mdx @@ -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. + + +Requests with a revoked key return `401 unauthorized`. Deploy the new key before you revoke the old one. + + +## 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 | + + +Set your business name before you go live. Users see it in their wallet when they sign, which helps them trust the request. + + +## 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). diff --git a/content/docs/wallet-verification/hosted-page.mdx b/content/docs/wallet-verification/hosted-page.mdx new file mode 100644 index 0000000..8caead2 --- /dev/null +++ b/content/docs/wallet-verification/hosted-page.mdx @@ -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 + +
+ ![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) + +
The first screen: your business name at the top, the wallet list, and a QR code for mobile wallets.
+
+ + + + ### Open the link + The page shows your business name, page title, and logo from [Customization](/wallet-verification/dashboard#customization). + + + ### 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. + + + ### 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) + + + ### 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) + + + ### Sign + The user signs the message in their wallet. The [signed message](/wallet-verification/attestations#signed-message) names your business and the purpose. + + + ### 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) + + + +### 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(); +}); +``` + + +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}`. + + +## 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). diff --git a/content/docs/wallet-verification/overview.mdx b/content/docs/wallet-verification/overview.mdx new file mode 100644 index 0000000..e736c1e --- /dev/null +++ b/content/docs/wallet-verification/overview.mdx @@ -0,0 +1,69 @@ +--- +title: "Wallet Verification" +description: "Prove that a user controls a crypto wallet by having them sign a message. No funds move and no transaction is sent." +sidebarTitle: "Overview" +--- + +Wallet Verification lets you confirm that a user controls a wallet address before you rely on it. Your backend creates a verification, the user opens a verification page, connects their wallet, and signs a plain-text message. Wallet Verification checks the signature and stores an attestation that you can read and re-check yourself. + +The user only signs a message. No funds move, no transaction is sent, and the user pays no gas. + +## Who is Wallet Verification for? + +- **Payout platforms and PSPs** that need to confirm a beneficiary controls the destination wallet before sending a payout or approving a withdrawal. +- **Platforms verifying their own customers** that want proof of ownership for the wallets customers link during onboarding or re-verification. +- **Regulated institutions** that need proof of control over self-hosted wallets for Travel Rule checks. + +## How does Wallet Verification work? + +```mermaid +sequenceDiagram + participant Backend as Your backend + participant API as Wallet Verification API + participant User as User + participant Page as Verification page + + Backend->>API: POST /verifications + API-->>Backend: 201 verification + url + Backend->>User: Send the url (link, redirect, or embed) + User->>Page: Open link and connect wallet + Page->>User: Ask for a signature + User->>Page: Sign the message + Page->>API: Submit signature + API->>API: Check signature and store attestation + loop Until the state is terminal + Backend->>API: GET /verifications/{id} + end + API-->>Backend: state verified + attestation +``` + +1. Your backend creates a verification with your own `reference_id` and a `purpose`. The response includes a one-time `url`. +2. You send the user that `url` as a link, a redirect, or an embedded iframe. +3. The user opens the [verification page](/wallet-verification/hosted-page), connects a wallet, and signs a message that names your business and the purpose. +4. Wallet Verification checks the signature and stores an [attestation](/wallet-verification/attestations). +5. Your backend polls the verification until it reaches a terminal state, then reads the attestation. + + +Wallet Verification does not send webhooks. Poll `GET /verifications/{id}` to get the result. + + +## Get started + + + } href="/wallet-verification/quickstart"> + Create your first verification and read the result. + + } href="/wallet-verification/dashboard"> + Request access, manage API keys, customize the page, and review activity. + + } href="/wallet-verification/hosted-page"> + What your users see, supported wallets, return URLs, and embedding. + + } href="/wallet-verification/attestations"> + States, the attestation, how to re-check a signature, and the signed message. + + + + +The API reference is coming soon. + diff --git a/content/docs/wallet-verification/quickstart.mdx b/content/docs/wallet-verification/quickstart.mdx new file mode 100644 index 0000000..aa67928 --- /dev/null +++ b/content/docs/wallet-verification/quickstart.mdx @@ -0,0 +1,202 @@ +--- +title: "Wallet Verification Quickstart" +description: "Create a wallet verification, send the link to your user, and read the signed attestation." +sidebarTitle: "Quickstart" +--- + +This guide takes you from an API key to a verified wallet in five steps. + +| Environment | API base URL | +|-------------|--------------| +| Production | `https://api.wallet-verification.walletconnect.com` | +| Staging | `https://staging.api.wallet-verification.walletconnect.com` | + + +Use staging for integration testing only; attestations created there are not evidence for compliance. There is no test mode in production. + + + + + +### Get access and an API key + +In the WalletConnect Pay merchant dashboard, open the **Wallet Verification** tab on the [API keys](https://merchant.pay.walletconnect.com/en/api-keys) page and request access. Once access is enabled, create an API key. The full key is shown only once, so store it in a secret manager. See [Dashboard & API keys](/wallet-verification/dashboard) for details. + +Send the key as a bearer token on every request: + +```bash title="Environment" +export WV_API_KEY="wv_your-api-key" +export WV_BASE="https://api.wallet-verification.walletconnect.com" +``` + +A missing, unknown, or revoked key, or an account without access, returns `401 unauthorized`. + + + + +### Create a verification + +```bash title="Create a verification" +curl -X POST "$WV_BASE/verifications" \ + -H "Authorization: Bearer $WV_API_KEY" \ + -H "Idempotency-Key: $(uuidgen)" \ + -H "Content-Type: application/json" \ + -d '{ + "reference_id": "customer-4821-payout-wallet", + "purpose": "payout", + "chain_id": "eip155:1", + "return_url": "https://app.example.com/wallets" + }' +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `reference_id` | Yes | Your own identifier, 1–200 printable ASCII characters with no spaces. Not included in the signed message. | +| `purpose` | Yes | `withdrawal`, `payout`, `onboarding`, or `re_verification`. Sets the wording of the signed message. | +| `chain_id` | No | CAIP-2 chain ID in the `eip155` or `solana` namespace. If omitted, the user chooses Ethereum-compatible or Solana when connecting. | +| `expected_address` | No | Pins the wallet. The page and the API refuse any other address. Returns `400 invalid_address` if it does not fit the pinned chain. | +| `validity_seconds` | No | How long the link stays valid. 60 to 2,592,000 (30 days). Defaults to 604,800 (7 days). | +| `metadata` | No | Object up to 4 KiB, echoed back on the verification. Never signed. Do not put personal data here. | +| `return_url` | No | HTTPS URL the page sends the user back to. Up to 2,048 characters, no credentials, and its origin must be registered for your account. See [Return to your app](/wallet-verification/hosted-page#return-to-your-app). | + +The request body can be up to 16 KiB. Larger bodies return `413 request_error`. + +A successful request returns `201` with the [verification object](/wallet-verification/attestations#verification-object) plus a `url`: + +```json title="201 Created" +{ + "id": "ver_3f9a1c2b7d8e4f6a9b0c1d2e3f4a5b6c", + "state": "pending", + "reference_id": "customer-4821-payout-wallet", + "purpose": "payout", + "chain_id": "eip155:1", + "expected_address": null, + "expires_at": "2026-10-15T10:00:00Z", + "return_url": "https://app.example.com/wallets", + "created_at": "2026-10-08T10:00:00Z", + "metadata": {}, + "failure_reason": null, + "attestation": null, + "url": "https://wallet-verification.walletconnect.com/s/your-session-token" +} +``` + + +The `url` is returned only in this response. Store it with the verification `id`. + + + + + +### Send the user the link + +Give the user the `url` however fits your product: send it as a link, redirect the browser to it, or embed it in an iframe. The [verification page](/wallet-verification/hosted-page) walks the user through connecting and signing. + + + + +### Poll for the result + +Poll the verification until its `state` is terminal: `verified`, `failed`, `expired`, or `cancelled`. + +```bash title="Read a verification" +curl "$WV_BASE/verifications/ver_3f9a1c2b7d8e4f6a9b0c1d2e3f4a5b6c" \ + -H "Authorization: Bearer $WV_API_KEY" +``` + + +Poll every few seconds while the user is on the page, then back off. If the user returns through your `return_url` or the page posts a message to your parent window, poll once more to confirm the result server-side. + + +To find verifications later, list them with `GET /verifications`. Results are newest first and support `limit` (1–100, default 20), an opaque `cursor`, and the filters `state`, `reference_id`, `from` (inclusive), and `to` (exclusive). The response is `{ "data": [...], "next_cursor": "..." }`, and `next_cursor` is `null` on the last page. + + + + +### Read the result + +When `state` is `verified`, the `attestation` field holds the address, the exact signed message, the signature, and how it was checked. See [Results & attestations](/wallet-verification/attestations) for every field and how to re-check the signature yourself. + + + + + +## Idempotency + +Send an `Idempotency-Key` header (1–255 characters) on create so you can retry safely. Keys are scoped to your account and kept for 24 hours. + +- Retrying with the same key and the same body returns `200` with the same verification and **no `url`**. Keep the `url` from the first `201`. +- Reusing a key with a different body returns `409 idempotency_key_mismatch`. + +## One live session per reference + +Each `reference_id` can have only one `pending` verification at a time. Creating another while one is pending returns `409 reference_in_progress`, and the link is not re-issued: + +```json title="409 Conflict" +{ + "status": "error", + "error": { + "code": "reference_in_progress", + "message": "A verification for this reference is already in progress.", + "details": { + "verification_id": "ver_3f9a1c2b7d8e4f6a9b0c1d2e3f4a5b6c", + "state": "pending", + "expires_at": "2026-10-15T10:00:00Z" + } + } +} +``` + +Once the earlier verification reaches a terminal state, create a new one with the same `reference_id`. Use the `re_verification` purpose so the signed message says why. + +## Cancel a verification + +Cancel a pending verification to stop its link from working: + +```bash title="Cancel a verification" +curl -X POST "$WV_BASE/verifications/ver_3f9a1c2b7d8e4f6a9b0c1d2e3f4a5b6c/cancel" \ + -H "Authorization: Bearer $WV_API_KEY" +``` + +The state moves to `cancelled`. Cancelling a verification in any other state returns `409 not_cancellable`. + +## Error handling + +Errors use one envelope. Switch on `code`. The `message` text may change. + +```json title="Error envelope" +{ + "status": "error", + "error": { + "code": "invalid_body", + "message": "Human-readable description.", + "details": {} + } +} +``` + +| Code | HTTP | Meaning | +|------|------|---------| +| `invalid_body` | 400 | The request body failed validation. | +| `invalid_address` | 400 | `expected_address` is not valid for the pinned chain. | +| `invalid_return_url` | 400 | `return_url` is malformed or its origin is not registered for your account. | +| `invalid_cursor` | 400 | The cursor is invalid or was reused with different filters. | +| `request_error` | 400, 413 | The request is malformed, or the body is larger than 16 KiB. | +| `unauthorized` | 401 | The API key is missing, unknown, or revoked, or the account has no access. | +| `not_found` | 404 | No verification with this ID exists for your account. | +| `not_cancellable` | 409 | Only `pending` verifications can be cancelled. | +| `not_verified` | 409 | The signed message is only available once the verification is `verified`. | +| `reference_in_progress` | 409 | A `pending` verification already exists for this `reference_id`. | +| `idempotency_key_mismatch` | 409 | The idempotency key was used with a different body. | +| `internal_error` | 500 | Something went wrong on the server. Retry with the same idempotency key. | + +## Frequently asked questions + + + + The verification moves to `expired` once `expires_at` passes without a signature, and the link stops working. Create a new verification to get a new link. + + + No. The `url` is returned only in the first `201` response. If you lose it, cancel the verification and create a new one. + + diff --git a/content/docs/wallets/android/verify.mdx b/content/docs/wallets/android/verify.mdx index eb03d8a..7fb9283 100644 --- a/content/docs/wallets/android/verify.mdx +++ b/content/docs/wallets/android/verify.mdx @@ -42,19 +42,66 @@ How your wallet should read `verifyContext` depends on how the dApp connects: ### Implementation -Wallet.Event.VerifyContext provides a domain verification information about SessionProposal, SessionRequest and AuthRequest. - -It consists of origin of an app from where the request has been sent, validation Enum that says whether origin is `VALID`, `INVALID` or `UNKNOWN` and verify url server. +`WalletKit.WalletDelegate` passes a `Wallet.Model.VerifyContext` to `onSessionProposal`, `onSessionAuthenticate` and `onSessionRequest`: ```kotlin data class VerifyContext( val id: Long, val origin: String, - val validation: Model.Validation, - val verifyUrl: String + val validation: Validation, + val verifyUrl: String, + val isScam: Boolean?, ) enum class Validation { VALID, INVALID, UNKNOWN } ``` + +Map it to one of the four states once, then use that state in each callback. + +```kotlin +enum class VerifyState { THREAT, MISMATCH, UNVERIFIED, DOMAIN_MATCH } + +fun Wallet.Model.VerifyContext.toVerifyState(): VerifyState = when { + // Threat: only set over relay, since Link Mode has no scam check + isScam == true -> VerifyState.THREAT + validation == Wallet.Model.Validation.VALID -> VerifyState.DOMAIN_MATCH + validation == Wallet.Model.Validation.INVALID -> VerifyState.MISMATCH + // UNKNOWN: expected for native dApps over relay. Link Mode never returns UNKNOWN + else -> VerifyState.UNVERIFIED +} + +val walletDelegate = object : WalletKit.WalletDelegate { + // Session proposals from a web or native dApp + override fun onSessionProposal(sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext) { + val state = verifyContext.toVerifyState() + // show the state on the approval screen + } + + // One-click auth requests can arrive over relay or Link Mode + override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit) + get() = { sessionAuthenticate, verifyContext -> + val state = verifyContext.toVerifyState() + val isLinkMode = sessionAuthenticate.participant.metadata?.linkMode == true + // in Link Mode, MISMATCH means the universal link domain doesn't match metadata.url + } + + override fun onSessionRequest(sessionRequest: Wallet.Model.SessionRequest, verifyContext: Wallet.Model.VerifyContext) { + val state = verifyContext.toVerifyState() + val isLinkMode = sessionRequest.peerMetaData?.linkMode == true + // show the state on the signing screen + } + + // ...other WalletDelegate callbacks +} +``` + +WalletKit doesn't expose the transport a request arrived on. To detect Link Mode, read `linkMode` from the dApp's metadata, as shown above. + +Present each state like this: + +- `THREAT`: Block the request, or require the user to explicitly accept the risk. Check before anything else, so the user doesn't also see a Mismatch warning. +- `MISMATCH`: Show a warning and proceed only if the user accepts the risk. +- `UNVERIFIED`: Show a neutral "Cannot verify" label. Don't block the request, since native dApps over relay always land here. +- `DOMAIN_MATCH`: Show a verified badge and proceed as normal. diff --git a/content/docs/wallets/flutter/verify.mdx b/content/docs/wallets/flutter/verify.mdx index 24885e6..ffbe05f 100644 --- a/content/docs/wallets/flutter/verify.mdx +++ b/content/docs/wallets/flutter/verify.mdx @@ -42,15 +42,50 @@ How your wallet should read `verifyContext` depends on how the dApp connects: ### Implementation -To check the Verify API validations and whether or not your user is interacting with potentially malicious dapp, you can do so by accessing the `verifyContext` included in the `SessionProposalEvent`: - -```javascript +`onSessionProposal` and `onSessionAuthRequest` include a `verifyContext`. `onSessionRequest` doesn't, so read it from the matching pending request. Map it to one of the four states once, then use that state in each handler. + +```dart +enum VerifyState { threat, mismatch, unverified, domainMatch } + +VerifyState getVerifyState(VerifyContext? verifyContext) { + return switch (verifyContext?.validation) { + // Threat: only set over relay, since Link Mode has no scam check + Validation.SCAM => VerifyState.threat, + Validation.INVALID => VerifyState.mismatch, + Validation.VALID => VerifyState.domainMatch, + // UNKNOWN: expected for native dApps over relay. Link Mode never returns UNKNOWN + _ => VerifyState.unverified, + }; +} + +// Session proposals from a web or native dApp _walletKit!.onSessionProposal.subscribe((SessionProposalEvent? args) { - if (args != null) { - final scamApp = args.verifyContext?.validation.scam; - final invalidApp = args.verifyContext?.validation.invalid; - final validApp = args.verifyContext?.validation.valid; - final unknown = args.verifyContext?.validation.unknown; - } + if (args == null) return; + final state = getVerifyState(args.verifyContext); + // show the state on the approval screen +}); + +// One-click auth requests can arrive over relay or Link Mode +_walletKit!.onSessionAuthRequest.subscribe((SessionAuthRequest? args) { + if (args == null) return; + final state = getVerifyState(args.verifyContext); + final isLinkMode = args.transportType.isLinkMode; + // in Link Mode, MISMATCH means the universal link domain doesn't match metadata.url }); -``` \ No newline at end of file + +// Session requests: read verifyContext from the pending request +_walletKit!.onSessionRequest.subscribe((SessionRequestEvent? args) { + if (args == null) return; + final pendingRequest = _walletKit!.pendingRequests.get(args.id.toString()); + final state = getVerifyState(pendingRequest?.verifyContext); + final isLinkMode = args.transportType.isLinkMode; + // show the state on the signing screen +}); +``` + +Present each state like this: + +- `THREAT`: Block the request, or require the user to explicitly accept the risk. Check before anything else, so the user doesn't also see a Mismatch warning. +- `MISMATCH`: Show a warning and proceed only if the user accepts the risk. +- `UNVERIFIED`: Show a neutral "Cannot verify" label. Don't block the request, since native dApps over relay always land here. +- `DOMAIN_MATCH`: Show a verified badge and proceed as normal. diff --git a/content/docs/wallets/ios/verify.mdx b/content/docs/wallets/ios/verify.mdx index b1f0920..c07293f 100644 --- a/content/docs/wallets/ios/verify.mdx +++ b/content/docs/wallets/ios/verify.mdx @@ -37,20 +37,77 @@ How your wallet should read `verifyContext` depends on how the dApp connects: ### Implementation -VerifyContext provides a domain verification information about Session.Proposal and Request and is relevant to the `verifyDapp` function. +`sessionProposalPublisher`, `authenticateRequestPublisher` and `sessionRequestPublisher` each emit a `VerifyContext?` alongside the request: -It consists of origin of an app from where the request has been sent, validation enum that says whether origin is unknown, valid or invalid and verify URL server. +```swift +public struct VerifyContext: Equatable, Hashable, Codable { + public enum ValidationStatus: Codable { + case unknown + case valid + case invalid + case scam + } + + public let origin: String? + public let validation: ValidationStatus +} +``` + +Map it to one of the four states once, then use that state in each subscriber. ```swift -public struct VerifyContext: Equatable, Hashable { - public enum ValidationStatus { - case unknown - case valid - case invalid - } - - public let origin: String? - public let validation: ValidationStatus - public let verifyUrl: String +enum VerifyState { case threat, mismatch, unverified, domainMatch } + +func verifyState(for context: VerifyContext?) -> VerifyState { + switch context?.validation { + case .scam: + // Threat: only set over relay, since Link Mode has no scam check + return .threat + case .invalid: + return .mismatch + case .valid: + return .domainMatch + case .unknown, .none: + // UNKNOWN: expected for native dApps over relay + return .unverified + } } + +// Session proposals from a web or native dApp +WalletKit.instance.sessionProposalPublisher + .receive(on: DispatchQueue.main) + .sink { proposal, context in + let state = verifyState(for: context) + // show the state on the approval screen + } + .store(in: &publishers) + +// One-click auth requests can arrive over relay or Link Mode +WalletKit.instance.authenticateRequestPublisher + .receive(on: DispatchQueue.main) + .sink { request, context in + let state = verifyState(for: context) + // in Link Mode, MISMATCH means the universal link domain doesn't match metadata.url + } + .store(in: &publishers) + +WalletKit.instance.sessionRequestPublisher + .receive(on: DispatchQueue.main) + .sink { request, context in + let state = verifyState(for: context) + // show the state on the signing screen + } + .store(in: &publishers) ``` + +Keep in mind: + +- WalletKit doesn't expose the transport a request arrived on. To detect Link Mode, check for `wc_ev` in the incoming universal link before passing it to `WalletKit.instance.dispatchEnvelope(_:)`, and keep track of that request's topic. +- Session requests sent over Link Mode arrive with a `nil` context, so they map to `UNVERIFIED`. + +Present each state like this: + +- `THREAT`: Block the request, or require the user to explicitly accept the risk. Check before anything else, so the user doesn't also see a Mismatch warning. +- `MISMATCH`: Show a warning and proceed only if the user accepts the risk. +- `UNVERIFIED`: Show a neutral "Cannot verify" label. Don't block the request, since native dApps over relay always land here. +- `DOMAIN_MATCH`: Show a verified badge and proceed as normal. diff --git a/content/docs/wallets/more/push-notifications.mdx b/content/docs/wallets/more/push-notifications.mdx new file mode 100644 index 0000000..00a58c4 --- /dev/null +++ b/content/docs/wallets/more/push-notifications.mdx @@ -0,0 +1,81 @@ +--- +sidebarTitle: Push Notifications +title: Push Notifications +description: "Receive push notifications for incoming wallet requests through a self-hosted push server." +--- + +WalletConnect lets your wallet receive push notifications for incoming requests through a self-hosted push server. + +When a dapp sends your wallet a session proposal, a session request or an authentication request, the relay sends it to the push URL set on your project. The push server behind that URL looks up the device and sends the notification through APNs or FCM. + +The push URL points at a server you run. Use the [open-source push server](https://github.com/reown-com/push-server), or write your own against the push server API. + +```mermaid +sequenceDiagram + participant W as Wallet app + participant P as Your push server + participant R as Relay + participant N as APNs / FCM + W->>P: POST /clients (client ID and device token) + R->>P: POST /clients/{client_id}, signed by the relay + P->>N: Notification + N->>W: Notification +``` + +## Run the push server + +Build the image from `slim.Dockerfile`. This is the single-tenant build: it serves one wallet app and reads its APNs and FCM credentials from environment variables. The credentials stay on your server; the dashboard stores only the push URL. + +| Variable | Value | +| --- | --- | +| `PUBLIC_URL` | The server's public address, for example `https://push.example.com`. | +| `DATABASE_URL` | A Postgres database for device registrations. The server runs its own migrations on start. | +| `TENANT_DATABASE_URL` | Must be set, and left empty in this build. | +| `RELAY_PUBLIC_KEY` | The relay's signing key, from `https://relay.walletconnect.com/public-key`. | +| `FCM_V1_CREDENTIALS` | Your Firebase service account key, as JSON. | +| `APNS_TYPE`, `APNS_TOPIC` | `Token` or `Certificate`, and your app's bundle ID. | +| `APNS_PKCS8_PEM`, `APNS_KEY_ID`, `APNS_TEAM_ID` | For `Token`: your `.p8` key, base64-encoded, and its key and team IDs. | +| `APNS_CERTIFICATE`, `APNS_CERTIFICATE_PASSWORD` | For `Certificate`: your `.p12` certificate, base64-encoded, and its password. | + +Keep `VALIDATE_SIGNATURES` at its default of `true`, so the server rejects requests that the relay did not sign. + +## Set the push URL + +In the [WalletConnect Dashboard](https://dashboard.walletconnect.com), open your project's **Push** page and choose **Create Push URL**. The page appears only for wallet projects. + +![The Push page in the WalletConnect Dashboard, with the Create Push URL button](/images/push-url.png) + +Enter your `PUBLIC_URL`: + +- It must use HTTPS and a public domain name, not an IP address. +- It can include a base path, but no query string, fragment or credentials, because the relay appends `/clients/{client_id}` to it. + +The relay caches project settings, so a change can take several minutes to take effect. + +## Register devices + +The relay knows each wallet only by its relay client ID, and APNs and FCM deliver only to a device token. Registering a device stores the pair on your push server, so that when the relay asks it to notify a client ID, the server knows which device token to send to. A device that is not registered gets no notifications. + +Don't use the SDK's push registration (`WalletKit.instance.register(deviceToken:)` on iOS, `WalletKit.registerDeviceToken` on Android, `walletKit.registerDeviceToken` in React Native) with a self-hosted server. On Android and React Native it always registers with WalletConnect's hosted server. On iOS you can change the host with `pushHost`, but the SDK sends to `/{projectId}/clients`, which the self-hosted server doesn't serve. Your app has to register with your server directly instead: + +```http +POST https://push.example.com/clients +Content-Type: application/json + +{ "client_id": "", "type": "fcm", "token": "", "always_raw": true } +``` + +- `client_id` is the ID the wallet uses on the relay: `CoreClient.Push.clientId` on Android, `Networking.interactor.getClientId()` on iOS, `walletKit.core.crypto.getClientId()` in React Native. +- `type` is `apns`, `apns-sandbox` or `fcm`. +- `always_raw` sets what the notification contains. It matches the SDKs' `enableEncrypted` option: + - `true`: the notification carries the encrypted request as `topic`, `tag` and `message`. Your app decrypts it and shows its own text, for example the dapp's name and what it asks for. + - `false`: the user sees a fixed "Signature required" alert written by the relay, and your app does not decrypt anything. + +Send the registration when the app starts and has a device token, and again whenever the OS issues a new token. To stop notifications, for example when the user turns them off or logs out, send `DELETE /clients/{client_id}`. + +## Delivery + +The open-source push server already handles this. If you write your own, respond to each request from the relay within 30 seconds: + +- Return a 2xx response once the notification is handed to APNs or FCM. +- Return a 5xx response only when a retry could succeed. The relay retries after a timeout, a network error or a 5xx response, and does not retry any other response. diff --git a/content/docs/wallets/react-native/verify.mdx b/content/docs/wallets/react-native/verify.mdx index 54cb08b..0ee9621 100644 --- a/content/docs/wallets/react-native/verify.mdx +++ b/content/docs/wallets/react-native/verify.mdx @@ -42,38 +42,54 @@ How your wallet should read `verifyContext` depends on how the dApp connects: ### Implementation -To check the Verify API validations and whether or not your user is interacting with potentially malicious app, you can do so by accessing the `verifyContext` included in the request payload. +Every `session_proposal`, `session_authenticate` and `session_request` event includes a `verifyContext`. Map it to one of the four states once, then use that state in each handler. ```javascript -... -walletKit.on("auth_request", async (authRequest) => { - const { verifyContext } = authRequest - const validation = verifyContext.verified.validation // can be VALID, INVALID or UNKNOWN - const origin = verifyContext.verified.origin // the actual verified origin of the request - const isScam = verifyContext.verified.isScam // true if the domain is flagged as malicious - - // if the domain is flagged as malicious, you should warn the user as they may lose their funds - check the `Threat` case for more info - if(isScam) { - // show a warning screen to the user - // and proceed only if the user accepts the risk - } +function getVerifyState(verifyContext) { + const { validation, isScam } = verifyContext.verified + + // Threat: only set over relay, since Link Mode has no scam check + if (isScam) return "THREAT" - switch(validation) { + switch (validation) { case "VALID": - // proceed with the request - check the `Domain match` case for more info - break + return "DOMAIN_MATCH" case "INVALID": - // show a warning dialog to the user - check the `Mismatch` case for more info - // and proceed only if the user accepts the risk - break - case "UNKNOWN": - // show a warning dialog to the user - check the `Unverified` case for more info - // and proceed only if the user accepts the risk - break + return "MISMATCH" + default: + // UNKNOWN: expected for native dApps over relay. Link Mode never returns UNKNOWN + return "UNVERIFIED" } +} + +// Session proposals always arrive over relay, from a web or native dApp +walletKit.on("session_proposal", async (proposal) => { + const state = getVerifyState(proposal.verifyContext) + // show the state on the approval screen +}) + +// One-click auth requests can arrive over relay or Link Mode +walletKit.on("session_authenticate", async (payload) => { + const state = getVerifyState(payload.verifyContext) + const isLinkMode = payload.transportType === "link_mode" + // in Link Mode, MISMATCH means the universal link domain doesn't match metadata.url +}) + +// Session requests: read the transport from the session they belong to +walletKit.on("session_request", async (request) => { + const state = getVerifyState(request.verifyContext) + const { transportType } = walletKit.getActiveSessions()[request.topic] + // show the state on the signing screen }) ``` +Present each state like this: + +- `THREAT`: Block the request, or require the user to explicitly accept the risk. Check before anything else, so the user doesn't also see a Mismatch warning. +- `MISMATCH`: Show a warning and proceed only if the user accepts the risk. +- `UNVERIFIED`: Show a neutral "Cannot verify" label. Don't block the request, since native dApps over relay always land here. +- `DOMAIN_MATCH`: Show a verified badge and proceed as normal. + For live demo examples of the intended Verify API flows, check out our demo apps: - [Demo Web Wallet](https://react-wallet.walletconnect.com) diff --git a/content/docs/wallets/walletguide/explorer-api.mdx b/content/docs/wallets/walletguide/explorer-api.mdx index cf11c74..d41f596 100644 --- a/content/docs/wallets/walletguide/explorer-api.mdx +++ b/content/docs/wallets/walletguide/explorer-api.mdx @@ -4,7 +4,7 @@ title: Explorer API The Cloud Explorer API currently offers the following functionality: -- [Listings](#listings) - Allows for fetching of wallets and dApps listed in the [WalletGuide](https://walletguide.walletconnect.network/). +- [Listings](#listings) - Allows for fetching of wallets listed in the [WalletGuide](https://walletguide.walletconnect.network/). - [Logos](#logos) - Provides logo assets in different sizes for a given Cloud explorer entry. ### Listings @@ -33,22 +33,6 @@ Examples: - `GET https://explorer-api.walletconnect.com/v3/wallets?projectId=YOUR_PROJECT_ID&entries=5&page=1` (will return the first 5 wallets from the first page) - `GET https://explorer-api.walletconnect.com/v3/wallets?projectId=YOUR_PROJECT_ID&platforms=injected` (will only return injected wallets) -#### `GET /v3/dapps` - -Returns a JSON object containing all dApps listed in the public cloud explorer. - -Examples: - -- `GET https://explorer-api.walletconnect.com/v3/dapps?projectId=YOUR_PROJECT_ID&entries=5&page=1` - -#### `GET /v3/hybrid` - -Returns a JSON object containing all hybrids listed in the public cloud explorer. - -Examples: - -- `GET https://explorer-api.walletconnect.com/v3/hybrid?projectId=YOUR_PROJECT_ID&entries=5&page=1` - #### `GET /v3/all` Returns a JSON object containing all entries listed in the public cloud explorer. @@ -66,29 +50,6 @@ Examples: - `GET https://explorer-api.walletconnect.com/v3/all?projectId=YOUR_PROJECT_ID&ids=be49f0a78d6ea1beed3804c3a6b62ea71f568d58d9df8097f3d61c7c9baf273d,4622a2b2d6af1c9844944291e5e7351a6aa24cd7b23099efac1b2fd875da31a0` -### Chains - -By default chains endpoint returns all chains registered under [CASA Namespace](https://github.com/ChainAgnostic/CASA) and that were approved by our Network Partnerships team. To add a new chain, fill out the application form linked on our [chains and foundations landing page](https://walletconnect.com/solutions/chains-and-foundations). - -#### Query Parameters - -You can use following query params to query chains by its namespace and exclude testnets: - -| Param | Description | -| ---------- | ---------------------------------------------------------------------------------------------------------------------------- | -| testnets | Determines if testnets should be included in the response
(e.g. `?testnets=false`, defaults to `true` if not provided) | -| namespaces | Returns chains that belong to one of the provided namespaces
(e.g. `?namespaces=eip155,cosmos,solana`) | - -#### `GET /v3/chains` - -Returns all chains registered under [CASA Namespace](https://github.com/ChainAgnostic/CASA) and that were approved by our Network Partnerships team. To add a new chain, fill out the application form linked on our [chains and foundations landing page](https://walletconnect.com/solutions/chains-and-foundations). - -Examples: - -- `GET https://explorer-api.walletconnect.com/v3/chains?projectId=YOUR_PROJECT_ID` -- `GET https://explorer-api.walletconnect.com/v3/chains?projectId=YOUR_PROJECT_ID&testnets=false` -- `GET https://explorer-api.walletconnect.com/v3/chains?projectId=YOUR_PROJECT_ID&namespaces=eip155,cosmos` - ### Logos #### Path Parameters diff --git a/content/docs/wallets/web/verify.mdx b/content/docs/wallets/web/verify.mdx index f5f8564..6d0f599 100644 --- a/content/docs/wallets/web/verify.mdx +++ b/content/docs/wallets/web/verify.mdx @@ -34,41 +34,58 @@ How your wallet should read `verifyContext` depends on how the dApp connects: - **Web dApp over QR code or deep link (relay):** This is the full Verify API, including scam checks. Show all four states: `VALID` as Domain match, `UNKNOWN` as Cannot verify, `INVALID` as Mismatch, and `isScam` as Security risk (ideally blocking or requiring the user to confirm). - **Native dApp over relay:** Native apps can't create a Verify attestation, so `UNKNOWN` is expected here. We suggest a neutral "Cannot verify" label rather than an alarming warning. +- **[Link Mode](/wallets/features/link-mode) (native to native):** The domain check above applies, with no scam check. `INVALID` means the dApp's universal link domain doesn't match its `metadata.url`. The wallet's own Link Mode setup also needs to be complete, or the result falls back to `INVALID`. ### Implementation -To check the Verify API validations and whether or not your user is interacting with potentially malicious app, you can do so by accessing the `verifyContext` included in the request payload. +Every `session_proposal`, `session_authenticate` and `session_request` event includes a `verifyContext`. Map it to one of the four states once, then use that state in each handler. ```javascript -... -walletKit.on("auth_request", async (authRequest) => { - const { verifyContext } = authRequest - const validation = verifyContext.verified.validation // can be VALID, INVALID or UNKNOWN - const origin = verifyContext.verified.origin // the actual verified origin of the request - const isScam = verifyContext.verified.isScam // true if the domain is flagged as malicious - - // if the domain is flagged as malicious, you should warn the user as they may lose their funds - check the `Threat` case for more info - if(isScam) { - // show a warning screen to the user - // and proceed only if the user accepts the risk - } +function getVerifyState(verifyContext) { + const { validation, isScam } = verifyContext.verified + + // Threat: only set over relay, since Link Mode has no scam check + if (isScam) return "THREAT" - switch(validation) { + switch (validation) { case "VALID": - // proceed with the request - check the `Domain match` case for more info - break + return "DOMAIN_MATCH" case "INVALID": - // show a warning dialog to the user - check the `Mismatch` case for more info - // and proceed only if the user accepts the risk - break - case "UNKNOWN": - // show a warning dialog to the user - check the `Unverified` case for more info - // and proceed only if the user accepts the risk - break + return "MISMATCH" + default: + // UNKNOWN: expected for native dApps over relay. Link Mode never returns UNKNOWN + return "UNVERIFIED" } +} + +// Session proposals always arrive over relay, from a web or native dApp +walletKit.on("session_proposal", async (proposal) => { + const state = getVerifyState(proposal.verifyContext) + // show the state on the approval screen +}) + +// One-click auth requests can arrive over relay or Link Mode +walletKit.on("session_authenticate", async (payload) => { + const state = getVerifyState(payload.verifyContext) + const isLinkMode = payload.transportType === "link_mode" + // in Link Mode, MISMATCH means the universal link domain doesn't match metadata.url +}) + +// Session requests: read the transport from the session they belong to +walletKit.on("session_request", async (request) => { + const state = getVerifyState(request.verifyContext) + const { transportType } = walletKit.getActiveSessions()[request.topic] + // show the state on the signing screen }) ``` +Present each state like this: + +- `THREAT`: Block the request, or require the user to explicitly accept the risk. Check before anything else, so the user doesn't also see a Mismatch warning. +- `MISMATCH`: Show a warning and proceed only if the user accepts the risk. +- `UNVERIFIED`: Show a neutral "Cannot verify" label. Don't block the request, since native dApps over relay always land here. +- `DOMAIN_MATCH`: Show a verified badge and proceed as normal. + For live demo examples of the intended Verify API flows, check out our demo apps: - [Demo Wallet](https://react-wallet.walletconnect.com) diff --git a/content/navigation.json b/content/navigation.json index aca273b..b9a19b8 100644 --- a/content/navigation.json +++ b/content/navigation.json @@ -61,6 +61,7 @@ "group": "More", "pages": [ "wallets/more/best-practices", + "wallets/more/push-notifications", "wallets/more/updating-wallet-sdk", { "group": "Web3Wallet to Wallet SDK", @@ -538,6 +539,27 @@ ] } ] + }, + { + "tab": "Wallet Verification", + "icon": "shield-check", + "groups": [ + { + "group": "Get Started", + "pages": [ + "wallet-verification/overview", + "wallet-verification/quickstart" + ] + }, + { + "group": "Integration", + "pages": [ + "wallet-verification/dashboard", + "wallet-verification/hosted-page", + "wallet-verification/attestations" + ] + } + ] } ], "global": { diff --git a/package.json b/package.json index 96cbdec..417d130 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "katex": "^0.19.0", "lucide-react": "^1.55.0", "mermaid": "^12.1.0", - "next": "16.4.0", + "next": "16.4.3", "next-themes": "^0.4.6", "posthog-js": "^1.438.7", "react": "^19.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8aa60f..c99f35f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,16 +13,16 @@ importers: version: 0.4.0 fumadocs-core: specifier: 16.17.2 - version: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) + version: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) fumadocs-mdx: specifier: 15.4.7 - version: 15.4.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(supports-color@10.2.2) + version: 15.4.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(supports-color@10.2.2) fumadocs-openapi: specifier: ^12.4.2 - version: 12.4.2(1b94ac6a67d8255aa4f7d7afd3202252) + version: 12.4.2(458d4ec720fb4a3ee9b9e4310bdcfa54) fumadocs-ui: specifier: npm:@fumadocs/base-ui@16.17.2 - version: '@fumadocs/base-ui@16.17.2(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(tailwindcss@4.3.3)' + version: '@fumadocs/base-ui@16.17.2(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(tailwindcss@4.3.3)' katex: specifier: ^0.19.0 version: 0.19.0 @@ -33,8 +33,8 @@ importers: specifier: ^12.1.0 version: 12.1.0 next: - specifier: 16.4.0 - version: 16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + specifier: 16.4.3 + version: 16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -693,57 +693,57 @@ packages: resolution: {integrity: sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==} engines: {node: '>=22.12.0'} - '@next/env@16.4.0': - resolution: {integrity: sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==} + '@next/env@16.4.3': + resolution: {integrity: sha512-a7tz1QCcWWkwC/rPJcPMJk/UNRu9CFhDd3OHYE5hddbJrs8BqRTalxI6GlKCWEGzRLHN+S/B7a4PAHCtm25aBQ==} - '@next/swc-darwin-arm64@16.4.0': - resolution: {integrity: sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==} + '@next/swc-darwin-arm64@16.4.3': + resolution: {integrity: sha512-4yq5EXBt+VDtAZwTyfijaFp5D2C5oz3Ikw5a2kiX2K9nw72vrZX2q5UuAlkPtUMO1tQz6JTKpaKWjS7OOefTDQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.4.0': - resolution: {integrity: sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==} + '@next/swc-darwin-x64@16.4.3': + resolution: {integrity: sha512-1Rvkn/LUhnEF8WxnRkOrqGhbwXui1bkWFxONKIbN+r7C+9KHDSZ+iUoj4CzymVMWUyZIyx9szcb9+kbKhD500A==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.4.0': - resolution: {integrity: sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==} + '@next/swc-linux-arm64-gnu@16.4.3': + resolution: {integrity: sha512-dCKpXErGg3abUM6t5JTJV/ottVBTStJB5lRJTbgdUdQaQ+jNHiK56Hsc0YAEg/rinSh42hYG3HWiYmEMdilnxg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.4.0': - resolution: {integrity: sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==} + '@next/swc-linux-arm64-musl@16.4.3': + resolution: {integrity: sha512-WwYbhWkhYBvBtib1K4W0kL8MW2snBD2A5b3SN4vPpqalZbZs5+bQ1yzAdepVoOqrFfEW3dmREy4Zs1MrWcrFbw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.4.0': - resolution: {integrity: sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==} + '@next/swc-linux-x64-gnu@16.4.3': + resolution: {integrity: sha512-QymWvfUwDEC5/lpJOsxX5VAbyoR2D13+2Jsjz4be7gT4LPqEgdpkeLIR/HFfCLgKzoMhpJbp6I/JnogyNZHcaA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.4.0': - resolution: {integrity: sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==} + '@next/swc-linux-x64-musl@16.4.3': + resolution: {integrity: sha512-Vmjd9D7DpA4Vv3cUlr82QWgJD/BQZmns801vtC5VWHg6jCIA4a5vMKpQnfrM2pyCrp2eV0ENdUg4PlN3bAc2Yw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.4.0': - resolution: {integrity: sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==} + '@next/swc-win32-arm64-msvc@16.4.3': + resolution: {integrity: sha512-lG1pPdPLk7gTyGb00n5JIMLdBJgjb+UEhSdZvFbhUMg0DyC6z4B7RnpSKF9VCd4imkLObVbXOFA0kFCpw7yFow==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.4.0': - resolution: {integrity: sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==} + '@next/swc-win32-x64-msvc@16.4.3': + resolution: {integrity: sha512-NmQC9RkJdnPXRaMxtkCzobB3teVjqF33oSOnwDfi+XYGWWYpt+xlGK3mtktSKLbdjwaWbJu2OkkMiRFGUUncKg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2172,8 +2172,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.4.0: - resolution: {integrity: sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==} + next@16.4.3: + resolution: {integrity: sha512-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2841,14 +2841,14 @@ snapshots: optionalDependencies: '@types/react': 19.3.0 - '@fumadocs/base-ui@16.17.2(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(tailwindcss@4.3.3)': + '@fumadocs/base-ui@16.17.2(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(tailwindcss@4.3.3)': dependencies: '@base-ui/react': 1.9.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@fuma-translate/react': 1.0.2(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@fumadocs/tailwind': 0.1.3(tailwindcss@4.3.3) class-variance-authority: 0.7.1 cn: 0.4.0 - fumadocs-core: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) + fumadocs-core: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) hast-util-to-jsx-runtime: 2.3.6(supports-color@10.2.2) lucide-react: 1.55.0(@types/react@19.3.0)(react@19.3.0) motion: 14.1.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -2861,7 +2861,7 @@ snapshots: optionalDependencies: '@types/mdx': 2.0.14 '@types/react': 19.3.0 - next: 16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: 16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) transitivePeerDependencies: - '@date-fns/tz' - date-fns @@ -3065,30 +3065,30 @@ snapshots: dependencies: '@chevrotain/types': 13.2.0 - '@next/env@16.4.0': {} + '@next/env@16.4.3': {} - '@next/swc-darwin-arm64@16.4.0': + '@next/swc-darwin-arm64@16.4.3': optional: true - '@next/swc-darwin-x64@16.4.0': + '@next/swc-darwin-x64@16.4.3': optional: true - '@next/swc-linux-arm64-gnu@16.4.0': + '@next/swc-linux-arm64-gnu@16.4.3': optional: true - '@next/swc-linux-arm64-musl@16.4.0': + '@next/swc-linux-arm64-musl@16.4.3': optional: true - '@next/swc-linux-x64-gnu@16.4.0': + '@next/swc-linux-x64-gnu@16.4.3': optional: true - '@next/swc-linux-x64-musl@16.4.0': + '@next/swc-linux-x64-musl@16.4.3': optional: true - '@next/swc-win32-arm64-msvc@16.4.0': + '@next/swc-win32-arm64-msvc@16.4.3': optional: true - '@next/swc-win32-x64-msvc@16.4.0': + '@next/swc-win32-x64-msvc@16.4.3': optional: true '@poppinss/colors@4.1.6': @@ -3919,7 +3919,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6): + fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6): dependencies: '@fumari/image-size': 0.1.1 estree-util-value-to-estree: 3.5.0 @@ -3948,21 +3948,21 @@ snapshots: '@types/mdast': 4.0.4 '@types/react': 19.3.0 lucide-react: 1.55.0(@types/react@19.3.0)(react@19.3.0) - next: 16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: 16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: 19.3.0 react-dom: 19.3.0(react@19.3.0) zod: 4.6.6 transitivePeerDependencies: - supports-color - fumadocs-mdx@15.4.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(supports-color@10.2.2): + fumadocs-mdx@15.4.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(supports-color@10.2.2): dependencies: '@mdx-js/mdx': 3.1.1(supports-color@10.2.2) '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.2 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) + fumadocs-core: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) github-slugger: 2.0.0 magic-string: 1.4.3 mdast-util-mdx: 3.0.0(supports-color@10.2.2) @@ -3980,12 +3980,12 @@ snapshots: '@types/mdast': 4.0.4 '@types/mdx': 2.0.14 '@types/react': 19.3.0 - next: 16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: 16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: 19.3.0 transitivePeerDependencies: - supports-color - fumadocs-openapi@12.4.2(1b94ac6a67d8255aa4f7d7afd3202252): + fumadocs-openapi@12.4.2(458d4ec720fb4a3ee9b9e4310bdcfa54): dependencies: '@base-ui/react': 1.9.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@fuma-translate/react': 1.0.2(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -3997,8 +3997,8 @@ snapshots: chokidar: 5.0.0 class-variance-authority: 0.7.1 cn: 0.4.0 - fumadocs-core: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) - fumadocs-ui: '@fumadocs/base-ui@16.17.2(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(tailwindcss@4.3.3)' + fumadocs-core: 16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6) + fumadocs-ui: '@fumadocs/base-ui@16.17.2(@types/mdx@2.0.14)(@types/react@19.3.0)(fumadocs-core@16.17.2(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.3.0)(lucide-react@1.55.0(@types/react@19.3.0)(react@19.3.0))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(zod@4.6.6))(next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(tailwindcss@4.3.3)' github-slugger: 2.0.0 hast-util-to-jsx-runtime: 2.3.6(supports-color@10.2.2) lucide-react: 1.55.0(@types/react@19.3.0)(react@19.3.0) @@ -4775,9 +4775,9 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - next@16.4.0(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + next@16.4.3(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@next/env': 16.4.0 + '@next/env': 16.4.3 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.28 caniuse-lite: 1.0.30001815 @@ -4786,14 +4786,14 @@ snapshots: react-dom: 19.3.0(react@19.3.0) styled-jsx: 5.1.6(react@19.3.0) optionalDependencies: - '@next/swc-darwin-arm64': 16.4.0 - '@next/swc-darwin-x64': 16.4.0 - '@next/swc-linux-arm64-gnu': 16.4.0 - '@next/swc-linux-arm64-musl': 16.4.0 - '@next/swc-linux-x64-gnu': 16.4.0 - '@next/swc-linux-x64-musl': 16.4.0 - '@next/swc-win32-arm64-msvc': 16.4.0 - '@next/swc-win32-x64-msvc': 16.4.0 + '@next/swc-darwin-arm64': 16.4.3 + '@next/swc-darwin-x64': 16.4.3 + '@next/swc-linux-arm64-gnu': 16.4.3 + '@next/swc-linux-arm64-musl': 16.4.3 + '@next/swc-linux-x64-gnu': 16.4.3 + '@next/swc-linux-x64-musl': 16.4.3 + '@next/swc-win32-arm64-msvc': 16.4.3 + '@next/swc-win32-x64-msvc': 16.4.3 sharp: 0.35.5(@types/node@26.6.5) transitivePeerDependencies: - '@babel/core' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1e79a01..03c2869 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,3 +8,13 @@ minimumReleaseAgeExclude: - fumadocs-openapi@12.4.2 - miniflare@5.20261006.2-beta - wrangler@4.150.0 + - '@next/env@16.4.3' + - '@next/swc-darwin-arm64@16.4.3' + - '@next/swc-darwin-x64@16.4.3' + - '@next/swc-linux-arm64-gnu@16.4.3' + - '@next/swc-linux-arm64-musl@16.4.3' + - '@next/swc-linux-x64-gnu@16.4.3' + - '@next/swc-linux-x64-musl@16.4.3' + - '@next/swc-win32-arm64-msvc@16.4.3' + - '@next/swc-win32-x64-msvc@16.4.3' + - next@16.4.3 diff --git a/public/images/push-url.png b/public/images/push-url.png new file mode 100644 index 0000000..fffce96 Binary files /dev/null and b/public/images/push-url.png differ diff --git a/public/images/wallet-verification/connect-wallet.png b/public/images/wallet-verification/connect-wallet.png new file mode 100644 index 0000000..9735f3c Binary files /dev/null and b/public/images/wallet-verification/connect-wallet.png differ diff --git a/public/images/wallet-verification/dashboard-activity.png b/public/images/wallet-verification/dashboard-activity.png new file mode 100644 index 0000000..9e7ea21 Binary files /dev/null and b/public/images/wallet-verification/dashboard-activity.png differ diff --git a/public/images/wallet-verification/dashboard-api-keys.png b/public/images/wallet-verification/dashboard-api-keys.png new file mode 100644 index 0000000..85d08bf Binary files /dev/null and b/public/images/wallet-verification/dashboard-api-keys.png differ diff --git a/public/images/wallet-verification/dashboard-customization.png b/public/images/wallet-verification/dashboard-customization.png new file mode 100644 index 0000000..02181f6 Binary files /dev/null and b/public/images/wallet-verification/dashboard-customization.png differ diff --git a/public/images/wallet-verification/personal-details.png b/public/images/wallet-verification/personal-details.png new file mode 100644 index 0000000..761dedc Binary files /dev/null and b/public/images/wallet-verification/personal-details.png differ diff --git a/public/images/wallet-verification/review.png b/public/images/wallet-verification/review.png new file mode 100644 index 0000000..d29c974 Binary files /dev/null and b/public/images/wallet-verification/review.png differ diff --git a/public/images/wallet-verification/verified.png b/public/images/wallet-verification/verified.png new file mode 100644 index 0000000..7943717 Binary files /dev/null and b/public/images/wallet-verification/verified.png differ