From 3c9c8a578b0f7e0043940f738a4cde0d5084e9bd Mon Sep 17 00:00:00 2001 From: oneshot Date: Wed, 29 Apr 2026 09:51:58 +0000 Subject: [PATCH 1/2] docs: add developer quickstart, tutorials, cookbook, and architecture --- developer-sidebars.js | 49 +++- developer/architecture.md | 171 +++++++++++ developer/cookbook/batch-operations.md | 125 +++++++++ developer/cookbook/delegation.md | 110 ++++++++ developer/cookbook/environments.md | 78 ++++++ developer/cookbook/error-handling.md | 129 +++++++++ developer/cookbook/extension-deeplinks.md | 96 +++++++ developer/cookbook/indexer-queries.md | 128 +++++++++ developer/cookbook/oracle-rates.md | 125 +++++++++ developer/cookbook/prepared-transactions.md | 125 +++++++++ developer/cookbook/referral-fees.md | 123 ++++++++ developer/cookbook/taker-tiers.md | 79 ++++++ developer/quickstart.md | 296 ++++++++++++++++++++ developer/sdk/client-reference.md | 71 ++++- developer/tutorials/maker-bot.md | 293 +++++++++++++++++++ developer/tutorials/onramp-widget.md | 277 ++++++++++++++++++ developer/tutorials/vault-dashboard.md | 279 ++++++++++++++++++ developer/use-cases.md | 75 +++++ 18 files changed, 2622 insertions(+), 7 deletions(-) create mode 100644 developer/architecture.md create mode 100644 developer/cookbook/batch-operations.md create mode 100644 developer/cookbook/delegation.md create mode 100644 developer/cookbook/environments.md create mode 100644 developer/cookbook/error-handling.md create mode 100644 developer/cookbook/extension-deeplinks.md create mode 100644 developer/cookbook/indexer-queries.md create mode 100644 developer/cookbook/oracle-rates.md create mode 100644 developer/cookbook/prepared-transactions.md create mode 100644 developer/cookbook/referral-fees.md create mode 100644 developer/cookbook/taker-tiers.md create mode 100644 developer/quickstart.md create mode 100644 developer/tutorials/maker-bot.md create mode 100644 developer/tutorials/onramp-widget.md create mode 100644 developer/tutorials/vault-dashboard.md create mode 100644 developer/use-cases.md diff --git a/developer-sidebars.js b/developer-sidebars.js index 0695579..ddf92ed 100644 --- a/developer-sidebars.js +++ b/developer-sidebars.js @@ -1,13 +1,50 @@ module.exports = { defaultSidebar: [ + { type: 'doc', id: 'quickstart', label: 'Quickstart' }, + { type: 'doc', id: 'architecture', label: 'Architecture' }, + { type: 'doc', id: 'use-cases', label: 'What Can You Build?' }, { - type: 'doc', - id: 'integrate-zkp2p/integrate-redirect-onramp', - label: 'Onramp Integration', + type: 'category', + label: 'Integration Guides', + collapsed: false, + items: [ + { + type: 'doc', + id: 'integrate-zkp2p/integrate-redirect-onramp', + label: 'Onramp Integration', + }, + { type: 'doc', id: 'offramp-integration', label: 'Offramp Integration' }, + { type: 'doc', id: 'post-intent-hooks', label: 'Intent Hooks' }, + { type: 'doc', id: 'build-payment-integration', label: 'Build a Payment Integration' }, + ], + }, + { + type: 'category', + label: 'Tutorials', + collapsed: false, + items: [ + { type: 'doc', id: 'tutorials/onramp-widget', label: 'Build an Onramp Widget' }, + { type: 'doc', id: 'tutorials/maker-bot', label: 'Build a Maker Bot' }, + { type: 'doc', id: 'tutorials/vault-dashboard', label: 'Build a Vault Dashboard' }, + ], + }, + { + type: 'category', + label: 'Cookbook', + collapsed: false, + items: [ + { type: 'doc', id: 'cookbook/prepared-transactions', label: 'Prepared Transactions' }, + { type: 'doc', id: 'cookbook/referral-fees', label: 'Referral Fees & Attribution' }, + { type: 'doc', id: 'cookbook/oracle-rates', label: 'Oracle Rate Configuration' }, + { type: 'doc', id: 'cookbook/batch-operations', label: 'Batch Operations' }, + { type: 'doc', id: 'cookbook/error-handling', label: 'Error Handling & Retries' }, + { type: 'doc', id: 'cookbook/environments', label: 'Multi-Environment Deployment' }, + { type: 'doc', id: 'cookbook/indexer-queries', label: 'Indexer Pagination & Filtering' }, + { type: 'doc', id: 'cookbook/extension-deeplinks', label: 'Extension Deeplinks' }, + { type: 'doc', id: 'cookbook/taker-tiers', label: 'Taker Tiers' }, + { type: 'doc', id: 'cookbook/delegation', label: 'Delegation State Machine' }, + ], }, - { type: 'doc', id: 'offramp-integration', label: 'Offramp Integration' }, - { type: 'doc', id: 'post-intent-hooks', label: 'Intent Hooks' }, - { type: 'doc', id: 'build-payment-integration', label: 'Build a Payment Integration' }, { type: 'category', label: 'SDK Reference', diff --git a/developer/architecture.md b/developer/architecture.md new file mode 100644 index 0000000..b33654b --- /dev/null +++ b/developer/architecture.md @@ -0,0 +1,171 @@ +--- +id: architecture +title: Architecture Overview +slug: /architecture +--- + +# Architecture Overview + +## What this does + +This page explains how the SDK fits into the rest of the ZKP2P stack so you can decide which surfaces belong in your app, which live off-chain, and which are enforced on-chain. + +## Who is this for? + +Read this before you build a production integration, especially if you need custom quote routing, analytics, vaults, smart-account support, or your own fulfillment UX. + +## System diagram + +```text + historical queries, analytics + ┌────────────────────────────────────────────┐ + │ Indexer │ + │ GraphQL: deposits, intents, vaults │ + └──────────────────────▲──────────────────────┘ + │ + │ client.indexer.* + │ +┌─────────────────────────────┐ quotes, payee registration, gating ┌──────────────────────────┐ +│ Your App │◀──────────────────────────────────────────────▶│ Curator API │ +│ React app / Node service │ │ /v2 quote / /v3 intent │ +│ Zkp2pClient / hooks / │ │ seller credential proxy │ +│ peerExtensionSdk │ └──────────────────────────┘ +└──────────────┬──────────────┘ + │ + │ RPC-first reads + write transactions + ▼ +┌─────────────────────────────┐ +│ ProtocolViewer + RPC │ +│ getDeposits/getIntent/etc. │ +└──────────────┬──────────────┘ + │ + │ signal / cancel / fulfill + ▼ +┌─────────────────────────────┐ verify attestation ┌──────────────────────────┐ +│ OrchestratorV2 │─────────────────────────────▶│ UnifiedPaymentVerifierV2 │ +│ intent lifecycle + fees │ └──────────────────────────┘ +└──────────────┬──────────────┘ + │ lock / unlock / transfer + ▼ +┌─────────────────────────────┐ delegated rates ┌──────────────────────────┐ +│ EscrowV2 │◀────────────────────────────▶│ RateManagerV1 │ +│ deposits + custody + floors │ │ vault / manager pricing │ +└──────────────┬──────────────┘ └──────────────────────────┘ + ▲ + │ proof capture / onramp UX + │ +┌─────────────────────────────┐ proof -> attestation ┌──────────────────────────┐ +│ PeerAuth / Peer app │─────────────────────────────▶│ Attestation Service │ +│ browser extension / mobile │◀─────────────────────────────│ returns EIP-712 payload │ +└─────────────────────────────┘ └──────────────────────────┘ +``` + +## Component roles + +### Your app + +Your app owns the product UX: when to show quotes, how to collect destination addresses, what to do after a fill, and whether you want a fully embedded flow or a handoff into the Peer extension. + +### `@zkp2p/sdk` + +The SDK is the main integration layer. Use `Zkp2pClient` for reads, writes, quote APIs, vault operations, and indexer access. Use `@zkp2p/sdk/react` for transaction-state hooks and `peerExtensionSdk` for extension-side onramp flows. + +### ProtocolViewer + +The SDK is RPC-first for primary reads. Methods like `getDeposits()`, `getDeposit()`, `getIntents()`, and `getIntent()` read live protocol state through ProtocolViewer and the current escrow/orchestrator contracts instead of waiting for indexer sync. + +### Curator API + +Curator is the main off-chain coordination layer for quotes, payee-detail registration, quote enrichment, gating signatures, and seller credential APIs. If you only need live on-chain state, you can avoid it. If you need routing, quote search, or authenticated enrichment, you will use it. + +### Indexer + +The indexer is for history, pagination, filtering, vault analytics, and timeline-style queries. It is eventually consistent. Use it for dashboards and bots, not for the single source of truth immediately before sending a transaction. + +### EscrowV2 + +EscrowV2 holds deposit liquidity, tracks supported payment methods and currencies, stores fixed floors and oracle configs, and enforces delegated rate-manager settings. It does not verify off-chain payment proofs or own the intent lifecycle. + +### OrchestratorV2 + +OrchestratorV2 owns intent lifecycle actions such as signal, cancel, and fulfill. It snapshots fees, runs pre-intent hooks, asks EscrowV2 to lock and release funds, and routes verification through the unified verifier. + +### UnifiedPaymentVerifierV2 + +This contract validates the attestation payload that comes back from the Attestation Service. It checks the proof snapshot against the signaled intent, prevents replay, and caps the release amount to what was reserved on-chain. + +### Attestation Service + +The attestation service is the off-chain proof verifier. It accepts the zkTLS or PeerAuth proof, validates it, and returns the EIP-712 payload that `fulfillIntent()` submits on-chain. + +### PeerAuth / Peer extension + +PeerAuth is the proof-capture surface for end users. In embedded onramp flows it can also handle the UX for payment submission, fulfillment callbacks, and optional bridge tracking. + +### RateManagerV1 + +Vaults are exposed through rate managers. A manager can set rates on behalf of delegated deposits and charge a fee on filled intents. EscrowV2 always treats the depositor floor as the minimum and only lets a manager raise the effective rate above it. + +## Data flow: onramp / taker flow + +1. Your app collects `amount`, `fiatCurrency`, destination chain, destination token, and recipient. +2. Your app calls `client.getQuote()` or `client.getQuotesBestByPlatform()` to find live liquidity. +3. The user picks a quote. Your app calls `client.signalIntent()` or opens the Peer extension with `peerExtensionSdk.onramp()`. +4. OrchestratorV2 records the intent and asks EscrowV2 to lock the matching liquidity. +5. The user pays fiat off-chain and produces a proof in PeerAuth or the extension. +6. Your app or the extension calls `client.fulfillIntent()` with that proof. +7. The SDK sends the proof to the Attestation Service, gets an EIP-712 payload back, and submits the final on-chain transaction. +8. UnifiedPaymentVerifierV2 validates the attestation. OrchestratorV2 distributes protocol, referral, and manager fees, then releases the remaining funds to the taker. +9. If a bridge or swap is part of the destination path, the extension reports `bridge.status` so your app can keep tracking delivery. + +## Data flow: maker / liquidity flow + +1. A maker initializes `Zkp2pClient`, ensures allowance with `ensureAllowance()`, and optionally registers reusable payee details with `registerPayeeDetails()`. +2. The maker creates a deposit with `createDeposit()` and configures payment methods, fixed floors, optional oracle configs, and optional delegation. +3. EscrowV2 stores the deposit. The indexer backfills the deposit for later search and analytics. +4. Takers discover the deposit through curator quotes or indexer-based discovery. +5. When a taker signals an intent, OrchestratorV2 locks the matched liquidity on EscrowV2. +6. The maker watches signaled intents through `client.indexer.getIntentsForDeposits()` and uses their own off-chain logic to watch for incoming fiat. +7. The taker fulfills by submitting a payment proof. If the payment is bad or the intent is stale, the maker can reject or clean it up with `releaseFundsToPayer()` or `pruneExpiredIntents()`. +8. The maker later tops up with `addFunds()`, pauses with `setAcceptingIntents()`, or exits with `withdrawDeposit()`. + +## Rate determination + +For EscrowV2 deposits, the effective rate is: + +```text +escrowFloor = max(fixedRate, oracleRate) +effectiveRate = max(escrowFloor, delegatedRate) +``` + +Example: + +- fixed floor: `1.00` +- oracle-derived floor: `1.03` +- delegated vault rate: `1.01` +- effective rate used on-chain: `1.03` + +If the oracle goes stale, its value is treated as `0`, so the rate falls back to the fixed floor and then to any delegated rate above that floor. + +## Environments + +| `runtimeEnv` | When to use it | Notes | +| --- | --- | --- | +| `production` | Real user traffic | Default SDK environment | +| `preproduction` | Integration testing against production-style services | Useful before a mainnet launch | +| `staging` | Development and rehearsals | SDK defaults skew toward the current staging deployment | + +Use `getContracts(chainId, env)` when you want to inspect the exact addresses and ABIs behind the client. + +## Common integration decisions + +- Use RPC-first reads for "can I submit this transaction right now?" +- Use `client.indexer.*` for search, pagination, vault stats, and historical reporting +- Use `peerExtensionSdk.onramp()` when you want the fastest embedded funding UX +- Use `signalIntent.prepare()` and the other prepared-transaction methods for smart accounts and relayers + +## Troubleshooting + +- `getDeposits()` is empty but public liquidity exists: `getDeposits()` only returns deposits owned by the connected wallet. Use `client.indexer.getDeposits()` or quote APIs for market-wide discovery +- Indexer data looks stale: confirm with RPC-first methods before you submit a transaction +- `fulfillIntent()` seems to do two steps: that is expected. It talks to the Attestation Service first, then submits the final on-chain transaction diff --git a/developer/cookbook/batch-operations.md b/developer/cookbook/batch-operations.md new file mode 100644 index 0000000..f568328 --- /dev/null +++ b/developer/cookbook/batch-operations.md @@ -0,0 +1,125 @@ +--- +id: batch-operations +title: Batch Operations +--- + +# Batch Operations + +## What this covers + +How to update many rate, oracle, currency, or delegation settings in one pass. + +## When to use this + +Use batch operations when a bot or vault operator needs to move several pairs together without N separate admin transactions. + +## Set many vault floor rates at once + +```ts +import { + resolveFiatCurrencyBytes32, + resolvePaymentMethodHash, +} from '@zkp2p/sdk'; + +const wise = resolvePaymentMethodHash('wise', { env: 'production' }); +const revolut = resolvePaymentMethodHash('revolut', { env: 'production' }); +const USD = resolveFiatCurrencyBytes32('USD'); +const EUR = resolveFiatCurrencyBytes32('EUR'); + +await client.setVaultMinRatesBatch({ + rateManagerId, + paymentMethods: [wise, revolut], + currencies: [[USD, EUR], [EUR]], + rates: [[ + 1_010000000000000000n, + 940000000000000000n, + ], [ + 938000000000000000n, + ]], +}); +``` + +## Batch-update currency config + +`updateCurrencyConfigBatch()` lets you change fixed floors and oracle settings together. + +```ts +await client.updateCurrencyConfigBatch({ + depositId: 42n, + paymentMethods: [wise], + updates: [[ + { + code: USD, + minConversionRate: 1_010000000000000000n, + updateOracle: true, + oracleRateConfig: { + adapter: usdOracle.adapter, + adapterConfig: usdOracle.adapterConfig, + spreadBps: -50, + maxStaleness: usdOracle.maxStaleness, + }, + }, + { + code: EUR, + minConversionRate: 940000000000000000n, + updateOracle: false, + oracleRateConfig: { + adapter: usdOracle.adapter, + adapterConfig: usdOracle.adapterConfig, + spreadBps: 0, + maxStaleness: usdOracle.maxStaleness, + }, + }, + ]], +}); +``` + +## Disable several currencies in one call + +```ts +await client.deactivateCurrenciesBatch({ + depositId: 42n, + paymentMethods: [wise, revolut], + currencyCodes: [[USD, EUR], [EUR]], +}); +``` + +## Batch delegation with a smart account + +`useVaultDelegation()` supports multi-call delegation through `sendBatch`. + +```tsx +import { useVaultDelegation } from '@zkp2p/sdk/react'; + +const { delegateDeposits, clearDelegations } = useVaultDelegation({ + client, + sendTransaction, + sendBatch: async (calls) => { + return smartAccount.sendUserOperation({ calls }); + }, +}); + +await delegateDeposits({ + registry: vaultAddress, + rateManagerId, + deposits: [ + { + compositeDepositId: '0xescrow_12', + escrow: '0xescrow', + depositId: 12n, + }, + { + compositeDepositId: '0xescrow_19', + escrow: '0xescrow', + depositId: 19n, + }, + ], +}); +``` + +## Key points + +- Group nested arrays by payment method index +- Batch methods are best paired with bots or admin tooling, not one-off manual UX +- `useVaultDelegation({ sendBatch })` is the easiest path for smart-account delegation switches +- If you only need one pair, prefer the single-item method for simpler failure handling diff --git a/developer/cookbook/delegation.md b/developer/cookbook/delegation.md new file mode 100644 index 0000000..3ed37cf --- /dev/null +++ b/developer/cookbook/delegation.md @@ -0,0 +1,110 @@ +--- +id: delegation +title: Delegation State Machine +--- + +# Delegation State Machine + +## What this covers + +How to reason about deposit delegation, when to clear an existing vault assignment, and how the React helper automates the current EscrowV2 path. + +## When to use this + +Use this when you are building vault tooling, delegation UIs, or any workflow where a depositor can switch a deposit between managers. + +## The current route + +At the hook-utility level, the stable route is direct EscrowV2 delegation: + +```ts +import { getDelegationRoute } from '@zkp2p/sdk'; + +const route = getDelegationRoute(client, escrowAddress); +console.log(route); // 'v2' +``` + +Low-level compatibility methods such as `setDepositRateManager()` still exist on the client, but `useVaultDelegation()` and the exported routing helpers currently target the direct V2 path. + +## Classify the current state + +```ts +import { classifyDelegationState } from '@zkp2p/sdk'; + +const state = classifyDelegationState( + currentRateManagerId, + currentRegistry, + targetRateManagerId, + targetRegistry, +); + +// 'not_delegated' | 'delegated_here' | 'delegated_elsewhere' +``` + +How to interpret it: + +- `not_delegated`: safe to delegate immediately +- `delegated_here`: already on the correct vault, so skip the write +- `delegated_elsewhere`: switch required + +## Delegate one deposit + +```tsx +import { useVaultDelegation } from '@zkp2p/sdk/react'; + +const { delegateDeposit, clearDelegation } = useVaultDelegation({ + client, + sendTransaction: async ({ to, data, value }) => { + return walletClient.sendTransaction({ to, data, value, account }); + }, +}); + +await delegateDeposit({ + escrow: '0xEscrowAddress', + depositId: 42n, + registry: '0xVaultAddress', + rateManagerId: '0xRateManagerId', + currentRateManagerId, + currentRateManagerRegistry: currentRegistry, +}); +``` + +## Switching vaults + +If a deposit is already delegated elsewhere: + +- with `sendBatch`: `useVaultDelegation()` can batch clear + set into one smart-account operation +- without `sendBatch`: clear first, then delegate in a second step + +```tsx +const { delegateDeposit } = useVaultDelegation({ + client, + sendTransaction, + sendBatch: async (calls) => smartAccount.sendUserOperation({ calls }), +}); +``` + +## Useful helpers + +```ts +import { + ZERO_RATE_MANAGER_ID, + isZeroRateManagerId, + normalizeRateManagerId, + normalizeRegistry, +} from '@zkp2p/sdk'; + +console.log(ZERO_RATE_MANAGER_ID); +console.log(isZeroRateManagerId(currentRateManagerId)); +console.log(normalizeRateManagerId(currentRateManagerId)); +console.log(normalizeRegistry(currentRegistry)); +``` + +These helpers are useful when your UI stores values from several sources and you want one canonical comparison format. + +## Key points + +- In the current SDK, the public routing helpers resolve to direct EscrowV2 delegation +- `delegated_elsewhere` is the state that requires a clear-first path unless you batch +- `useVaultDelegation()` is the easiest integration surface because it handles skips, clears, and smart-account batching logic for you +- Keep delegation comparisons normalized; address casing differences should not produce false state changes diff --git a/developer/cookbook/environments.md b/developer/cookbook/environments.md new file mode 100644 index 0000000..a1a9f2e --- /dev/null +++ b/developer/cookbook/environments.md @@ -0,0 +1,78 @@ +--- +id: environments +title: Multi-Environment Deployment +--- + +# Multi-Environment Deployment + +## What this covers + +How to switch the SDK between production, preproduction, and staging without rewriting your app code. + +## When to use this + +Use this when you want one codebase for local development, integration testing, and mainnet launch readiness. + +## Environment selection + +| `runtimeEnv` | Best use | Notes | +| --- | --- | --- | +| `production` | Real users and live liquidity | Default | +| `preproduction` | Integration testing with production-style services | Good pre-launch step | +| `staging` | Development and rehearsal environments | Useful for SDK and contract drills | + +## Initialize from env vars + +```ts +import { Zkp2pClient, getContracts } from '@zkp2p/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createWalletClient, http } from 'viem'; +import { base } from 'viem/chains'; + +const runtimeEnv = + process.env.RUNTIME_ENV === 'staging' + ? 'staging' + : process.env.RUNTIME_ENV === 'preproduction' + ? 'preproduction' + : 'production'; + +const walletClient = createWalletClient({ + account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`), + chain: base, + transport: http(process.env.RPC_URL), +}); + +const client = new Zkp2pClient({ + walletClient, + chainId: base.id, + runtimeEnv, + indexerUrl: process.env.INDEXER_URL, + baseApiUrl: process.env.BASE_API_URL, + indexerApiKey: process.env.INDEXER_API_KEY, +}); + +const { addresses } = getContracts(base.id, runtimeEnv); +console.log(addresses.escrow, addresses.orchestratorV2); +``` + +## Override service URLs + +You only need overrides when you are pointing at a non-standard deployment or a proxy layer you control. + +- `indexerUrl`: custom GraphQL endpoint +- `baseApiUrl`: custom curator or API host +- `indexerApiKey`: `x-api-key` header for indexer proxy auth +- `authorizationToken` / `getAuthorizationToken`: bearer auth for long-lived clients + +## A practical rollout pattern + +1. Develop locally against `staging` +2. Run pre-launch integration tests against `preproduction` +3. Flip the same app code to `production` once you are ready for real users + +## Key points + +- The SDK environment controls contract resolution and default service endpoints +- `getContracts(chainId, env)` is the safest way to verify the addresses you are about to use +- Keep overrides in env vars instead of hard-coding them inside app logic +- If your app has both backend and frontend clients, keep their `runtimeEnv` values aligned diff --git a/developer/cookbook/error-handling.md b/developer/cookbook/error-handling.md new file mode 100644 index 0000000..9813e3e --- /dev/null +++ b/developer/cookbook/error-handling.md @@ -0,0 +1,129 @@ +--- +id: error-handling +title: Error Handling & Retries +--- + +# Error Handling & Retries + +## What this covers + +How to branch on SDK error classes, surface actionable messages to users, and retry transient failures without hiding contract bugs. + +## When to use this + +Use this pattern in any app that sends real transactions or talks to the quote and indexer APIs in production. + +## Handle the SDK error model + +```ts +import { + APIError, + ContractError, + ErrorCode, + NetworkError, + ValidationError, + ZKP2PError, +} from '@zkp2p/sdk'; + +try { + await client.createDeposit({ + token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + amount: 1_000_000000n, + intentAmountRange: { min: 25_000000n, max: 250_000000n }, + processorNames: ['wise'], + payeeData: [{ offchainId: 'maker@example.com' }], + conversionRates: [[ + { currency: 'USD', conversionRate: '1015000000000000000' }, + ]], + }); +} catch (error) { + if (error instanceof ValidationError) { + console.error('validation', error.field, error.message, error.details); + } else if (error instanceof APIError) { + console.error('api', error.status, error.message, error.details); + } else if (error instanceof NetworkError) { + console.error('network', error.message); + } else if (error instanceof ContractError) { + console.error('contract', error.message, error.details); + } else if (error instanceof ZKP2PError) { + console.error('sdk', error.code ?? ErrorCode.UNKNOWN, error.message); + } else { + console.error('unknown', error); + } +} +``` + +## Retry only the transient failures + +```ts +async function withNetworkRetry(fn: () => Promise, attempts = 3): Promise { + let lastError: unknown; + + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + return await fn(); + } catch (error) { + lastError = error; + + if (!(error instanceof NetworkError) || attempt === attempts) { + throw error; + } + + const delayMs = 250 * 2 ** (attempt - 1); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + + throw lastError; +} + +const quote = await withNetworkRetry(() => + client.getQuote({ + paymentPlatforms: ['wise'], + fiatCurrency: 'USD', + user: buyerAddress, + recipient: buyerAddress, + destinationChainId: 8453, + destinationToken: usdc, + amount: '50', + isExactFiat: true, + }), +); +``` + +## Contract errors need user action + +Contract failures usually mean the inputs were valid but the on-chain state changed underneath you. + +```ts +try { + await client.addFunds({ + depositId: 42n, + amount: 500_000000n, + }); +} catch (error) { + if (error instanceof ContractError) { + throw new Error( + 'The transaction simulated but failed on-chain. Check allowance, token balance, and whether the deposit still exists.', + ); + } + throw error; +} +``` + +## Turn on debug logging when you need it + +```ts +import { setLogLevel } from '@zkp2p/sdk'; + +setLogLevel('debug'); +``` + +Use `debug` in development or during incident response, then move back to `info` or `error`. + +## Key points + +- `ValidationError.field` is the quickest way to map an SDK failure back to a form field +- `APIError.status` is useful for quote and seller-credential flows +- Retry `NetworkError`, not `ValidationError` or `ContractError` +- Keep one user-facing message and one structured internal log; do not leak raw proof payloads into logs diff --git a/developer/cookbook/extension-deeplinks.md b/developer/cookbook/extension-deeplinks.md new file mode 100644 index 0000000..970b4be --- /dev/null +++ b/developer/cookbook/extension-deeplinks.md @@ -0,0 +1,96 @@ +--- +id: extension-deeplinks +title: Extension Deeplinks +--- + +# Extension Deeplinks + +## What this covers + +How to open the Peer extension onramp with prefilled params, resume an active intent, and open common side-panel routes. + +## When to use this + +Use this when your product wants the Peer extension to own the final funding UX while your app supplies the destination, amount, and branding. + +:::note Browser requirement +`peerExtensionSdk` requires a browser window with the Peer extension installed. If you need a mobile handoff, mirror the same params in your app layer and keep the desktop path on `peerExtensionSdk.onramp()`. +::: + +## Open the onramp with prefilled params + +```ts +import { createPeerExtensionSdk } from '@zkp2p/sdk'; + +const peerSdk = createPeerExtensionSdk({ window }); + +peerSdk.onramp({ + referrer: 'Acme Wallet', + referrerLogo: 'https://acme.xyz/logo.png', + inputCurrency: 'USD', + inputAmount: '25', + paymentPlatform: 'wise', + toToken: '8453:0x0000000000000000000000000000000000000000', + recipientAddress: '0xBuyerAddress', +}); +``` + +Use: + +- `inputAmount` when the user thinks in fiat +- `amountUsdc` when you want an exact Base USDC output amount in base units +- `intentHash` when you want to reopen an already-created order at the payment step + +## Resume an active intent + +```ts +peerSdk.onramp({ + intentHash: '0x1234...abcd', +}); +``` + +This is useful if your UI stores active orders and offers a "continue payment" button. + +## Open common side-panel routes + +The extension supports direct route opens through `openSidebar(route)`. + +```ts +peerSdk.openSidebar('/buy'); +peerSdk.openSidebar('/send'); +peerSdk.openSidebar('/verify'); +peerSdk.openSidebar('/proofs'); +``` + +Use `openSidebar()` when you want a specific extension view without constructing a full onramp payload. + +## Check compatibility first + +```ts +const version = await peerSdk.getVersion(); +console.log('peer extension version:', version); +``` + +This is useful when you need to gate new UI behind a minimum extension version. + +## `toToken` format + +`toToken` is always: + +```text +chainId:tokenAddress +``` + +Examples: + +- Base ETH: `8453:0x0000000000000000000000000000000000000000` +- Base USDC: `8453:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` +- Ethereum ETH: `1:0x0000000000000000000000000000000000000000` +- Solana SOL: `792703809:11111111111111111111111111111111` + +## Key points + +- Register `onIntentFulfilled()` before calling `onramp()` +- Prefer a scoped client from `createPeerExtensionSdk({ window })` instead of relying on the default singleton +- Use `intentHash` to resume flows and `openSidebar()` to jump to a known route +- Keep mobile handoff logic outside the SDK; the SDK wrapper itself is desktop-extension-first diff --git a/developer/cookbook/indexer-queries.md b/developer/cookbook/indexer-queries.md new file mode 100644 index 0000000..18af407 --- /dev/null +++ b/developer/cookbook/indexer-queries.md @@ -0,0 +1,128 @@ +--- +id: indexer-queries +title: Indexer Pagination & Filtering +--- + +# Indexer Pagination & Filtering + +## What this covers + +How to use `client.indexer.*` for paginated deposit discovery, intent history, expired-intent cleanup, raw GraphQL, and conversion helpers. + +## When to use this + +Use the indexer when you need history, filtering, search, or vault analytics. Use RPC-first reads when you need the live on-chain source of truth right before a transaction. + +## Query deposits with filters + +```ts +const deposits = await client.indexer.getDeposits( + { + status: 'ACTIVE', + depositor: '0xMakerAddress', + acceptingIntents: true, + minLiquidity: '1000000', + }, + { + limit: 50, + offset: 0, + orderBy: 'remainingDeposits', + orderDirection: 'desc', + }, +); +``` + +Available deposit filters: + +- `status` +- `depositor` +- `delegate` +- `delegateIsSet` +- `chainId` +- `escrowAddress` +- `escrowAddresses` +- `minLiquidity` +- `acceptingIntents` + +## Include payment methods, currencies, and intents + +```ts +const depositsWithRelations = await client.indexer.getDepositsWithRelations( + { status: 'ACTIVE' }, + { limit: 20, orderBy: 'updatedAt', orderDirection: 'desc' }, + { + includeIntents: true, + intentStatuses: ['SIGNALED', 'FULFILLED'], + }, +); +``` + +## Pull intent history + +```ts +const ownerIntents = await client.indexer.getOwnerIntents(buyerAddress, [ + 'SIGNALED', + 'FULFILLED', + 'MANUAL_RELEASED', +]); + +const expired = await client.indexer.getExpiredIntents({ + now: Math.floor(Date.now() / 1000).toString(), + depositIds: ['0xescrow_12', '0xescrow_18'], + limit: 100, +}); +``` + +Composite deposit IDs are formatted as `escrowAddress_depositId`. + +## Run a raw GraphQL query + +When the flat helper namespace does not cover your exact shape yet, use `query()`: + +```ts +const data = await client.indexer.query<{ + Deposit: Array<{ id: string; remainingDeposits: string }>; +}>({ + query: ` + query DepositsByLiquidity($min: numeric!) { + Deposit(where: { remainingDeposits: { _gte: $min } }, limit: 5) { + id + remainingDeposits + } + } + `, + variables: { min: '1000000' }, +}); +``` + +## Convert indexer payloads into RPC-like views + +```ts +import { + convertDepositsForLiquidity, + convertIndexerDepositToEscrowView, + convertIndexerIntentsToEscrowViews, +} from '@zkp2p/sdk'; + +const deposits = await client.indexer.getDepositsWithRelations( + { status: 'ACTIVE' }, + { limit: 10 }, +); + +const liquidityViews = convertDepositsForLiquidity(deposits, 8453, '0xescrowAddress'); +const firstDepositView = convertIndexerDepositToEscrowView( + deposits[0], + 8453, + '0xescrowAddress', +); +const intentViews = convertIndexerIntentsToEscrowViews( + deposits.flatMap((deposit) => deposit.intents ?? []), +); +``` + +## Key points + +- Pagination is `limit` plus `offset`; there is no cursor helper in the public SDK today +- `getDepositsWithRelations()` is the best default for dashboards because it avoids N extra round trips +- `getExpiredIntents()` expects a current timestamp in seconds +- Use `query()` when you need a custom aggregate or schema-specific field that is not wrapped yet diff --git a/developer/cookbook/oracle-rates.md b/developer/cookbook/oracle-rates.md new file mode 100644 index 0000000..c797db5 --- /dev/null +++ b/developer/cookbook/oracle-rates.md @@ -0,0 +1,125 @@ +--- +id: oracle-rates +title: Oracle Rate Configuration +--- + +# Oracle Rate Configuration + +## What this covers + +How to configure oracle-backed pricing floors on EscrowV2 deposits and how that floor interacts with fixed rates and delegated vault rates. + +## When to use this + +Use oracle config when you want deposit pricing to track a market reference instead of relying only on a manually updated fixed floor. + +## Configure one payment-method and currency pair + +```ts +import { + getSpreadOracleConfig, + resolveFiatCurrencyBytes32, + resolvePaymentMethodHash, + validateOracleFeedsOnChain, +} from '@zkp2p/sdk'; + +const paymentMethodHash = resolvePaymentMethodHash('wise', { env: 'production' }); +const currencyHash = resolveFiatCurrencyBytes32('USD'); +const oracle = getSpreadOracleConfig('USD'); + +if (!oracle) { + throw new Error('No bundled oracle config for USD'); +} + +const liveFeeds = await validateOracleFeedsOnChain(client.publicClient); +if (!liveFeeds.has('USD')) { + throw new Error('USD oracle feed is not currently live'); +} + +await client.setOracleRateConfig({ + depositId: 42n, + paymentMethodHash, + currencyHash, + config: { + adapter: oracle.adapter, + adapterConfig: oracle.adapterConfig, + spreadBps: -50, + maxStaleness: oracle.maxStaleness, + }, +}); +``` + +The `config` fields are: + +- `adapter`: the deployed oracle adapter contract +- `adapterConfig`: feed-specific bytes for that adapter +- `spreadBps`: signed spread in basis points applied to the market price +- `maxStaleness`: maximum allowed age in seconds before the oracle is ignored + +## Remove oracle pricing + +```ts +await client.removeOracleRateConfig({ + depositId: 42n, + paymentMethodHash, + currencyHash, +}); +``` + +After removal, the pair falls back to fixed-rate-only behavior. + +## Batch-configure several currencies + +```ts +const usdOracle = getSpreadOracleConfig('USD'); +const eurOracle = getSpreadOracleConfig('EUR'); + +await client.setOracleRateConfigBatch({ + depositId: 42n, + paymentMethods: [paymentMethodHash], + currencies: [[ + resolveFiatCurrencyBytes32('USD'), + resolveFiatCurrencyBytes32('EUR'), + ]], + configs: [[ + { + adapter: usdOracle!.adapter, + adapterConfig: usdOracle!.adapterConfig, + spreadBps: -50, + maxStaleness: usdOracle!.maxStaleness, + }, + { + adapter: eurOracle!.adapter, + adapterConfig: eurOracle!.adapterConfig, + spreadBps: 0, + maxStaleness: eurOracle!.maxStaleness, + }, + ]], +}); +``` + +## How the final rate is chosen + +EscrowV2 computes: + +```text +escrowFloor = max(fixedRate, oracleRate) +effectiveRate = max(escrowFloor, delegatedRate) +``` + +Example: + +- fixed rate: `1.00` +- oracle-derived rate: `1.02` +- delegated vault rate: `1.01` +- final effective rate: `1.02` + +If the oracle becomes stale, its contribution becomes `0`, so the pair falls back to the fixed floor and then any delegated rate above it. + +## Key points + +- Oracle config is EscrowV2-only +- Negative `spreadBps` is allowed, but the effective multiplier must stay above zero +- `maxStaleness` is in seconds, not milliseconds +- A stale or invalid oracle does not halt a pair if a non-zero fixed rate still exists +- For background on the contract behavior, see [Escrow](/protocol/v3/smart-contracts/escrow) diff --git a/developer/cookbook/prepared-transactions.md b/developer/cookbook/prepared-transactions.md new file mode 100644 index 0000000..ae1a35c --- /dev/null +++ b/developer/cookbook/prepared-transactions.md @@ -0,0 +1,125 @@ +--- +id: prepared-transactions +title: Prepared Transactions +--- + +# Prepared Transactions + +## What this covers + +How to use the SDK's `.prepare()` pattern for relayers, smart accounts, gas estimation, and user-operation batching. + +## When to use this + +Use prepared transactions whenever your app should not call `walletClient.sendTransaction()` directly. + +## The common pattern + +Most write methods in `@zkp2p/sdk` expose a `.prepare()` variant that returns: + +```ts +type PreparedTransaction = { + to: `0x${string}`; + data: `0x${string}`; + value: bigint; + chainId: number; +}; +``` + +The main exception is `createDeposit()`, which uses `prepareCreateDeposit()`. + +## Prepare a taker transaction + +```ts +const prepared = await client.signalIntent.prepare({ + depositId: 42n, + amount: 250_000000n, + toAddress: '0xBuyerAddress', + processorName: 'wise', + payeeDetails: '0xPayeeDetailsHash', + fiatCurrencyCode: 'USD', + conversionRate: 1_020000000000000000n, +}); + +await smartAccount.sendUserOperation({ + calls: [ + { + to: prepared.to, + data: prepared.data, + value: prepared.value, + }, + ], +}); +``` + +The same pattern works for: + +- `signalIntent.prepare()` +- `fulfillIntent.prepare()` +- `cancelIntent.prepare()` +- `releaseFundsToPayer.prepare()` +- `addFunds.prepare()` +- `removeFunds.prepare()` +- `withdrawDeposit.prepare()` +- `setVaultFee.prepare()` +- `setVaultMinRate.prepare()` +- `setVaultConfig.prepare()` + +## Prepare a deposit creation + +`createDeposit()` may also register payee details, so its prepared form returns both the payee payload and the calldata: + +```ts +const { depositDetails, prepared } = await client.prepareCreateDeposit({ + token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + amount: 1_000_000000n, + intentAmountRange: { + min: 25_000000n, + max: 250_000000n, + }, + processorNames: ['wise'], + payeeData: [{ offchainId: 'maker@example.com' }], + conversionRates: [[ + { currency: 'USD', conversionRate: '1015000000000000000' }, + ]], +}); + +console.log(depositDetails); +console.log(prepared.to, prepared.chainId); +``` + +## Smart-wallet pattern in React + +Hooks such as `useCreateVault()` and `useVaultDelegation()` let you inject your own sender: + +```tsx +import { useCreateVault } from '@zkp2p/sdk/react'; + +const { createVault, prepareCreateVault, txHash } = useCreateVault({ + client, + sendTransaction: async ({ to, data, value }) => { + return smartWallet.sendTransaction({ to, data, value }); + }, + referrer: ['acme-wallet'], +}); + +await createVault({ + config: { + manager: '0xManager', + feeRecipient: '0xFeeRecipient', + maxFee: 50_000000000000000n, + fee: 10_000000000000000n, + name: 'Acme Vault', + uri: 'ipfs://vault-metadata', + }, +}); +``` + +For delegation switches, `useVaultDelegation({ sendBatch })` can batch the clear-plus-set sequence into one user operation. + +## Key points + +- Prepared transactions are the right interface for EIP-4337, relayers, simulations, and hardware wallet review screens +- `txOverrides.referrer` is already encoded into the prepared calldata through ERC-8021 attribution +- `prepareCreateDeposit()` is the only deposit-creation path that keeps payee registration and calldata preparation together +- If you need the direct wallet path again, call the method itself instead of `.prepare()` diff --git a/developer/cookbook/referral-fees.md b/developer/cookbook/referral-fees.md new file mode 100644 index 0000000..1cbcd6e --- /dev/null +++ b/developer/cookbook/referral-fees.md @@ -0,0 +1,123 @@ +--- +id: referral-fees +title: Referral Fees & Attribution +--- + +# Referral Fees & Attribution + +## What this covers + +How to add partner fees to quotes and intents, and how to append ERC-8021 attribution data to transactions. + +## When to use this + +Use this when you are embedding Peer inside another app and need revenue sharing, campaign codes, or Base builder attribution. + +## Configure a referrer fee + +```ts +import { + assertValidReferrerFeeConfig, + parseReferrerFeeConfig, +} from '@zkp2p/sdk'; + +const referrerFeeConfig = assertValidReferrerFeeConfig( + parseReferrerFeeConfig('0xReferrerFeeRecipient', 50), + 'getQuote', +); + +const quoteResponse = await client.getQuote({ + paymentPlatforms: ['wise'], + fiatCurrency: 'USD', + user: buyerAddress, + recipient: buyerAddress, + destinationChainId: 8453, + destinationToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + amount: '100', + isExactFiat: true, + referrerFeeConfig, +}); +``` + +Useful helpers: + +- `assertValidReferrerFeeConfig(config, context)` +- `isValidReferrerFeeBps(value)` +- `parseReferrerFeeConfig(recipient, feeBpsValue)` +- `referrerFeeConfigToPreciseUnits(config)` + +## Carry the same fee into `signalIntent()` + +When a fee is present, use the quote's `signalIntentAmount` if it is available. + +```ts +const quote = quoteResponse.responseObject?.quotes?.[0]; +if (!quote) { + throw new Error('No quote found'); +} + +await client.signalIntent({ + depositId: BigInt(quote.intent.depositId), + amount: BigInt(quote.signalIntentAmount ?? quote.intent.amount), + toAddress: buyerAddress, + processorName: quote.intent.processorName, + payeeDetails: quote.intent.payeeDetails, + fiatCurrencyCode: quote.intent.fiatCurrencyCode, + conversionRate: BigInt(quote.conversionRate), + referrerFeeConfig, + escrowAddress: quote.intent.escrowAddress as `0x${string}` | undefined, + orchestratorAddress: quote.intent.orchestratorAddress as `0x${string}` | undefined, +}); +``` + +## Add ERC-8021 attribution + +The SDK always appends `BASE_BUILDER_CODE` last. Your referrer codes go in front of it. + +```ts +import { + BASE_BUILDER_CODE, + ZKP2P_IOS_REFERRER, + appendAttributionToCalldata, + encodeWithAttribution, + getAttributionDataSuffix, +} from '@zkp2p/sdk'; + +const suffix = getAttributionDataSuffix([ZKP2P_IOS_REFERRER, 'acme-wallet']); +console.log(BASE_BUILDER_CODE, suffix); + +const calldata = encodeWithAttribution( + { + abi: escrowAbi, + functionName: 'addFunds', + args: [42n, 100_000000n], + }, + ['acme-wallet'], +); + +const finalCalldata = appendAttributionToCalldata(calldata, 'partner-campaign'); +``` + +If you are not using SDK write helpers, send the transaction manually: + +```ts +import { sendTransactionWithAttribution } from '@zkp2p/sdk'; + +await sendTransactionWithAttribution( + walletClient, + { + address: escrowAddress, + abi: escrowAbi, + functionName: 'addFunds', + args: [42n, 100_000000n], + }, + ['acme-wallet', 'campaign-q2'], +); +``` + +## Key points + +- `referrerFeeConfig` is for app-level fee sharing; `txOverrides.referrer` is for ERC-8021 attribution +- `signalIntentAmount` is the safest gross amount to pass into `signalIntent()` when a fee is applied +- `BASE_BUILDER_CODE` is always appended automatically and cannot be overridden +- Mobile apps commonly reuse `ZKP2P_IOS_REFERRER` or `ZKP2P_ANDROID_REFERRER` as one of the attribution codes diff --git a/developer/cookbook/taker-tiers.md b/developer/cookbook/taker-tiers.md new file mode 100644 index 0000000..28ae18f --- /dev/null +++ b/developer/cookbook/taker-tiers.md @@ -0,0 +1,79 @@ +--- +id: taker-tiers +title: Taker Tiers +--- + +# Taker Tiers + +## What this covers + +How to fetch taker tier data, render caps and cooldowns, and wire the React hook into your onramp UI. + +## When to use this + +Use this when you want to show a buyer what they can do before they open a quote or submit an intent. + +## Fetch a taker tier + +```ts +const response = await client.getTakerTier({ + owner: '0xBuyerAddress', + chainId: 8453, +}); + +console.log(response.responseObject); +``` + +The tier can be one of: + +- `PEASANT` +- `PEER` +- `PLUS` +- `PRO` +- `PLATINUM` +- `PEER_PRESIDENT` + +Important response fields: + +- `perIntentCapBaseUnits` +- `perIntentCapDisplay` +- `cooldownHours` +- `cooldownActive` +- `nextIntentAvailableAt` +- `platformLimits` + +## Render friendly UI + +```ts +import { getNextTierCap, getTierDisplayInfo } from '@zkp2p/sdk/react'; + +const tier = response.responseObject; +const display = getTierDisplayInfo(tier); + +console.log(display.tierLabel, display.capDisplay); +console.log('next cap:', getNextTierCap(tier.tier)); +``` + +`platformLimits` gives you per-rail overrides such as minimum tier and cooldown behavior for high-risk payment methods. + +## Use the React hook + +```tsx +import { useGetTakerTier } from '@zkp2p/sdk/react'; + +const { getTakerTier, takerTier, isLoading, error } = useGetTakerTier({ + client, + owner: buyerAddress, + chainId: 8453, + autoFetch: true, +}); +``` + +This is a good fit for onramp buttons, order forms, and quote pages where the connected wallet already exists. + +## Key points + +- Taker tiers are user-facing policy, not just analytics data +- `perIntentCapDisplay` is convenient for copy, but `perIntentCapBaseUnits` is the safer value for calculations +- `platformLimits` lets you explain why one payment rail has tighter rules than another +- `useGetTakerTier({ autoFetch: true })` is the easiest path in React diff --git a/developer/quickstart.md b/developer/quickstart.md new file mode 100644 index 0000000..92efe23 --- /dev/null +++ b/developer/quickstart.md @@ -0,0 +1,296 @@ +--- +id: quickstart +title: Quickstart +slug: /quickstart +--- + +# Quickstart + +## What this does + +This guide gets you from a blank TypeScript project to a working ZKP2P taker flow with `@zkp2p/sdk` `0.3.2+`. You will initialize a `Zkp2pClient`, inspect live liquidity, fetch a quote, and signal your first intent. + +## Who is this for? + +Use this if you want the fastest path from "I want to build on Peer" to "I have real code talking to the protocol." + +## What you will build + +- A Node.js script that connects to Base, reads deposits, fetches a quote, and signals an intent +- A minimal React component that does the same thing with `useSignalIntent()` +- A list of the next docs to read once the first transaction works + +## Prerequisites + +- Node.js `20+` or Bun +- A Base RPC URL +- A wallet with ETH for gas on Base +- For the Node example: a private key for that wallet + +:::info Base USDC +All examples below use Base USDC: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. +::: + +## 1. Create a project + +Start with Bun: + +```bash +mkdir peer-quickstart +cd peer-quickstart +bun init -y +bun add @zkp2p/sdk viem +``` + +If you also want the React example in the same session: + +```bash +bun create vite peer-react-quickstart --template react-ts +cd peer-react-quickstart +bun add @zkp2p/sdk viem +``` + +## 2. Initialize `Zkp2pClient` + +Create `scripts/quickstart.ts`: + +```ts +import { Zkp2pClient, setLogLevel } from '@zkp2p/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createWalletClient, http } from 'viem'; +import { base } from 'viem/chains'; + +const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const; + +const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined; +const rpcUrl = process.env.RPC_URL; + +if (!privateKey || !rpcUrl) { + throw new Error('Set PRIVATE_KEY and RPC_URL first.'); +} + +setLogLevel('info'); + +const account = privateKeyToAccount(privateKey); +const walletClient = createWalletClient({ + account, + chain: base, + transport: http(rpcUrl), +}); + +const client = new Zkp2pClient({ + walletClient, + chainId: base.id, + runtimeEnv: 'production', +}); +``` + +## 3. Read deposits and fetch a quote + +`client.getDeposits()` reads deposits owned by the connected wallet. `client.indexer.getDeposits()` is the faster way to inspect public liquidity when you are building a taker flow. + +```ts +const myDeposits = await client.getDeposits(); +console.log('connected wallet deposits:', myDeposits.length); + +const publicDeposits = await client.indexer.getDeposits( + { status: 'ACTIVE', acceptingIntents: true }, + { limit: 3, orderBy: 'remainingDeposits', orderDirection: 'desc' }, +); + +console.log( + 'top deposits:', + publicDeposits.map((deposit) => ({ + id: deposit.id, + remaining: deposit.remainingDeposits, + depositor: deposit.depositor, + })), +); + +const quoteResponse = await client.getQuote({ + paymentPlatforms: ['wise'], + fiatCurrency: 'USD', + user: account.address, + recipient: account.address, + destinationChainId: base.id, + destinationToken: USDC, + amount: '25', + isExactFiat: true, +}); + +const quote = quoteResponse.responseObject?.quotes?.[0]; +if (!quote) { + throw new Error('No quote found for the requested pair.'); +} + +console.log({ + paymentMethod: quote.paymentMethod, + fiatAmount: quote.fiatAmountFormatted, + tokenAmount: quote.tokenAmountFormatted, + depositId: quote.intent.depositId, +}); +``` + +## 4. Signal your first intent + +Once you have a quote, pass its intent fields directly into `signalIntent()`. + +```ts +const txHash = await client.signalIntent({ + depositId: BigInt(quote.intent.depositId), + amount: BigInt(quote.signalIntentAmount ?? quote.intent.amount), + toAddress: account.address, + processorName: quote.intent.processorName, + payeeDetails: quote.intent.payeeDetails, + fiatCurrencyCode: quote.intent.fiatCurrencyCode, + conversionRate: BigInt(quote.conversionRate), + escrowAddress: quote.intent.escrowAddress as `0x${string}` | undefined, + orchestratorAddress: quote.intent.orchestratorAddress as `0x${string}` | undefined, +}); + +console.log('signalIntent tx hash:', txHash); +``` + +Run it: + +```bash +PRIVATE_KEY=0x... \ +RPC_URL=https://base-mainnet.g.alchemy.com/v2/your-key \ +bun run scripts/quickstart.ts +``` + +## 5. React version + +For a React app, keep the quote lookup on the core client and use `useSignalIntent()` for transaction state. + +```tsx +import { + Zkp2pClient, + type GetQuoteSingleResponse, +} from '@zkp2p/sdk'; +import { useSignalIntent } from '@zkp2p/sdk/react'; +import type { Address } from 'viem'; +import { createWalletClient, custom } from 'viem'; +import { base } from 'viem/chains'; +import { useEffect, useState } from 'react'; + +const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const; + +declare global { + interface Window { + ethereum?: unknown; + } +} + +export default function App() { + const [client, setClient] = useState(null); + const [address, setAddress] = useState
(null); + const [quote, setQuote] = useState(null); + + const { signalIntent, isLoading, error, txHash } = useSignalIntent({ client }); + + useEffect(() => { + async function init() { + if (!window.ethereum) return; + + const transport = custom(window.ethereum as any); + const bootstrap = createWalletClient({ chain: base, transport }); + const [connectedAddress] = await bootstrap.requestAddresses(); + + const walletClient = createWalletClient({ + account: connectedAddress, + chain: base, + transport, + }); + + setAddress(connectedAddress); + setClient( + new Zkp2pClient({ + walletClient, + chainId: base.id, + }), + ); + } + + void init(); + }, []); + + async function loadQuote() { + if (!client || !address) return; + + const response = await client.getQuote({ + paymentPlatforms: ['wise'], + fiatCurrency: 'USD', + user: address, + recipient: address, + destinationChainId: base.id, + destinationToken: USDC, + amount: '25', + isExactFiat: true, + }); + + setQuote(response.responseObject?.quotes?.[0] ?? null); + } + + async function reserveQuote() { + if (!quote || !address) return; + + await signalIntent({ + depositId: BigInt(quote.intent.depositId), + amount: BigInt(quote.signalIntentAmount ?? quote.intent.amount), + toAddress: address, + processorName: quote.intent.processorName, + payeeDetails: quote.intent.payeeDetails, + fiatCurrencyCode: quote.intent.fiatCurrencyCode, + conversionRate: BigInt(quote.conversionRate), + escrowAddress: quote.intent.escrowAddress as `0x${string}` | undefined, + orchestratorAddress: quote.intent.orchestratorAddress as `0x${string}` | undefined, + }); + } + + return ( +
+ + + {quote ? ( + + ) : null} + + {quote ? ( +
+          {JSON.stringify(
+            {
+              paymentMethod: quote.paymentMethod,
+              fiatAmount: quote.fiatAmountFormatted,
+              tokenAmount: quote.tokenAmountFormatted,
+            },
+            null,
+            2,
+          )}
+        
+ ) : null} + + {txHash ?

tx hash: {txHash}

: null} + {error ?

error: {String(error)}

: null} +
+ ); +} +``` + +## Next steps + +- Read [Architecture Overview](/developer/architecture) before you build anything stateful +- Use [Build an Onramp Widget](/developer/tutorials/onramp-widget) for the fastest embedded UX +- Use [Build a Maker Bot](/developer/tutorials/maker-bot) if you are supplying liquidity +- Keep [Client Reference](/developer/sdk/client-reference) open once you move past the first happy path + +## Troubleshooting + +- No quotes returned: widen `paymentPlatforms`, lower the amount, or confirm you are targeting the right `destinationToken` +- `Wallet client is missing account`: make sure your browser wallet client was created with an attached `account`, or use `privateKeyToAccount()` in Node +- Missing gating signature: some deposits require authenticated gating. Provide `apiKey` or `authorizationToken` so the SDK can auto-fetch it, or pass `gatingServiceSignature` and `signatureExpiration` yourself +- Transaction reverts on submit: check Base gas, the selected quote's lifetime, and whether your wallet is still on chain `8453` diff --git a/developer/sdk/client-reference.md b/developer/sdk/client-reference.md index 813e339..c09a075 100644 --- a/developer/sdk/client-reference.md +++ b/developer/sdk/client-reference.md @@ -250,7 +250,76 @@ Use one of the delegation paths below depending on how the deposit is routed. | `getManagerFee(escrow, depositId)` | `bigint` | Reads the effective manager fee | | `getEffectiveRate({ escrow, depositId, paymentMethod, fiatCurrency })` | `bigint` | Reads effective EscrowV2 rate after manager logic | -For EscrowV2 pricing flows, the client also exposes `setOracleRateConfig()`, `removeOracleRateConfig()`, `setOracleRateConfigBatch()`, `updateCurrencyConfigBatch()`, and `deactivateCurrenciesBatch()`. +### Oracle rate configuration + +Use these methods when a deposit should track an oracle-backed floor on EscrowV2 instead of a purely manual fixed rate. + +#### `setOracleRateConfig()` + +| Field | Required | Description | +| --- | --- | --- | +| `depositId` | Yes | Deposit to update | +| `paymentMethodHash` | Yes | Payment method hash such as the result of `resolvePaymentMethodHash()` | +| `currencyHash` | Yes | Fiat currency bytes32 such as the result of `resolveFiatCurrencyBytes32()` | +| `config.adapter` | Yes | Oracle adapter contract address | +| `config.adapterConfig` | Yes | Adapter-specific bytes payload | +| `config.spreadBps` | Yes | Signed spread in basis points applied to the market price | +| `config.maxStaleness` | Yes | Maximum oracle age in seconds | +| `escrowAddress` | No | Explicit EscrowV2 override | +| `txOverrides` | No | viem transaction overrides | + +#### `removeOracleRateConfig()` + +| Field | Required | Description | +| --- | --- | --- | +| `depositId` | Yes | Deposit to update | +| `paymentMethodHash` | Yes | Payment method hash | +| `currencyHash` | Yes | Fiat currency bytes32 | +| `escrowAddress` | No | Explicit EscrowV2 override | +| `txOverrides` | No | viem transaction overrides | + +#### `setOracleRateConfigBatch()` + +| Field | Required | Description | +| --- | --- | --- | +| `depositId` | Yes | Deposit to update | +| `paymentMethods` | Yes | Payment method hashes grouped by outer index | +| `currencies` | Yes | Nested currency arrays grouped by payment method index | +| `configs` | Yes | Nested oracle-config arrays grouped by payment method index | +| `escrowAddress` | No | Explicit EscrowV2 override | +| `txOverrides` | No | viem transaction overrides | + +For a full setup example, see [Oracle Rate Configuration](/developer/cookbook/oracle-rates). + +### Batch currency operations + +These methods are useful for bots, vault operators, and admin tooling that need to move several currency pairs at once. + +#### `updateCurrencyConfigBatch()` + +| Field | Required | Description | +| --- | --- | --- | +| `depositId` | Yes | Deposit to update | +| `paymentMethods` | Yes | Payment method hashes grouped by outer index | +| `updates` | Yes | Nested `CurrencyRateUpdate` arrays grouped by payment method index | +| `updates[][code]` | Yes | Fiat currency bytes32 | +| `updates[][minConversionRate]` | Yes | Fixed floor with 18 decimals | +| `updates[][updateOracle]` | Yes | Whether to apply `oracleRateConfig` | +| `updates[][oracleRateConfig]` | Yes | Oracle config payload when `updateOracle` is `true` | +| `escrowAddress` | No | Explicit EscrowV2 override | +| `txOverrides` | No | viem transaction overrides | + +#### `deactivateCurrenciesBatch()` + +| Field | Required | Description | +| --- | --- | --- | +| `depositId` | Yes | Deposit to update | +| `paymentMethods` | Yes | Payment method hashes grouped by outer index | +| `currencyCodes` | Yes | Nested currency arrays grouped by payment method index | +| `escrowAddress` | No | Explicit EscrowV2 override | +| `txOverrides` | No | viem transaction overrides | + +For higher-level recipes that combine these methods with vault and delegation flows, see [Batch Operations](/developer/cookbook/batch-operations). ## Quote API diff --git a/developer/tutorials/maker-bot.md b/developer/tutorials/maker-bot.md new file mode 100644 index 0000000..7f6cbbf --- /dev/null +++ b/developer/tutorials/maker-bot.md @@ -0,0 +1,293 @@ +--- +id: maker-bot +title: Build a Maker Bot +--- + +# Build a Maker Bot + +## What this does + +This tutorial shows how to run a Node.js bot that creates deposits, monitors signaled intents, prunes expired reservations, and manages liquidity over time. + +## Who is this for? + +Use this if you are supplying fiat-liquidity inventory and want operations code around deposits instead of a browser UI. + +## Important note + +A maker bot manages the supply side. It typically does **not** call `fulfillIntent()` itself because the taker owns the payment proof. The bot watches signaled intents, watches for fulfillment, and decides when to top up, pause, reject, or prune. + +If you operate both sides of the flow, you can still call `client.fulfillIntent()` from a separate taker service once the proof is available. + +## Prerequisites + +- Node.js 20+ or Bun +- A Base RPC URL +- A maker wallet with ETH for gas and USDC for liquidity +- Off-chain logic that watches incoming fiat payments + +## 1. Create the project + +```bash +mkdir peer-maker-bot +cd peer-maker-bot +bun init -y +bun add @zkp2p/sdk viem dotenv +``` + +Create `.env`: + +```bash +PRIVATE_KEY=0x... +RPC_URL=https://base-mainnet.g.alchemy.com/v2/your-key +RUNTIME_ENV=production +``` + +## 2. Bootstrap the client + +Create `src/bot.ts`: + +```ts +import 'dotenv/config'; +import { Zkp2pClient, setLogLevel } from '@zkp2p/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createWalletClient, http } from 'viem'; +import { base } from 'viem/chains'; + +const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const; + +const runtimeEnv = + process.env.RUNTIME_ENV === 'staging' + ? 'staging' + : process.env.RUNTIME_ENV === 'preproduction' + ? 'preproduction' + : 'production'; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); +const walletClient = createWalletClient({ + account, + chain: base, + transport: http(process.env.RPC_URL), +}); + +setLogLevel('debug'); + +const client = new Zkp2pClient({ + walletClient, + chainId: base.id, + runtimeEnv, +}); +``` + +## 3. Ensure allowance and create a deposit + +Use `registerPayeeDetails()` if you want reusable hashes, then pass those hashes into `createDeposit()`. + +```ts +async function ensureMakerDeposit() { + await client.ensureAllowance({ + token: USDC, + amount: 5_000_000000n, + maxApprove: true, + }); + + const payeeData = [ + { offchainId: 'maker@example.com' }, + { offchainId: '@maker-revolut' }, + ]; + + const { hashedOnchainIds } = await client.registerPayeeDetails({ + processorNames: ['wise', 'revolut'], + payeeData, + }); + + const { hash } = await client.createDeposit({ + token: USDC, + amount: 5_000_000000n, + intentAmountRange: { + min: 25_000000n, + max: 500_000000n, + }, + processorNames: ['wise', 'revolut'], + payeeData, + payeeDetailsHashes: hashedOnchainIds, + conversionRates: [ + [{ currency: 'USD', conversionRate: '1015000000000000000' }], + [{ currency: 'EUR', conversionRate: '940000000000000000' }], + ], + retainOnEmpty: true, + }); + + console.log('createDeposit tx:', hash); +} +``` + +## 4. Read your active deposits + +The indexer gives you stable composite IDs for monitoring and analytics. + +```ts +async function getActiveDepositIds() { + const deposits = await client.indexer.getDeposits( + { depositor: account.address, status: 'ACTIVE' }, + { limit: 50, orderBy: 'updatedAt', orderDirection: 'desc' }, + ); + + return deposits.map((deposit) => deposit.id); +} +``` + +## 5. Poll for signaled intents + +This loop watches new intents on your deposits and prints the ones that need operator or automation attention. + +```ts +const seenIntents = new Set(); + +async function watchSignaledIntents() { + const depositIds = await getActiveDepositIds(); + if (!depositIds.length) { + console.log('No active deposits yet.'); + return; + } + + const signaled = await client.indexer.getIntentsForDeposits(depositIds, ['SIGNALED']); + + for (const intent of signaled) { + const key = intent.intentHash.toLowerCase(); + if (seenIntents.has(key)) continue; + + seenIntents.add(key); + console.log('new intent', { + intentHash: intent.intentHash, + owner: intent.owner, + amount: intent.amount, + paymentMethodHash: intent.paymentMethodHash, + depositId: intent.depositId, + }); + + // Hand off to your bank / payment watcher here. + // Example: enqueueForFiatMonitoring(intent) + } +} +``` + +## 6. Prune expired intents and reject bad ones + +Use `getExpiredIntents()` plus `pruneExpiredIntents()` for cleanup. Use `releaseFundsToPayer()` when you explicitly want to reject an intent after checking the fiat side. + +```ts +async function pruneExpired() { + const depositIds = await getActiveDepositIds(); + if (!depositIds.length) return; + + const expired = await client.indexer.getExpiredIntents({ + now: Math.floor(Date.now() / 1000).toString(), + depositIds, + limit: 100, + }); + + for (const intent of expired) { + const [escrowAddress, rawDepositId] = intent.depositId.split('_'); + if (!escrowAddress || !rawDepositId) continue; + + console.log('pruning expired intent', intent.intentHash); + await client.pruneExpiredIntents({ + escrowAddress: escrowAddress as `0x${string}`, + depositId: BigInt(rawDepositId), + }); + } +} + +async function rejectIntent(intentHash: `0x${string}`) { + const txHash = await client.releaseFundsToPayer({ intentHash }); + console.log('manual release tx:', txHash); +} +``` + +## 7. Track fulfillments + +The taker or extension submits the proof. Your bot can still track completion and fee outcomes. + +```ts +async function logFulfillments(intentHashes: string[]) { + const fulfilled = await client.indexer.getFulfilledIntentEvents(intentHashes); + + for (const item of fulfilled) { + const amounts = await client.indexer.getIntentFulfillmentAmounts(item.intentHash); + console.log('fulfilled intent', { + intentHash: item.intentHash, + isManualRelease: item.isManualRelease, + releasedAmount: amounts?.releasedAmount, + takerAmountNetFees: amounts?.takerAmountNetFees, + }); + } +} +``` + +## 8. Manage the deposit lifecycle + +Once the bot is running, these are the main supply-side actions it performs: + +```ts +async function topUpDeposit(depositId: bigint) { + await client.addFunds({ + depositId, + amount: 500_000000n, + }); +} + +async function pauseDeposit(depositId: bigint) { + await client.setAcceptingIntents({ + depositId, + accepting: false, + }); +} + +async function exitDeposit(depositId: bigint) { + await client.withdrawDeposit({ depositId }); +} +``` + +## 9. Run the loop + +```ts +async function main() { + await ensureMakerDeposit(); + + for (;;) { + await watchSignaledIntents(); + await pruneExpired(); + await new Promise((resolve) => setTimeout(resolve, 15_000)); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); +``` + +## 10. If you also operate the taker side + +When your system receives a valid proof, call `fulfillIntent()` from the taker workflow, not from the maker-monitoring loop: + +```ts +await client.fulfillIntent({ + intentHash: '0x...', + proof: proofFromPeerAuthOrReclaim, +}); +``` + +## Troubleshooting + +- `getDeposits()` stays empty after creation: the transaction may not be mined yet. Poll the indexer with `depositor: account.address` until the new deposit appears +- `signal` intents show up but never fulfill: that usually means the taker never finished the fiat leg or never submitted a proof +- `pruneExpiredIntents()` reverts: make sure you parsed the composite deposit ID into `escrowAddress` and raw `depositId` correctly +- Repeated approval prompts: use `maxApprove: true` in `ensureAllowance()` if that matches your risk model + +## Next steps + +- Read [Prepared Transactions](/developer/cookbook/prepared-transactions) if your bot submits through a relayer or smart account +- Read [Error Handling & Retries](/developer/cookbook/error-handling) before you put the loop in production +- Read [Indexer Pagination & Filtering](/developer/cookbook/indexer-queries) for higher-volume monitoring diff --git a/developer/tutorials/onramp-widget.md b/developer/tutorials/onramp-widget.md new file mode 100644 index 0000000..3ff9073 --- /dev/null +++ b/developer/tutorials/onramp-widget.md @@ -0,0 +1,277 @@ +--- +id: onramp-widget +title: Build an Onramp Widget +--- + +# Build an Onramp Widget + +## What this does + +This tutorial builds a React funding widget that checks Peer extension state, previews taker limits with `useGetTakerTier()`, and opens the Peer onramp with pre-filled params. + +## Who is this for? + +Use this if you want a "Fund with Peer" button inside an existing product flow instead of redirecting users to a separate page. + +## What you will build + +- An install / connect / ready state machine for the Peer extension +- A reusable `OnrampWidget` component +- A completion listener wired to `onIntentFulfilled()` +- Optional tier and cap UI powered by the SDK React hook layer + +:::note Desktop-first +`peerExtensionSdk` requires a browser window and the Peer extension. For mobile handoff patterns, see [Extension Deeplinks](/developer/cookbook/extension-deeplinks). +::: + +## Prerequisites + +- React 18+ +- `@zkp2p/sdk`, `viem` +- A Base wallet for testing +- Peer extension installed in a Chromium browser + +## 1. Install dependencies + +```bash +bun add @zkp2p/sdk viem +``` + +## 2. Create a browser client helper + +Create `src/lib/peer.ts`: + +```ts +import { createPeerExtensionSdk, Zkp2pClient } from '@zkp2p/sdk'; +import { createWalletClient, custom } from 'viem'; +import { base } from 'viem/chains'; + +declare global { + interface Window { + ethereum?: unknown; + } +} + +export const peerSdk = createPeerExtensionSdk({ window }); + +export async function createBrowserClient() { + if (!window.ethereum) { + return { client: null, address: null }; + } + + const transport = custom(window.ethereum as any); + const bootstrap = createWalletClient({ chain: base, transport }); + const [address] = await bootstrap.requestAddresses(); + + const walletClient = createWalletClient({ + account: address, + chain: base, + transport, + }); + + return { + client: new Zkp2pClient({ + walletClient, + chainId: base.id, + }), + address, + }; +} +``` + +## 3. Build the widget + +Create `src/components/OnrampWidget.tsx`: + +```tsx +import { + createPeerExtensionSdk, + getTierDisplayInfo, + type PeerExtensionState, + type PeerIntentFulfilledResult, + type Zkp2pClient, +} from '@zkp2p/sdk'; +import { useGetTakerTier } from '@zkp2p/sdk/react'; +import { base } from 'viem/chains'; +import { useEffect, useMemo, useState } from 'react'; +import { createBrowserClient } from '../lib/peer'; + +const peerSdk = createPeerExtensionSdk({ window }); + +type OnrampWidgetProps = { + toToken: string; + referrer: string; + referrerLogo: string; + inputCurrency?: string; + inputAmount?: string; + paymentPlatform?: string; + recipientAddress?: `0x${string}`; + onFulfilled?: (result: PeerIntentFulfilledResult) => void; +}; + +export function OnrampWidget({ + toToken, + referrer, + referrerLogo, + inputCurrency = 'USD', + inputAmount = '25', + paymentPlatform = 'wise', + recipientAddress, + onFulfilled, +}: OnrampWidgetProps) { + const [client, setClient] = useState(null); + const [connectedAddress, setConnectedAddress] = useState<`0x${string}` | null>( + recipientAddress ?? null, + ); + const [extensionState, setExtensionState] = useState('needs_install'); + const [lastResult, setLastResult] = useState(null); + const [isOpening, setIsOpening] = useState(false); + const [message, setMessage] = useState(null); + + const { takerTier, isLoading: isTierLoading } = useGetTakerTier({ + client, + owner: connectedAddress, + chainId: base.id, + autoFetch: Boolean(client && connectedAddress), + }); + + const tierDisplay = useMemo( + () => getTierDisplayInfo(takerTier ?? undefined), + [takerTier], + ); + + useEffect(() => { + async function init() { + const { client, address } = await createBrowserClient(); + setClient(client); + setConnectedAddress((recipientAddress ?? address ?? null) as `0x${string}` | null); + setExtensionState(await peerSdk.getState()); + } + + void init(); + }, [recipientAddress]); + + useEffect(() => { + const unsubscribe = peerSdk.onIntentFulfilled((result) => { + setLastResult(result); + setMessage( + result.bridge.status === 'pending' + ? 'Intent fulfilled. Bridge delivery is still pending.' + : 'Intent fulfilled. Funds were delivered to the destination wallet.', + ); + onFulfilled?.(result); + }); + + return unsubscribe; + }, [onFulfilled]); + + async function openOnramp() { + setIsOpening(true); + setMessage(null); + + try { + const state = await peerSdk.getState(); + setExtensionState(state); + + if (state === 'needs_install') { + setMessage('Install the Peer extension before opening the onramp.'); + return; + } + + if (state === 'needs_connection') { + const approved = await peerSdk.requestConnection(); + if (!approved) { + setMessage('The extension must be connected before the onramp can open.'); + return; + } + } + + peerSdk.onramp({ + referrer, + referrerLogo, + inputCurrency, + inputAmount, + paymentPlatform, + toToken, + recipientAddress: connectedAddress ?? undefined, + }); + } finally { + setIsOpening(false); + setExtensionState(await peerSdk.getState()); + } + } + + return ( +
+

Extension state: {extensionState}

+ {connectedAddress ?

Recipient: {connectedAddress}

: null} + + {connectedAddress && !isTierLoading ? ( +

+ Tier: {tierDisplay.tierLabel} | Cap: {tierDisplay.capDisplay} +

+ ) : null} + + {extensionState === 'needs_install' ? ( + + ) : ( + + )} + + {message ?

{message}

: null} + {lastResult ?

Last fulfilled intent: {lastResult.intentHash}

: null} +
+ ); +} +``` + +## 4. Mount the widget + +Use it anywhere you already know the destination token and recipient: + +```tsx +import { OnrampWidget } from './components/OnrampWidget'; + +export default function App() { + return ( + + ); +} +``` + +## 5. What each piece is doing + +- `createPeerExtensionSdk({ window })` gives you a scoped extension client. That is the preferred pattern for app integrations +- `peerSdk.getState()` reduces the extension UX to three states: `needs_install`, `needs_connection`, and `ready` +- `useGetTakerTier()` gives you a fast way to show a cap before the user opens the flow +- `peerSdk.onIntentFulfilled()` is your callback for success and bridge-pending status +- `peerSdk.onramp()` opens the side panel with the params your app already knows + +## 6. Production hardening + +- Keep `referrer` and `referrerLogo` stable so users recognize your brand inside the flow +- Precompute `toToken` from your product state instead of string-building it inline across the app +- Register `onIntentFulfilled()` once near the page root if several buttons can open the onramp +- If your app already has a connected wallet, always pass `recipientAddress` so users land in a one-click flow + +## Troubleshooting + +- Button always shows install: the page is not running in a browser where the Peer extension is available +- User can connect a wallet but not the extension: that is normal. Wallet connection and extension connection are separate +- Callback never fires: make sure the same tab that called `onramp()` is still open and the listener was registered first +- Need to resume an active intent: pass `intentHash` into `peerSdk.onramp()` or see [Extension Deeplinks](/developer/cookbook/extension-deeplinks) + +## Next steps + +- Read [Onramp Integration](/developer/integrate-zkp2p/integrate-redirect-onramp) for the full parameter reference +- Read [Extension Deeplinks](/developer/cookbook/extension-deeplinks) for route and resume patterns +- Read [Taker Tiers](/developer/cookbook/taker-tiers) if you want to turn tiering into UI copy and guardrails diff --git a/developer/tutorials/vault-dashboard.md b/developer/tutorials/vault-dashboard.md new file mode 100644 index 0000000..d1677d5 --- /dev/null +++ b/developer/tutorials/vault-dashboard.md @@ -0,0 +1,279 @@ +--- +id: vault-dashboard +title: Build a Vault Dashboard +--- + +# Build a Vault Dashboard + +## What this does + +This tutorial builds a React dashboard for vault operators and depositors. It loads vault lists, vault detail, delegated deposits, daily snapshots, profit history, and manual or oracle-driven rate updates. + +## Who is this for? + +Use this if you operate a vault, compare vaults, or want a custom analytics surface instead of relying on the default Peer UI. + +## Prerequisites + +- React 18+ +- `@zkp2p/sdk`, `viem` +- A Base wallet for browser access + +## 1. Install dependencies + +```bash +bun add @zkp2p/sdk viem +``` + +## 2. Create a browser client + +Create `src/lib/client.ts`: + +```ts +import { Zkp2pClient } from '@zkp2p/sdk'; +import { createWalletClient, custom } from 'viem'; +import { base } from 'viem/chains'; + +declare global { + interface Window { + ethereum?: unknown; + } +} + +export async function createDashboardClient() { + if (!window.ethereum) return null; + + const transport = custom(window.ethereum as any); + const bootstrap = createWalletClient({ chain: base, transport }); + const [address] = await bootstrap.requestAddresses(); + + return new Zkp2pClient({ + walletClient: createWalletClient({ + account: address, + chain: base, + transport, + }), + chainId: base.id, + }); +} +``` + +## 3. Load vault list and detail + +Create `src/App.tsx`: + +```tsx +import { + classifyDelegationState, + type IndexerManagerDailySnapshot, + type IndexerManualRateUpdate, + type IndexerOracleConfigUpdate, + type IndexerRateManagerDelegation, + type IndexerRateManagerDetail, + type IndexerRateManagerListItem, + type Zkp2pClient, +} from '@zkp2p/sdk'; +import { useEffect, useMemo, useState } from 'react'; +import { createDashboardClient } from './lib/client'; + +type VaultBundle = { + detail: IndexerRateManagerDetail | null; + delegations: IndexerRateManagerDelegation[]; + dailySnapshots: IndexerManagerDailySnapshot[]; + profitSnapshots: Array>; + manualRateUpdates: IndexerManualRateUpdate[]; + oracleConfigUpdates: IndexerOracleConfigUpdate[]; +}; + +export default function App() { + const [client, setClient] = useState(null); + const [vaults, setVaults] = useState([]); + const [selectedVaultId, setSelectedVaultId] = useState(null); + const [selectedVaultAddress, setSelectedVaultAddress] = useState(null); + const [bundle, setBundle] = useState(null); + + useEffect(() => { + async function init() { + const client = await createDashboardClient(); + if (!client) return; + + setClient(client); + + const vaults = await client.indexer.getRateManagers( + { limit: 25, orderBy: 'currentDelegatedBalance', orderDirection: 'desc' }, + {}, + ); + + setVaults(vaults); + + const first = vaults[0]?.manager; + if (first) { + setSelectedVaultId(first.rateManagerId); + setSelectedVaultAddress(first.rateManagerAddress ?? null); + } + } + + void init(); + }, []); + + useEffect(() => { + async function loadVaultBundle() { + if (!client || !selectedVaultId) return; + + const detail = await client.indexer.getRateManagerDetail(selectedVaultId, { + rateManagerAddress: selectedVaultAddress, + statsLimit: 30, + }); + + const delegations = await client.indexer.getRateManagerDelegations(selectedVaultId, { + limit: 100, + orderBy: 'delegatedAt', + orderDirection: 'desc', + rateManagerAddress: selectedVaultAddress ?? undefined, + }); + + const dailySnapshots = await client.indexer.getManagerDailySnapshots(selectedVaultId, { + limit: 30, + rateManagerAddress: selectedVaultAddress, + }); + + const profitSnapshots = await client.indexer.getProfitSnapshotsByDeposits( + delegations.map((item) => item.depositId), + ); + + const manualRateUpdates = await client.indexer.getManualRateUpdates(selectedVaultId, { + limit: 50, + rateManagerAddress: selectedVaultAddress, + }); + + const oracleConfigUpdates = await client.indexer.getOracleConfigUpdates(selectedVaultId, { + limit: 50, + rateManagerAddress: selectedVaultAddress, + }); + + setBundle({ + detail, + delegations, + dailySnapshots, + profitSnapshots, + manualRateUpdates, + oracleConfigUpdates, + }); + } + + void loadVaultBundle(); + }, [client, selectedVaultId, selectedVaultAddress]); + + const delegationRows = useMemo(() => { + if (!bundle?.detail) return []; + + return bundle.delegations.map((delegation) => ({ + ...delegation, + state: classifyDelegationState( + delegation.rateManagerId, + delegation.rateManagerAddress, + bundle.detail?.manager.rateManagerId, + bundle.detail?.manager.rateManagerAddress, + ), + })); + }, [bundle]); + + return ( +
+

Vault Dashboard

+ +
+

Vaults

+ {vaults.map((item) => ( + + ))} +
+ + {bundle?.detail ? ( +
+

{bundle.detail.manager.name}

+

fee: {bundle.detail.manager.fee}

+

max fee: {bundle.detail.manager.maxFee}

+

delegated deposits: {bundle.delegations.length}

+

manual rate updates: {bundle.manualRateUpdates.length}

+

oracle config updates: {bundle.oracleConfigUpdates.length}

+
+ ) : null} + +
+

Delegations

+ {delegationRows.map((item) => ( +
+ {item.depositId} | {item.state} +
+ ))} +
+ +
+

Daily snapshots

+ {bundle?.dailySnapshots.map((snapshot) => ( +
+ {snapshot.dayTimestamp}: volume {snapshot.totalFilledVolume} +
+ ))} +
+
+ ); +} +``` + +## 4. What the dashboard is querying + +- `getRateManagers()` gives you the top-level vault list +- `getRateManagerDetail()` gives you the selected vault's metadata, rates, aggregate stats, and recent stats +- `getRateManagerDelegations()` gives you the deposit list currently delegated to that vault +- `getManagerDailySnapshots()` gives you time-series rollups +- `getProfitSnapshotsByDeposits()` lets you project PnL across the delegated deposit set +- `getManualRateUpdates()` and `getOracleConfigUpdates()` fill the change-log panels + +## 5. Add richer panels + +Once the base screen is working, the next useful panels are: + +- A rate table built from `bundle.detail.rates` +- A PnL chart built from `bundle.profitSnapshots` +- A spread-history table that merges `manualRateUpdates` and `oracleConfigUpdates` +- A drill-down drawer powered by `client.indexer.getDelegationForDeposit(depositId)` + +## 6. Render delegation state clearly + +`classifyDelegationState()` is the simplest way to tag a row without duplicating vault-logic in your UI: + +```ts +const state = classifyDelegationState( + delegation.rateManagerId, + delegation.rateManagerAddress, + selectedVault.manager.rateManagerId, + selectedVault.manager.rateManagerAddress, +); + +// 'delegated_here' | 'delegated_elsewhere' | 'not_delegated' +``` + +This is useful when a depositor can compare several vaults side by side. + +## Troubleshooting + +- Vault list is empty: make sure you are querying the right `runtimeEnv` and that your indexer endpoint is reachable +- Delegation state looks wrong: compare both `rateManagerId` and `rateManagerAddress`; the helper expects both when available +- Metrics lag behind live fills: the indexer is eventually consistent. Use RPC-first reads for immediate pre-transaction checks and indexer queries for reporting +- You need a read-only dashboard: the SDK still expects a `walletClient`, so the simplest browser path is to use a connected wallet for reads + +## Next steps + +- Read [Delegation State Machine](/developer/cookbook/delegation) for vault-routing edge cases +- Read [Oracle Rate Configuration](/developer/cookbook/oracle-rates) if you want to surface floor or spread data in the UI +- Keep [Client Reference](/developer/sdk/client-reference) open for the exact indexer method list diff --git a/developer/use-cases.md b/developer/use-cases.md new file mode 100644 index 0000000..dcc67d7 --- /dev/null +++ b/developer/use-cases.md @@ -0,0 +1,75 @@ +--- +id: use-cases +title: What Can You Build? +slug: /use-cases +--- + +# What Can You Build? + +## What this does + +This page shows the main product shapes that external developers build on top of ZKP2P so you can pick the right starting point before you dive into the API surface. + +## 1. Embedded onramp + +Let users fund your product without leaving your app. This is the fastest path for wallets, games, trading apps, and consumer dapps that want a "Buy with Peer" button next to any action gated by token balance. + +The usual SDK surface is `peerExtensionSdk.onramp()`, `peerExtensionSdk.onIntentFulfilled()`, `useGetTakerTier()`, and `getQuote()` when you want to preview liquidity before opening the extension. + +Start with [Build an Onramp Widget](/developer/tutorials/onramp-widget) and [Onramp Integration](/developer/integrate-zkp2p/integrate-redirect-onramp). + +## 2. Maker or liquidity bot + +Run supply-side infrastructure that creates deposits, updates rates, watches for signaled intents, and manages liquidity over time. This is the right fit for market makers, treasury teams, and liquidity desks. + +The usual SDK surface is `ensureAllowance()`, `registerPayeeDetails()`, `createDeposit()`, `addFunds()`, `withdrawDeposit()`, `client.indexer.getIntentsForDeposits()`, and `pruneExpiredIntents()`. + +Start with [Build a Maker Bot](/developer/tutorials/maker-bot) and [Offramp Integration](/developer/offramp). + +## 3. Vault dashboard + +Build an analytics and control plane for delegated rate management. This is the right fit if you operate a vault, compare managers, or need a rate-history and PnL surface for depositors. + +The usual SDK surface is `client.indexer.getRateManagers()`, `getRateManagerDetail()`, `getRateManagerDelegations()`, `getManagerDailySnapshots()`, `getProfitSnapshotsByDeposits()`, and `classifyDelegationState()`. + +Start with [Build a Vault Dashboard](/developer/tutorials/vault-dashboard). + +## 4. Payment integration + +Add a new fiat rail to the ecosystem by building a provider template for PeerAuth and the mobile app. This is the right fit when you want to bring a new bank, payment app, or regional transfer method into ZKP2P. + +The main surface is the provider/template layer rather than `Zkp2pClient`, but app teams still use the SDK later for quotes, intent handling, and fulfillment around that provider. + +Start with [Build a Payment Integration](/developer/build-payment-integration). + +## 5. Custom settlement logic + +Add your own logic after fulfillment, such as bridging, splitting, or routing funds into another protocol. This is useful when a plain USDC receipt is not the final product experience you want. + +The usual SDK surface is `signalIntent()`, `fulfillIntent()`, `postIntentHook`, `postIntentHookData`, and the protocol hook contracts. + +Start with [Intent Hooks](/developer/api/v3/post-intent-hooks). + +## 6. Analytics tools and explorers + +Build search, reporting, and monitoring tools across deposits, intents, volumes, fund activity, and vault performance. This is the right fit for internal ops dashboards, public explorers, and BI pipelines. + +The main surface is `client.indexer.*`, raw `query()` access, and the converter helpers such as `convertIndexerDepositToEscrowView()` and `convertDepositsForLiquidity()`. + +Start with [Indexer Pagination & Filtering](/developer/cookbook/indexer-queries) and [Client Reference](/developer/sdk/client-reference). + +## 7. Smart-account and wallet infrastructure + +Support EIP-4337, batched user operations, relayers, and custom senders without giving up the ZKP2P flow. This is the right fit for wallets, account-abstraction stacks, and enterprise custody products. + +The main surface is the prepared-transaction pattern: `signalIntent.prepare()`, `fulfillIntent.prepare()`, `prepareCreateDeposit()`, `useCreateVault()`, and `useVaultDelegation({ sendBatch })`. + +Start with [Prepared Transactions](/developer/cookbook/prepared-transactions). + +## Picking a starting doc + +- Building a taker UX: start at [Quickstart](/developer/quickstart) +- Embedding a funding CTA: start at [Build an Onramp Widget](/developer/tutorials/onramp-widget) +- Supplying liquidity: start at [Build a Maker Bot](/developer/tutorials/maker-bot) +- Operating a vault: start at [Build a Vault Dashboard](/developer/tutorials/vault-dashboard) +- Looking up specific methods: keep [Client Reference](/developer/sdk/client-reference) open From aa8f8734ed9f11c0b6317712c9b39ec9700e6d6b Mon Sep 17 00:00:00 2001 From: Andrew Wilkinson Date: Wed, 29 Apr 2026 10:01:37 +0000 Subject: [PATCH 2/2] fix: address review findings --- .oneshot-pr-body.txt | 40 ++++++++++++++++----- .oneshot-pr-title.txt | 2 +- developer/architecture.md | 2 +- developer/cookbook/batch-operations.md | 10 +++--- developer/cookbook/delegation.md | 11 ++++-- developer/cookbook/extension-deeplinks.md | 4 +-- developer/cookbook/indexer-queries.md | 14 +++++--- developer/cookbook/prepared-transactions.md | 11 +++--- developer/cookbook/referral-fees.md | 10 +++--- developer/cookbook/taker-tiers.md | 2 +- developer/quickstart.md | 18 ++++++---- developer/sdk/client-reference.md | 33 ++++++++--------- developer/sdk/overview.md | 2 +- developer/sdk/react-hooks.md | 2 +- developer/tutorials/maker-bot.md | 6 ++-- developer/tutorials/onramp-widget.md | 3 +- 16 files changed, 106 insertions(+), 64 deletions(-) diff --git a/.oneshot-pr-body.txt b/.oneshot-pr-body.txt index 05af129..fa24a04 100644 --- a/.oneshot-pr-body.txt +++ b/.oneshot-pr-body.txt @@ -1,20 +1,42 @@ ## Summary -- Add new seller guide explaining how to create Private Orders with Peer Pay inline whitelisting -- Add sidebar entry in For Sellers section between Manual Releases and ARM +- Adds 15 new pages to the developer section: quickstart, architecture overview, 3 end-to-end tutorials, 10 cookbook recipes, a use-cases page, and expanded SDK reference tables +- Restructures `developer-sidebars.js` with Quickstart, Tutorials, Cookbook, and Integration Guides categories while preserving all existing reference docs +- Total addition: ~2,600 lines of TypeScript code examples and documentation across 18 files ## Why -zkp2p-clients PR #714 shipped the Private Orders + Peer Pay inline creation feature in both Express and Advanced deposit flows. Existing protocol docs cover the underlying whitelist hook at the contract level, but there was no user-facing guide for sellers. - -Closes #80 +The developer docs had solid SDK reference material but no pathway from "I want to build on ZKP2P" to a working app. External developers had to reverse-engineer method signatures from the SDK source. This closes the gap with a 15-minute quickstart, three complete tutorials (onramp widget, maker bot, vault dashboard), and cookbook recipes for every major SDK surface area. ## Changes -- **guides/for-sellers/private-orders.md** -- new guide covering who the feature is for, step-by-step enable flow, key behaviors, and common issues -- **guides-sidebars.js** -- added `for-sellers/private-orders` entry after `manual-releases` and before the ARM category +**New pages:** +- `developer/quickstart.md` — Node.js and React examples from `bun init` to `signalIntent()` +- `developer/architecture.md` — system diagram and data flow for onramp and maker flows +- `developer/use-cases.md` — "What Can You Build?" ecosystem overview with starting-doc pointers +- `developer/tutorials/onramp-widget.md` — React widget using `peerExtensionSdk` and `useGetTakerTier()` +- `developer/tutorials/maker-bot.md` — Node.js bot for deposit creation, intent monitoring, and lifecycle management +- `developer/tutorials/vault-dashboard.md` — React dashboard using indexer vault and delegation APIs + +**Cookbook recipes (10):** +- Prepared transactions for smart accounts +- Referral fees and ERC-8021 attribution +- Oracle rate configuration +- Batch currency and delegation operations +- Error handling and retry strategies +- Multi-environment deployment +- Indexer pagination, filtering, and raw GraphQL +- Extension deeplinks and side-panel routes +- Taker tier display +- Delegation state machine + +**Expanded reference:** +- `developer/sdk/client-reference.md` — added oracle rate config, batch currency ops, and deactivation method tables with field-level documentation + +**Sidebar:** +- `developer-sidebars.js` — reorganized into Quickstart, Architecture, Use Cases, Integration Guides, Tutorials, Cookbook, and SDK Reference sections ## Test plan -- `yarn build` -- passed, all internal links compile-validated -- `git diff --stat origin/main...HEAD` -- confirms exactly 2 files changed (1 new, 1 modified) +- `yarn run typecheck` -- unavailable; no `typecheck` script is defined in `package.json` +- `yarn build` -- passed diff --git a/.oneshot-pr-title.txt b/.oneshot-pr-title.txt index 9b1f34f..062cf13 100644 --- a/.oneshot-pr-title.txt +++ b/.oneshot-pr-title.txt @@ -1 +1 @@ -docs: add private orders seller guide \ No newline at end of file +docs: add developer quickstart, tutorials, cookbook, and architecture \ No newline at end of file diff --git a/developer/architecture.md b/developer/architecture.md index b33654b..7fa4ef1 100644 --- a/developer/architecture.md +++ b/developer/architecture.md @@ -166,6 +166,6 @@ Use `getContracts(chainId, env)` when you want to inspect the exact addresses an ## Troubleshooting -- `getDeposits()` is empty but public liquidity exists: `getDeposits()` only returns deposits owned by the connected wallet. Use `client.indexer.getDeposits()` or quote APIs for market-wide discovery +- `getAccountDeposits()` is empty but public liquidity exists: account-scoped reads only show that owner's deposits. Use `client.indexer.getDeposits()` or quote APIs for market-wide discovery - Indexer data looks stale: confirm with RPC-first methods before you submit a transaction - `fulfillIntent()` seems to do two steps: that is expected. It talks to the Attestation Service first, then submits the final on-chain transaction diff --git a/developer/cookbook/batch-operations.md b/developer/cookbook/batch-operations.md index f568328..75fc360 100644 --- a/developer/cookbook/batch-operations.md +++ b/developer/cookbook/batch-operations.md @@ -91,6 +91,8 @@ await client.deactivateCurrenciesBatch({ ```tsx import { useVaultDelegation } from '@zkp2p/sdk/react'; +const escrowAddress = '0x0000000000000000000000000000000000000000' as const; + const { delegateDeposits, clearDelegations } = useVaultDelegation({ client, sendTransaction, @@ -104,13 +106,13 @@ await delegateDeposits({ rateManagerId, deposits: [ { - compositeDepositId: '0xescrow_12', - escrow: '0xescrow', + compositeDepositId: `8453_${escrowAddress}_12`, + escrow: escrowAddress, depositId: 12n, }, { - compositeDepositId: '0xescrow_19', - escrow: '0xescrow', + compositeDepositId: `8453_${escrowAddress}_19`, + escrow: escrowAddress, depositId: 19n, }, ], diff --git a/developer/cookbook/delegation.md b/developer/cookbook/delegation.md index 3ed37cf..85fcc5a 100644 --- a/developer/cookbook/delegation.md +++ b/developer/cookbook/delegation.md @@ -52,6 +52,11 @@ How to interpret it: ```tsx import { useVaultDelegation } from '@zkp2p/sdk/react'; +const escrowAddress = '0x0000000000000000000000000000000000000001' as const; +const vaultAddress = '0x0000000000000000000000000000000000000002' as const; +const rateManagerId = + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' as const; + const { delegateDeposit, clearDelegation } = useVaultDelegation({ client, sendTransaction: async ({ to, data, value }) => { @@ -60,10 +65,10 @@ const { delegateDeposit, clearDelegation } = useVaultDelegation({ }); await delegateDeposit({ - escrow: '0xEscrowAddress', + escrow: escrowAddress, depositId: 42n, - registry: '0xVaultAddress', - rateManagerId: '0xRateManagerId', + registry: vaultAddress, + rateManagerId, currentRateManagerId, currentRateManagerRegistry: currentRegistry, }); diff --git a/developer/cookbook/extension-deeplinks.md b/developer/cookbook/extension-deeplinks.md index 970b4be..ee0591a 100644 --- a/developer/cookbook/extension-deeplinks.md +++ b/developer/cookbook/extension-deeplinks.md @@ -31,7 +31,7 @@ peerSdk.onramp({ inputAmount: '25', paymentPlatform: 'wise', toToken: '8453:0x0000000000000000000000000000000000000000', - recipientAddress: '0xBuyerAddress', + recipientAddress: '0x0000000000000000000000000000000000000001', }); ``` @@ -45,7 +45,7 @@ Use: ```ts peerSdk.onramp({ - intentHash: '0x1234...abcd', + intentHash: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', }); ``` diff --git a/developer/cookbook/indexer-queries.md b/developer/cookbook/indexer-queries.md index 18af407..4919d29 100644 --- a/developer/cookbook/indexer-queries.md +++ b/developer/cookbook/indexer-queries.md @@ -19,7 +19,7 @@ Use the indexer when you need history, filtering, search, or vault analytics. Us const deposits = await client.indexer.getDeposits( { status: 'ACTIVE', - depositor: '0xMakerAddress', + depositor: '0x0000000000000000000000000000000000000001', acceptingIntents: true, minLiquidity: '1000000', }, @@ -68,12 +68,15 @@ const ownerIntents = await client.indexer.getOwnerIntents(buyerAddress, [ const expired = await client.indexer.getExpiredIntents({ now: Math.floor(Date.now() / 1000).toString(), - depositIds: ['0xescrow_12', '0xescrow_18'], + depositIds: [ + '8453_0x0000000000000000000000000000000000000000_12', + '8453_0x0000000000000000000000000000000000000000_18', + ], limit: 100, }); ``` -Composite deposit IDs are formatted as `escrowAddress_depositId`. +Composite deposit IDs are formatted as `chainId_escrowAddress_depositId`. ## Run a raw GraphQL query @@ -109,11 +112,12 @@ const deposits = await client.indexer.getDepositsWithRelations( { limit: 10 }, ); -const liquidityViews = convertDepositsForLiquidity(deposits, 8453, '0xescrowAddress'); +const escrowAddress = '0x0000000000000000000000000000000000000000' as const; +const liquidityViews = convertDepositsForLiquidity(deposits, 8453, escrowAddress); const firstDepositView = convertIndexerDepositToEscrowView( deposits[0], 8453, - '0xescrowAddress', + escrowAddress, ); const intentViews = convertIndexerIntentsToEscrowViews( deposits.flatMap((deposit) => deposit.intents ?? []), diff --git a/developer/cookbook/prepared-transactions.md b/developer/cookbook/prepared-transactions.md index ae1a35c..48c4c48 100644 --- a/developer/cookbook/prepared-transactions.md +++ b/developer/cookbook/prepared-transactions.md @@ -34,9 +34,9 @@ The main exception is `createDeposit()`, which uses `prepareCreateDeposit()`. const prepared = await client.signalIntent.prepare({ depositId: 42n, amount: 250_000000n, - toAddress: '0xBuyerAddress', + toAddress: '0x0000000000000000000000000000000000000001', processorName: 'wise', - payeeDetails: '0xPayeeDetailsHash', + payeeDetails: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', fiatCurrencyCode: 'USD', conversionRate: 1_020000000000000000n, }); @@ -103,10 +103,13 @@ const { createVault, prepareCreateVault, txHash } = useCreateVault({ referrer: ['acme-wallet'], }); +const manager = '0x0000000000000000000000000000000000000001' as const; +const feeRecipient = '0x0000000000000000000000000000000000000002' as const; + await createVault({ config: { - manager: '0xManager', - feeRecipient: '0xFeeRecipient', + manager, + feeRecipient, maxFee: 50_000000000000000n, fee: 10_000000000000000n, name: 'Acme Vault', diff --git a/developer/cookbook/referral-fees.md b/developer/cookbook/referral-fees.md index 1cbcd6e..bb90ac4 100644 --- a/developer/cookbook/referral-fees.md +++ b/developer/cookbook/referral-fees.md @@ -20,9 +20,10 @@ import { assertValidReferrerFeeConfig, parseReferrerFeeConfig, } from '@zkp2p/sdk'; +import { parseUnits } from 'viem'; const referrerFeeConfig = assertValidReferrerFeeConfig( - parseReferrerFeeConfig('0xReferrerFeeRecipient', 50), + parseReferrerFeeConfig('0x0000000000000000000000000000000000000001', 50), 'getQuote', ); @@ -63,7 +64,7 @@ await client.signalIntent({ processorName: quote.intent.processorName, payeeDetails: quote.intent.payeeDetails, fiatCurrencyCode: quote.intent.fiatCurrencyCode, - conversionRate: BigInt(quote.conversionRate), + conversionRate: parseUnits(quote.conversionRate, 18), referrerFeeConfig, escrowAddress: quote.intent.escrowAddress as `0x${string}` | undefined, orchestratorAddress: quote.intent.orchestratorAddress as `0x${string}` | undefined, @@ -86,7 +87,7 @@ import { const suffix = getAttributionDataSuffix([ZKP2P_IOS_REFERRER, 'acme-wallet']); console.log(BASE_BUILDER_CODE, suffix); -const calldata = encodeWithAttribution( +const attributedCalldata = encodeWithAttribution( { abi: escrowAbi, functionName: 'addFunds', @@ -95,7 +96,8 @@ const calldata = encodeWithAttribution( ['acme-wallet'], ); -const finalCalldata = appendAttributionToCalldata(calldata, 'partner-campaign'); +const existingCalldata = '0x1234' as const; +const campaignCalldata = appendAttributionToCalldata(existingCalldata, 'partner-campaign'); ``` If you are not using SDK write helpers, send the transaction manually: diff --git a/developer/cookbook/taker-tiers.md b/developer/cookbook/taker-tiers.md index 28ae18f..a1b59e7 100644 --- a/developer/cookbook/taker-tiers.md +++ b/developer/cookbook/taker-tiers.md @@ -17,7 +17,7 @@ Use this when you want to show a buyer what they can do before they open a quote ```ts const response = await client.getTakerTier({ - owner: '0xBuyerAddress', + owner: '0x0000000000000000000000000000000000000001', chainId: 8453, }); diff --git a/developer/quickstart.md b/developer/quickstart.md index 92efe23..e242bc0 100644 --- a/developer/quickstart.md +++ b/developer/quickstart.md @@ -22,10 +22,11 @@ Use this if you want the fastest path from "I want to build on Peer" to "I have ## Prerequisites -- Node.js `20+` or Bun +- Node.js `22+` or Bun - A Base RPC URL - A wallet with ETH for gas on Base - For the Node example: a private key for that wallet +- For automatic `signalIntent()` gating signatures: a ZKP2P API key or an equivalent backend that returns `gatingServiceSignature` and `signatureExpiration` :::info Base USDC All examples below use Base USDC: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. @@ -57,7 +58,7 @@ Create `scripts/quickstart.ts`: ```ts import { Zkp2pClient, setLogLevel } from '@zkp2p/sdk'; import { privateKeyToAccount } from 'viem/accounts'; -import { createWalletClient, http } from 'viem'; +import { createWalletClient, http, parseUnits } from 'viem'; import { base } from 'viem/chains'; const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const; @@ -82,15 +83,17 @@ const client = new Zkp2pClient({ walletClient, chainId: base.id, runtimeEnv: 'production', + baseApiUrl: 'https://api.zkp2p.xyz', + apiKey: process.env.ZKP2P_API_KEY, }); ``` ## 3. Read deposits and fetch a quote -`client.getDeposits()` reads deposits owned by the connected wallet. `client.indexer.getDeposits()` is the faster way to inspect public liquidity when you are building a taker flow. +`client.getDeposits()` reads live protocol deposits from ProtocolViewer. Use `client.getAccountDeposits(account.address)` when you only want the connected wallet's deposits. `client.indexer.getDeposits()` is the faster way to inspect public liquidity when you are building a taker flow. ```ts -const myDeposits = await client.getDeposits(); +const myDeposits = await client.getAccountDeposits(account.address); console.log('connected wallet deposits:', myDeposits.length); const publicDeposits = await client.indexer.getDeposits( @@ -143,7 +146,7 @@ const txHash = await client.signalIntent({ processorName: quote.intent.processorName, payeeDetails: quote.intent.payeeDetails, fiatCurrencyCode: quote.intent.fiatCurrencyCode, - conversionRate: BigInt(quote.conversionRate), + conversionRate: parseUnits(quote.conversionRate, 18), escrowAddress: quote.intent.escrowAddress as `0x${string}` | undefined, orchestratorAddress: quote.intent.orchestratorAddress as `0x${string}` | undefined, }); @@ -156,6 +159,7 @@ Run it: ```bash PRIVATE_KEY=0x... \ RPC_URL=https://base-mainnet.g.alchemy.com/v2/your-key \ +ZKP2P_API_KEY=your-api-key \ bun run scripts/quickstart.ts ``` @@ -170,7 +174,7 @@ import { } from '@zkp2p/sdk'; import { useSignalIntent } from '@zkp2p/sdk/react'; import type { Address } from 'viem'; -import { createWalletClient, custom } from 'viem'; +import { createWalletClient, custom, parseUnits } from 'viem'; import { base } from 'viem/chains'; import { useEffect, useState } from 'react'; @@ -242,7 +246,7 @@ export default function App() { processorName: quote.intent.processorName, payeeDetails: quote.intent.payeeDetails, fiatCurrencyCode: quote.intent.fiatCurrencyCode, - conversionRate: BigInt(quote.conversionRate), + conversionRate: parseUnits(quote.conversionRate, 18), escrowAddress: quote.intent.escrowAddress as `0x${string}` | undefined, orchestratorAddress: quote.intent.orchestratorAddress as `0x${string}` | undefined, }); diff --git a/developer/sdk/client-reference.md b/developer/sdk/client-reference.md index c09a075..574fb19 100644 --- a/developer/sdk/client-reference.md +++ b/developer/sdk/client-reference.md @@ -25,7 +25,7 @@ Create a client with `new Zkp2pClient(opts)`. | `runtimeEnv` | No | Runtime environment: `production`, `preproduction`, or `staging`. Defaults to `production` | | `indexerUrl` | No | Override for the indexer GraphQL endpoint | | `baseApiUrl` | No | Override for ZKP2P service APIs | -| `apiKey` | No | Optional curator API key — not required for any method. When provided, some responses are enriched (e.g. `getQuote` returns maker `depositData`) | +| `apiKey` | No | Optional curator API key — not required for any method. When provided, some responses are enriched (e.g. `getQuote` returns maker `payeeData`) | | `authorizationToken` | No | Optional bearer token for hybrid authentication | | `getAuthorizationToken` | No | Async token provider for long-lived clients | | `indexerApiKey` | No | Optional `x-api-key` header for indexer proxy authentication | @@ -41,7 +41,7 @@ const client = new Zkp2pClient({ ``` :::info No API key required -All SDK methods work without `apiKey` or `authorizationToken`. Auth credentials are optional and only affect response richness. `signalIntent()` can auto-fetch its gating signature when you provide `apiKey` or `authorizationToken`; if you do not want the SDK to make that request, pass `gatingServiceSignature` and `signatureExpiration` yourself. +All SDK methods work without `apiKey` or `authorizationToken` when you provide any required signatures yourself. Auth credentials are optional and mostly affect response richness. `signalIntent()` can auto-fetch its gating signature when you provide `baseApiUrl` plus `apiKey` or `authorizationToken`; if you do not want the SDK to make that request, pass `gatingServiceSignature` and `signatureExpiration` yourself. ::: ## Prepared transactions @@ -55,9 +55,9 @@ Most write methods are "prepareable": const prepared = await client.signalIntent.prepare({ depositId: 42n, amount: 100_000000n, - toAddress: '0xYourRecipientAddress', + toAddress: '0x0000000000000000000000000000000000000001', processorName: 'wise', - payeeDetails: '0xPayeeHash', + payeeDetails: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', fiatCurrencyCode: 'USD', conversionRate: 1_020000000000000000n, }); @@ -77,7 +77,7 @@ const { depositDetails, prepared } = await client.prepareCreateDeposit({ amount: 1_000_000000n, intentAmountRange: { min: 10_000000n, max: 500_000000n }, processorNames: ['wise'], - depositData: [{ email: 'maker@example.com' }], + payeeData: [{ offchainId: 'maker@example.com' }], conversionRates: [[ { currency: 'USD', conversionRate: '1020000000000000000' }, ]], @@ -91,14 +91,15 @@ Use `registerPayeeDetails()` when you want to register payment details first and | Parameter | Type | Description | | --- | --- | --- | | `processorNames` | `string[]` | Payment platforms such as `wise`, `revolut`, or `venmo` | -| `depositData` | `Array>` | Processor-specific payment details in the same order as `processorNames` | +| `payeeData` | `Array<{ offchainId: string; telegramUsername?: string \| null; metadata?: Record \| null }>` | Processor-specific payment details in the same order as `processorNames` | +| `depositData` | Same shape as `payeeData` | Backward-compatible alias for `payeeData` | ```ts const { hashedOnchainIds } = await client.registerPayeeDetails({ processorNames: ['wise', 'revolut'], - depositData: [ - { email: 'maker@example.com' }, - { tag: '@maker' }, + payeeData: [ + { offchainId: 'maker@example.com' }, + { offchainId: '@maker' }, ], }); @@ -107,9 +108,9 @@ await client.createDeposit({ amount: 1_000_000000n, intentAmountRange: { min: 10_000000n, max: 500_000000n }, processorNames: ['wise', 'revolut'], - depositData: [ - { email: 'maker@example.com' }, - { tag: '@maker' }, + payeeData: [ + { offchainId: 'maker@example.com' }, + { offchainId: '@maker' }, ], conversionRates: [ [{ currency: 'USD', conversionRate: '1020000000000000000' }], @@ -355,8 +356,8 @@ The response includes: const quote = await client.getQuote({ paymentPlatforms: ['wise'], fiatCurrency: 'USD', - user: '0xYourAddress', - recipient: '0xRecipientAddress', + user: '0x0000000000000000000000000000000000000001', + recipient: '0x0000000000000000000000000000000000000002', destinationChainId: 8453, destinationToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', amount: '100', @@ -394,8 +395,8 @@ The response shape mirrors `getQuote` but is keyed by platform: ```ts const best = await client.getQuotesBestByPlatform({ fiatCurrency: 'USD', - user: '0xYourAddress', - recipient: '0xRecipientAddress', + user: '0x0000000000000000000000000000000000000001', + recipient: '0x0000000000000000000000000000000000000002', destinationChainId: 8453, destinationToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', amount: '100', diff --git a/developer/sdk/overview.md b/developer/sdk/overview.md index 21d120a..3419901 100644 --- a/developer/sdk/overview.md +++ b/developer/sdk/overview.md @@ -8,7 +8,7 @@ slug: /sdk ## What this does -`@zkp2p/sdk` is the TypeScript SDK for building with Peer. Use it to manage deposits, signal and fulfill intents, access quote and taker-tier APIs, work with vault and rate-manager flows, and embed the Peer extension onramp. The current npm release is `0.3.0` and is published under the MIT license. +`@zkp2p/sdk` is the TypeScript SDK for building with Peer. Use it to manage deposits, signal and fulfill intents, access quote and taker-tier APIs, work with vault and rate-manager flows, and embed the Peer extension onramp. The current npm release is `0.3.2` and is published under the MIT license. ## Who is this for? diff --git a/developer/sdk/react-hooks.md b/developer/sdk/react-hooks.md index 781a519..17f9085 100644 --- a/developer/sdk/react-hooks.md +++ b/developer/sdk/react-hooks.md @@ -217,7 +217,7 @@ The hooks package re-exports several delegation helpers and types. | `isZeroRateManagerId(value)` | Checks whether a vault ID is the zero ID | | `normalizeRateManagerId(value)` | Normalizes a vault ID string | | `normalizeRegistry(value)` | Normalizes a registry address string | -| `getDelegationRoute(client, escrow)` | Returns whether the deposit should use the `legacy` or `v2` delegation path | +| `getDelegationRoute(client, escrow)` | Returns the current delegation route. In `0.3.2`, this resolves to direct `v2` delegation | | `classifyDelegationState(currentRateManagerId, currentRegistry, targetRateManagerId, targetRegistry)` | Classifies whether a deposit is delegated here, elsewhere, or not delegated | Useful exported types: diff --git a/developer/tutorials/maker-bot.md b/developer/tutorials/maker-bot.md index 7f6cbbf..ee55f42 100644 --- a/developer/tutorials/maker-bot.md +++ b/developer/tutorials/maker-bot.md @@ -21,7 +21,7 @@ If you operate both sides of the flow, you can still call `client.fulfillIntent( ## Prerequisites -- Node.js 20+ or Bun +- Node.js 22+ or Bun - A Base RPC URL - A maker wallet with ETH for gas and USDC for liquidity - Off-chain logic that watches incoming fiat payments @@ -188,7 +188,7 @@ async function pruneExpired() { }); for (const intent of expired) { - const [escrowAddress, rawDepositId] = intent.depositId.split('_'); + const [, escrowAddress, rawDepositId] = intent.depositId.split('_'); if (!escrowAddress || !rawDepositId) continue; console.log('pruning expired intent', intent.intentHash); @@ -274,7 +274,7 @@ When your system receives a valid proof, call `fulfillIntent()` from the taker w ```ts await client.fulfillIntent({ - intentHash: '0x...', + intentHash: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', proof: proofFromPeerAuthOrReclaim, }); ``` diff --git a/developer/tutorials/onramp-widget.md b/developer/tutorials/onramp-widget.md index 3ff9073..353bde6 100644 --- a/developer/tutorials/onramp-widget.md +++ b/developer/tutorials/onramp-widget.md @@ -86,12 +86,11 @@ Create `src/components/OnrampWidget.tsx`: ```tsx import { createPeerExtensionSdk, - getTierDisplayInfo, type PeerExtensionState, type PeerIntentFulfilledResult, type Zkp2pClient, } from '@zkp2p/sdk'; -import { useGetTakerTier } from '@zkp2p/sdk/react'; +import { getTierDisplayInfo, useGetTakerTier } from '@zkp2p/sdk/react'; import { base } from 'viem/chains'; import { useEffect, useMemo, useState } from 'react'; import { createBrowserClient } from '../lib/peer';