From 55423433ba16aa6fa5c13646323a3af00623a68b Mon Sep 17 00:00:00 2001 From: Olaleye Blessing Date: Fri, 3 Jul 2026 14:35:13 +0100 Subject: [PATCH 1/5] feat: migrate Peer onramp to headless ZKP2P flow (SDK 0.5.2) Peer removed the extension-owned onramp API (onramp(), onIntentFulfilled()) in extension 0.6.0, breaking the old integration. Rebuild it on the headless Buyer TEE flow where the app owns the order lifecycle: - use-peer-onramp: quote (destination Gnosis xDAI) -> signalIntent via prepare() + Privy-sponsored tx on Base -> user pays fiat -> extension metadata capture + payment row matching -> fulfillIntent (sponsored) -> poll for bridged xDAI arrival -> sponsored BREAD mint. Supports Wise, Revolut and Venmo with per-platform buyer TEE configs. - peer-onramp-modal: multi-step UI (platform/currency/amount, payment instructions, capture/verify/bridge/mint states), wired via new PEER_ONRAMP modal state. Completion is self-contained and does not depend on useWatchFundedXdai. - fund-with-peer becomes a thin launcher; install-extension flow kept. - Upgrade @zkp2p/sdk 0.3.2 -> 0.5.2 (newest release passing the 10-day minimumReleaseAge policy; headless capture needs >=0.5.0). - pnpm overrides: force provenance-signed undici-types and @noble/hashes (scoped to @walletconnect/relay-auth) to satisfy trustPolicy no-downgrade. Requires Peer extension >=0.6.3 and Privy gas sponsorship enabled on Base; bump SDK to 0.7.x once it clears the release-age window. Co-Authored-By: Claude Fable 5 --- package.json | 2 +- pnpm-lock.yaml | 406 ++++++++++++--- pnpm-workspace.yaml | 5 + src/components/modal/context.tsx | 6 + .../modal/modals/fund-wallet/fund-wallet.tsx | 20 +- .../modals/fund-wallet/fund-with-peer.tsx | 113 +--- .../modals/fund-wallet/peer-onramp-modal.tsx | 208 ++++++++ src/components/modal/presenter.tsx | 4 + src/hooks/use-peer-onramp.ts | 493 ++++++++++++++++++ 9 files changed, 1059 insertions(+), 198 deletions(-) create mode 100644 src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx create mode 100644 src/hooks/use-peer-onramp.ts diff --git a/package.json b/package.json index de134cda..afceedf7 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@tanstack/react-query": "^5.85.6", "@upstash/redis": "^1.36.2", "@wagmi/core": "2.x.x", - "@zkp2p/sdk": "^0.3.2", + "@zkp2p/sdk": "^0.5.2", "clsx": "^2.1.1", "embla-carousel-react": "^8.3.1", "next": "15.4.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e8faf51..0532967d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,8 @@ overrides: "@noble/curves": ">=1.9.0" semver: ">=6.3.2" "@breadcoop/ui": 2.0.1 + undici-types: 6.27.0 + "@walletconnect/relay-auth>@noble/hashes": ">=1.8.0" importers: .: @@ -70,8 +72,8 @@ importers: specifier: 2.x.x version: 2.22.1(@tanstack/query-core@5.91.2)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6)) "@zkp2p/sdk": - specifier: ^0.3.2 - version: 0.3.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6) + specifier: ^0.5.2 + version: 0.5.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -185,6 +187,12 @@ packages: integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==, } + "@adraffy/ens-normalize@1.11.0": + resolution: + { + integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==, + } + "@adraffy/ens-normalize@1.11.1": resolution: { @@ -1803,10 +1811,10 @@ packages: } engines: { node: ">= 16" } - "@noble/hashes@1.7.0": + "@noble/hashes@1.5.0": resolution: { - integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==, + integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==, } engines: { node: ^14.21.3 || >=16 } @@ -1866,6 +1874,72 @@ packages: } deprecated: 'The package is now available as "qr": npm install qr' + "@peculiar/asn1-cms@2.5.0": + resolution: + { + integrity: sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==, + } + + "@peculiar/asn1-csr@2.5.0": + resolution: + { + integrity: sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==, + } + + "@peculiar/asn1-ecc@2.5.0": + resolution: + { + integrity: sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==, + } + + "@peculiar/asn1-pfx@2.5.0": + resolution: + { + integrity: sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==, + } + + "@peculiar/asn1-pkcs8@2.5.0": + resolution: + { + integrity: sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==, + } + + "@peculiar/asn1-pkcs9@2.5.0": + resolution: + { + integrity: sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==, + } + + "@peculiar/asn1-rsa@2.5.0": + resolution: + { + integrity: sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==, + } + + "@peculiar/asn1-schema@2.5.0": + resolution: + { + integrity: sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==, + } + + "@peculiar/asn1-x509-attr@2.5.0": + resolution: + { + integrity: sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==, + } + + "@peculiar/asn1-x509@2.5.0": + resolution: + { + integrity: sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==, + } + + "@peculiar/x509@1.14.0": + resolution: + { + integrity: sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==, + } + "@phosphor-icons/react@2.1.10": resolution: { @@ -2956,6 +3030,12 @@ packages: integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==, } + "@scure/bip32@1.5.0": + resolution: + { + integrity: sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==, + } + "@scure/bip32@1.6.2": resolution: { @@ -2980,6 +3060,12 @@ packages: integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==, } + "@scure/bip39@1.4.0": + resolution: + { + integrity: sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==, + } + "@scure/bip39@1.5.4": resolution: { @@ -4760,10 +4846,10 @@ packages: integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==, } - "@zkp2p/contracts-v2@0.2.1": + "@zkp2p/contracts-v2@0.2.2": resolution: { - integrity: sha512-tpxtrBV9GmTcdtUGzzo5g3U0H1jJA9hnMd7VSu+oM7WY0cH4U6tghvyBzPQcmddwHnG63qm2T5P/jy7Cb1uQ1w==, + integrity: sha512-mI7v491SkHjFHqXnSTFkzNaFPSiVli1Czb1D943tNWAMLCERZaBGQTZe12HJP5kZQpDVndrI1GnnPrB0BU7lFQ==, } peerDependencies: ethers: ^5.0.0 || ^6.0.0 @@ -4775,10 +4861,10 @@ packages: } engines: { node: ">=16" } - "@zkp2p/sdk@0.3.2": + "@zkp2p/sdk@0.5.2": resolution: { - integrity: sha512-xaTug1rRdjNqhSKlWtUXRqM1exiQAbrPC1dJrJlMcCdV18gMapYfMzV1NYwWqxLHp666MaDHMrcdqi3fExfe0Q==, + integrity: sha512-RmFzlxYW526oK1GFQ6fj4Aa56tccO0UgQlDnD1WoFH00FwIDxo44bFaCg7FDJSqrqsz5lqhfmwcl3mdCdvGTVw==, } engines: { node: ">=22" } peerDependencies: @@ -4788,6 +4874,14 @@ packages: react: optional: true + "@zkp2p/zkp2p-attestation@1.5.1": + resolution: + { + integrity: sha512-s4JkhGQCPONomQxfKa9CNAgFrHiNnUO/sgdz4brxu2qrvbhF0hYuM8pGbsWDnXt24Yjqv2i75z4BGzNEj1Cb2Q==, + } + engines: { node: ">=20" } + hasBin: true + abitype@1.0.6: resolution: { @@ -5045,6 +5139,13 @@ packages: integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==, } + asn1js@3.0.6: + resolution: + { + integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==, + } + engines: { node: ">=12.0.0" } + ast-types-flow@0.0.8: resolution: { @@ -6429,10 +6530,10 @@ packages: integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==, } - ethers@6.16.0: + ethers@6.13.4: resolution: { - integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==, + integrity: sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==, } engines: { node: ">=14.0.0" } @@ -8951,6 +9052,19 @@ packages: } engines: { node: ">=6" } + pvtsutils@1.3.6: + resolution: + { + integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==, + } + + pvutils@1.1.3: + resolution: + { + integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==, + } + engines: { node: ">=6.0.0" } + qr@0.5.5: resolution: { @@ -9245,6 +9359,12 @@ packages: } engines: { node: ">= 12.13.0" } + reflect-metadata@0.2.2: + resolution: + { + integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==, + } + reflect.getprototypeof@1.0.10: resolution: { @@ -10086,6 +10206,13 @@ packages: integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, } + tsyringe@4.10.0: + resolution: + { + integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==, + } + engines: { node: ">= 6.0.0" } + type-check@0.4.0: resolution: { @@ -10188,22 +10315,10 @@ packages: integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==, } - undici-types@6.19.8: - resolution: - { - integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==, - } - - undici-types@6.21.0: - resolution: - { - integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, - } - - undici-types@7.24.5: + undici-types@6.27.0: resolution: { - integrity: sha512-kNh333UBSbgK35OIW7FwJTr9tTfVIG51Fm1tSVT7m8foPHfDVjsb7OIee/q/rs3bB2aV/3qOPgG5mHNWl1odiA==, + integrity: sha512-HDBmCak1Ccswl7xS6/blapTYRVsqva4skTCeeJPLiDn6qCszeqiPGDljQBH2Kf/S0BI6EN14FQsgKVyGid9xnA==, } unpipe@1.0.0: @@ -10893,6 +11008,8 @@ snapshots: "@adraffy/ens-normalize@1.10.1": {} + "@adraffy/ens-normalize@1.11.0": {} + "@adraffy/ens-normalize@1.11.1": {} "@alloc/quick-lru@5.2.0": {} @@ -12119,8 +12236,8 @@ snapshots: dependencies: "@ethereumjs/tx": 4.2.0 "@metamask/superstruct": 3.2.1 - "@noble/hashes": 1.8.0 - "@scure/base": 1.2.6 + "@noble/hashes": 1.5.0 + "@scure/base": 1.1.9 "@types/debug": 4.1.13 "@types/lodash": 4.17.24 debug: 4.4.3 @@ -12145,8 +12262,8 @@ snapshots: dependencies: "@ethereumjs/tx": 4.2.0 "@metamask/superstruct": 3.2.1 - "@noble/hashes": 1.8.0 - "@scure/base": 1.2.6 + "@noble/hashes": 1.5.0 + "@scure/base": 1.1.9 "@types/debug": 4.1.13 debug: 4.4.3 pony-cause: 2.1.11 @@ -12159,8 +12276,8 @@ snapshots: dependencies: "@ethereumjs/tx": 4.2.0 "@metamask/superstruct": 3.2.1 - "@noble/hashes": 1.8.0 - "@scure/base": 1.2.6 + "@noble/hashes": 1.5.0 + "@scure/base": 1.1.9 "@types/debug": 4.1.12 debug: 4.3.4 pony-cause: 2.1.11 @@ -12444,7 +12561,7 @@ snapshots: "@noble/hashes@1.4.0": {} - "@noble/hashes@1.7.0": {} + "@noble/hashes@1.5.0": {} "@noble/hashes@1.7.1": {} @@ -12468,6 +12585,96 @@ snapshots: "@paulmillr/qr@0.2.1": {} + "@peculiar/asn1-cms@2.5.0": + dependencies: + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + "@peculiar/asn1-x509-attr": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-csr@2.5.0": + dependencies: + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-ecc@2.5.0": + dependencies: + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-pfx@2.5.0": + dependencies: + "@peculiar/asn1-cms": 2.5.0 + "@peculiar/asn1-pkcs8": 2.5.0 + "@peculiar/asn1-rsa": 2.5.0 + "@peculiar/asn1-schema": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-pkcs8@2.5.0": + dependencies: + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-pkcs9@2.5.0": + dependencies: + "@peculiar/asn1-cms": 2.5.0 + "@peculiar/asn1-pfx": 2.5.0 + "@peculiar/asn1-pkcs8": 2.5.0 + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + "@peculiar/asn1-x509-attr": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-rsa@2.5.0": + dependencies: + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-schema@2.5.0": + dependencies: + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + "@peculiar/asn1-x509-attr@2.5.0": + dependencies: + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + "@peculiar/asn1-x509@2.5.0": + dependencies: + "@peculiar/asn1-schema": 2.5.0 + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + "@peculiar/x509@1.14.0": + dependencies: + "@peculiar/asn1-cms": 2.5.0 + "@peculiar/asn1-csr": 2.5.0 + "@peculiar/asn1-ecc": 2.5.0 + "@peculiar/asn1-pkcs9": 2.5.0 + "@peculiar/asn1-rsa": 2.5.0 + "@peculiar/asn1-schema": 2.5.0 + "@peculiar/asn1-x509": 2.5.0 + pvtsutils: 1.3.6 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + tsyringe: 4.10.0 + "@phosphor-icons/react@2.1.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0)": dependencies: react: 19.1.0 @@ -14034,6 +14241,12 @@ snapshots: "@noble/hashes": 1.4.0 "@scure/base": 1.1.9 + "@scure/bip32@1.5.0": + dependencies: + "@noble/curves": 1.9.1 + "@noble/hashes": 1.5.0 + "@scure/base": 1.1.9 + "@scure/bip32@1.6.2": dependencies: "@noble/curves": 1.9.1 @@ -14057,6 +14270,11 @@ snapshots: "@noble/hashes": 1.4.0 "@scure/base": 1.1.9 + "@scure/bip39@1.4.0": + dependencies: + "@noble/hashes": 1.5.0 + "@scure/base": 1.1.9 + "@scure/bip39@1.5.4": dependencies: "@noble/hashes": 1.7.1 @@ -14509,7 +14727,7 @@ snapshots: "@solana/errors": 5.5.1(typescript@5.9.3) "@solana/rpc-spec": 5.5.1(typescript@5.9.3) "@solana/rpc-spec-types": 5.5.1(typescript@5.9.3) - undici-types: 7.24.5 + undici-types: 6.27.0 optionalDependencies: typescript: 5.9.3 @@ -14876,7 +15094,7 @@ snapshots: "@types/connect@3.4.38": dependencies: - "@types/node": 20.19.37 + "@types/node": 22.7.5 "@types/debug@4.1.12": dependencies: @@ -14912,11 +15130,11 @@ snapshots: "@types/node@20.19.37": dependencies: - undici-types: 6.21.0 + undici-types: 6.27.0 "@types/node@22.7.5": dependencies: - undici-types: 6.19.8 + undici-types: 6.27.0 "@types/parse-json@4.0.2": {} @@ -14944,11 +15162,11 @@ snapshots: "@types/ws@7.4.7": dependencies: - "@types/node": 20.19.37 + "@types/node": 22.7.5 "@types/ws@8.18.1": dependencies: - "@types/node": 20.19.37 + "@types/node": 22.7.5 "@types/yargs-parser@21.0.3": {} @@ -15807,7 +16025,7 @@ snapshots: "@walletconnect/relay-auth@1.1.0": dependencies: "@noble/curves": 1.9.1 - "@noble/hashes": 1.7.0 + "@noble/hashes": 1.8.0 "@walletconnect/safe-json": 1.0.2 "@walletconnect/time": 1.0.2 uint8arrays: 3.1.1 @@ -16618,7 +16836,7 @@ snapshots: dependencies: "@msgpack/msgpack": 3.1.2 "@noble/ciphers": 1.3.0 - "@noble/curves": 1.9.7 + "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 "@scure/base": 1.2.6 "@walletconnect/jsonrpc-utils": 1.0.8 @@ -16668,21 +16886,22 @@ snapshots: "@walletconnect/window-getters": 1.0.1 tslib: 1.14.1 - "@zkp2p/contracts-v2@0.2.1(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6)": + "@zkp2p/contracts-v2@0.2.2(ethers@6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6)": dependencies: - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) - ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + abitype: 1.0.6(typescript@5.9.3)(zod@4.3.6) + ethers: 6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - typescript - zod "@zkp2p/indexer-schema@0.2.3": {} - "@zkp2p/sdk@0.3.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6)": + "@zkp2p/sdk@0.5.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6)": dependencies: - "@zkp2p/contracts-v2": 0.2.1(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6) + "@zkp2p/contracts-v2": 0.2.2(ethers@6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6) "@zkp2p/indexer-schema": 0.2.3 - ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + "@zkp2p/zkp2p-attestation": 1.5.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ethers: 6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) ox: 0.11.3(typescript@5.9.3)(zod@4.3.6) viem: 2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6) optionalDependencies: @@ -16693,11 +16912,24 @@ snapshots: - utf-8-validate - zod + "@zkp2p/zkp2p-attestation@1.5.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)": + dependencies: + "@peculiar/x509": 1.14.0 + ethers: 6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + abitype@1.0.6(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 zod: 3.25.76 + abitype@1.0.6(typescript@5.9.3)(zod@4.3.6): + optionalDependencies: + typescript: 5.9.3 + zod: 4.3.6 + abitype@1.0.8(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 @@ -16867,6 +17099,12 @@ snapshots: asap@2.0.6: {} + asn1js@3.0.6: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.3 + tslib: 2.8.1 + ast-types-flow@0.0.8: {} async-function@1.0.0: {} @@ -17777,7 +18015,7 @@ snapshots: "@scure/bip32": 1.4.0 "@scure/bip39": 1.3.0 - ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + ethers@6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6): dependencies: "@adraffy/ens-normalize": 1.10.1 "@noble/curves": 1.9.1 @@ -18992,7 +19230,7 @@ snapshots: ox@0.11.3(typescript@5.9.3)(zod@4.3.6): dependencies: - "@adraffy/ens-normalize": 1.11.1 + "@adraffy/ens-normalize": 1.11.0 "@noble/ciphers": 1.3.0 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 @@ -19052,12 +19290,12 @@ snapshots: ox@0.6.7(typescript@5.9.3)(zod@3.25.76): dependencies: - "@adraffy/ens-normalize": 1.11.1 + "@adraffy/ens-normalize": 1.10.1 "@noble/curves": 1.9.1 - "@noble/hashes": 1.8.0 - "@scure/bip32": 1.7.0 - "@scure/bip39": 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + "@noble/hashes": 1.5.0 + "@scure/bip32": 1.5.0 + "@scure/bip39": 1.4.0 + abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -19066,12 +19304,12 @@ snapshots: ox@0.6.7(typescript@5.9.3)(zod@4.3.6): dependencies: - "@adraffy/ens-normalize": 1.11.1 + "@adraffy/ens-normalize": 1.10.1 "@noble/curves": 1.9.1 - "@noble/hashes": 1.8.0 - "@scure/bip32": 1.7.0 - "@scure/bip39": 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + "@noble/hashes": 1.5.0 + "@scure/bip32": 1.5.0 + "@scure/bip39": 1.4.0 + abitype: 1.0.6(typescript@5.9.3)(zod@4.3.6) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -19080,12 +19318,12 @@ snapshots: ox@0.6.9(typescript@5.9.3)(zod@3.25.76): dependencies: - "@adraffy/ens-normalize": 1.11.1 - "@noble/curves": 1.9.7 - "@noble/hashes": 1.8.0 - "@scure/bip32": 1.7.0 - "@scure/bip39": 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + "@adraffy/ens-normalize": 1.10.1 + "@noble/curves": 1.9.1 + "@noble/hashes": 1.5.0 + "@scure/bip32": 1.5.0 + "@scure/bip39": 1.4.0 + abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -19094,12 +19332,12 @@ snapshots: ox@0.6.9(typescript@5.9.3)(zod@4.3.6): dependencies: - "@adraffy/ens-normalize": 1.11.1 - "@noble/curves": 1.9.7 - "@noble/hashes": 1.8.0 - "@scure/bip32": 1.7.0 - "@scure/bip39": 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + "@adraffy/ens-normalize": 1.10.1 + "@noble/curves": 1.9.1 + "@noble/hashes": 1.5.0 + "@scure/bip32": 1.5.0 + "@scure/bip39": 1.4.0 + abitype: 1.0.6(typescript@5.9.3)(zod@4.3.6) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -19108,13 +19346,13 @@ snapshots: ox@0.9.1(typescript@5.9.3)(zod@4.3.6): dependencies: - "@adraffy/ens-normalize": 1.11.1 + "@adraffy/ens-normalize": 1.11.0 "@noble/ciphers": 1.3.0 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 "@scure/bip32": 1.7.0 "@scure/bip39": 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + abitype: 1.0.8(typescript@5.9.3)(zod@4.3.6) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -19123,7 +19361,7 @@ snapshots: ox@0.9.17(typescript@5.9.3)(zod@4.3.6): dependencies: - "@adraffy/ens-normalize": 1.11.1 + "@adraffy/ens-normalize": 1.11.0 "@noble/ciphers": 1.3.0 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 @@ -19138,7 +19376,7 @@ snapshots: ox@0.9.3(typescript@5.9.3)(zod@4.3.6): dependencies: - "@adraffy/ens-normalize": 1.11.1 + "@adraffy/ens-normalize": 1.11.0 "@noble/ciphers": 1.3.0 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 @@ -19388,6 +19626,12 @@ snapshots: punycode@2.3.1: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.3: {} + qr@0.5.5: {} qrcode@1.5.3: @@ -19607,6 +19851,8 @@ snapshots: real-require@0.2.0: {} + reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -19669,9 +19915,9 @@ snapshots: "@types/uuid": 10.0.0 "@types/ws": 8.18.1 buffer: 6.0.3 - eventemitter3: 5.0.4 + eventemitter3: 5.0.1 uuid: 11.1.0 - ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: bufferutil: 4.1.0 utf-8-validate: 6.0.6 @@ -20156,6 +20402,10 @@ snapshots: tslib@2.8.1: {} + tsyringe@4.10.0: + dependencies: + tslib: 1.14.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -20222,11 +20472,7 @@ snapshots: uncrypto@0.1.3: {} - undici-types@6.19.8: {} - - undici-types@6.21.0: {} - - undici-types@7.24.5: {} + undici-types@6.27.0: {} unpipe@1.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5c98001c..37958e98 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -21,3 +21,8 @@ overrides: "@noble/curves": ">=1.9.0" "semver": ">=6.3.2" "@breadcoop/ui": "2.0.1" + # 6.19.8 (via ethers -> @types/node) has no provenance attestation; force a signed release + "undici-types": "6.27.0" + # 1.7.0 (via @walletconnect/relay-auth) has no provenance attestation; force a signed release + # (scoped: a blanket override breaks ethereum-cryptography's internal _assert import) + "@walletconnect/relay-auth>@noble/hashes": ">=1.8.0" diff --git a/src/components/modal/context.tsx b/src/components/modal/context.tsx index 7a5b9c90..1b89d900 100644 --- a/src/components/modal/context.tsx +++ b/src/components/modal/context.tsx @@ -126,6 +126,11 @@ export type PeerOnRampInstallModalState = { fundWalletModalState: Omit; }; +export type PeerOnrampModalState = { + type: "PEER_ONRAMP"; + fundWalletModalState: Omit; +}; + export type ModalState = | DepositInitModalState | DepositLoadingModalState @@ -144,6 +149,7 @@ export type ModalState = | FundWalletModalState | LiFiBridgeSwapModalState | PeerOnRampInstallModalState + | PeerOnrampModalState | FundWithConnectedWalletModalAmountModalState | AutomaticClaimsModalState | null; diff --git a/src/components/modal/modals/fund-wallet/fund-wallet.tsx b/src/components/modal/modals/fund-wallet/fund-wallet.tsx index 82d2caac..daf1ca8c 100644 --- a/src/components/modal/modals/fund-wallet/fund-wallet.tsx +++ b/src/components/modal/modals/fund-wallet/fund-wallet.tsx @@ -7,10 +7,9 @@ import { useEffect, useState } from "react"; import { useWatchFundedXdai } from "@/hooks/use-watch-funded-xdai"; import { Body, useConnectedUser } from "@breadcoop/ui"; import Alert from "@/components/alert"; -import { PeerIntentFulfilledResult } from "@zkp2p/sdk"; +import FundWithPeer from "./fund-with-peer"; const FundWallet = ({ modalState }: { modalState: FundWalletModalState }) => { - const [result, _setResult] = useState(null); const { setModal } = useModal(); const { user } = useConnectedUser(); @@ -37,29 +36,20 @@ const FundWallet = ({ modalState }: { modalState: FundWalletModalState }) => { return ( - + Send xDAI to your wallet and automatically get BREAD -
- {result && ( -
-

Peer Onramp result:

-

{JSON.stringify(result, undefined, 2)}

-
- )} -
-
+
- {/* */} + />
& { - setResult: Dispatch>; -}; - -const XDAI_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000"; -const XDAI_DESTINATION_TOKEN = `100:${XDAI_TOKEN_ADDRESS}`; +type FundWithPeerProps = Omit & {}; -const FundWithPeer = ({ - setResult, - ...fundWalletModalState -}: FundWithPeerProps) => { +const FundWithPeer = ({ ...fundWalletModalState }: FundWithPeerProps) => { const { setModal } = useModal(); - // const { sendSponsoredTransaction } = useSponsoredTx(); - // const { waitForTxReceipt } = useWaitForTxReceipt(); const peerSdkRef = useRef | null>( null ); - const peerUnsubscribe = useRef<(() => void) | null>(null); useEffect(() => { if (typeof window === "undefined") return; peerSdkRef.current = createPeerExtensionSdk({ window }); - - return () => { - peerUnsubscribe.current?.(); - }; }, []); - // const mintBread = async (amount: bigint) => { - // setModal({ type: "WALLET_FUNDING_STATUS", status: "loading" }); - - // try { - // const data = encodeFunctionData({ - // abi: breadAbi, - // functionName: "mint", - // args: [fundWalletModalState.address], - // }); - - // const { hash } = await sendSponsoredTransaction( - // { - // to: clientEnv.NEXT_PUBLIC_BREAD_TOKEN_ADDRESS, - // data, - // value: amount, - // }, - // { uiOptions: { showWalletUIs: false } } - // ); - - // setModal({ type: "WALLET_FUNDING_STATUS", status: "success" }); - // void (await waitForTxReceipt(hash)); - // } catch (error) { - // console.error("Peer onramp: minting bread error:", error); - // setModal({ type: "WALLET_FUNDING_STATUS", status: "error" }); - // } - // }; - const handleOnramp = async () => { const sdk = peerSdkRef.current; if (!sdk) { @@ -95,59 +47,16 @@ const FundWithPeer = ({ if (!approved) return; // user declined } - // Register before calling onramp() per SDK requirements. - peerUnsubscribe.current?.(); - peerUnsubscribe.current = sdk.onIntentFulfilled(async (result) => { - if (result.bridge.status === "pending") { - console.log("Bridge pending:", result.bridge.trackingUrl); - return; - } - - console.log("__ PEER BRIDGE RESULT __", result); - setResult(result); - - if (result.destinationToken !== XDAI_DESTINATION_TOKEN) { - console.error( - "Peer onramp: unexpected destination token:", - result.destinationToken - ); - return; - } - - if (result.recipientAddress !== fundWalletModalState.address) { - console.error( - "Peer onramp: recipient address mismatch:", - result.recipientAddress - ); - return; - } - - const outputAmount = result.bridge.outputAmount; - if (!outputAmount) { - console.error("Peer onramp: missing outputAmount on fulfilled result"); - return; - } - - // await mintBread(BigInt(outputAmount)); - }); - - sdk.onramp({ - toToken: XDAI_DESTINATION_TOKEN, - referrer: "Stacks", - referrerLogo: "https://fund.bread.coop/logo.svg", - recipientAddress: fundWalletModalState.address, - }); + setModal({ type: "PEER_ONRAMP", fundWalletModalState }); }; return ( - <> - - + ); }; diff --git a/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx b/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx new file mode 100644 index 00000000..b32fe744 --- /dev/null +++ b/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx @@ -0,0 +1,208 @@ +"use client"; + +import { useState } from "react"; +import { Body } from "@breadcoop/ui"; +import { Label } from "@/components/label"; +import NumericInput from "@/components/numeric-input"; +import LocalButton from "@/components/button"; +import Loading from "@/app/loading"; +import { ModalContainer, ModalHeader } from "../../components"; +import { PeerOnrampModalState, useModal } from "../../context"; +import { + PEER_PLATFORMS, + PEER_PLATFORM_CONFIG, + PeerPlatform, + usePeerOnramp, +} from "@/hooks/use-peer-onramp"; + +const PeerOnrampModal = ({ + modalState, +}: { + modalState: PeerOnrampModalState; +}) => { + const { setModal } = useModal(); + const address = modalState.fundWalletModalState.address; + + const [platform, setPlatform] = useState("wise"); + const [currency, setCurrency] = useState("USD"); + const [amount, setAmount] = useState(""); + + const { stepState, startOnramp, confirmPaid, reset } = usePeerOnramp( + address, + async ({ prevBalance, newBalance, breadAmount }) => { + setModal({ + type: "WALLET_FUNDING_STATUS", + status: "success", + breadAmount, + }); + await modalState.fundWalletModalState.onFunded?.(newBalance, prevBalance); + } + ); + + const goBack = () => { + reset(); + setModal({ type: "FUND_WALLET", ...modalState.fundWalletModalState }); + }; + + const selectPlatform = (p: PeerPlatform) => { + setPlatform(p); + if (!PEER_PLATFORM_CONFIG[p].currencies.includes(currency)) { + setCurrency(PEER_PLATFORM_CONFIG[p].currencies[0]); + } + }; + + const amountInvalid = !amount || Number(amount) <= 0; + + return ( + +
+ + {(stepState.step === "form" || + stepState.step === "awaiting_payment") && ( + + )} +
+ + {stepState.step === "form" && ( +
{ + e.preventDefault(); + if (amountInvalid) return; + startOnramp({ platform, currency, fiatAmount: amount }); + }} + > + {stepState.error && ( + {stepState.error} + )} + +
+ {PEER_PLATFORMS.map((p) => ( + + ))} +
+ +
+ {PEER_PLATFORM_CONFIG[platform].currencies.map((c) => ( + + ))} +
+ + setAmount(e.target.value)} + name="peerFiatAmount" + id="peerFiatAmount" + className="w-full" + allowDecimal + /> +
+ + Continue + +
+ + )} + + {(stepState.step === "quoting" || stepState.step === "signaling") && ( +
+ + + {stepState.step === "quoting" + ? "Finding the best rate..." + : "Reserving your order..."} + +
+ )} + + {stepState.step === "awaiting_payment" && ( +
+ + Send{" "} + + {stepState.quote.fiatAmountFormatted} {stepState.quote.currency} + {" "} + with {PEER_PLATFORM_CONFIG[stepState.quote.platform].label} to: + + + {stepState.quote.payeeHandle || "See your payment app"} + + + You will receive ~{stepState.quote.tokenAmountFormatted} xDAI, + automatically baked into BREAD. Send exactly the amount shown, then + confirm below. + +
+ + I've sent the payment + +
+
+ )} + + {stepState.step === "capturing" && ( +
+ + + Complete the verification in the Peer extension window. It signs in + to {PEER_PLATFORM_CONFIG[stepState.quote.platform].label} to confirm + your payment. + +
+ )} + + {(stepState.step === "fulfilling" || + stepState.step === "bridging" || + stepState.step === "minting" || + stepState.step === "done") && ( +
+ + + {stepState.step === "fulfilling" && "Verifying your payment..."} + {stepState.step === "bridging" && + "Payment verified! Bridging your funds to Gnosis - this usually takes a few minutes. Keep this window open."} + {stepState.step === "minting" && "Baking your BREAD..."} + {stepState.step === "done" && + `Done! ${stepState.breadAmount} BREAD baked.`} + +
+ )} +
+ ); +}; + +export default PeerOnrampModal; diff --git a/src/components/modal/presenter.tsx b/src/components/modal/presenter.tsx index 638f0654..d7108b26 100644 --- a/src/components/modal/presenter.tsx +++ b/src/components/modal/presenter.tsx @@ -19,6 +19,7 @@ import NewUserOnboarding from "./modals/new-user-onboarding"; import FundWallet from "./modals/fund-wallet/fund-wallet"; import LiFiSwapModal from "../lifi/swap-modal"; import InstallPeerModal from "../peer/install-modal"; +import PeerOnrampModal from "./modals/fund-wallet/peer-onramp-modal"; import FundWithConnectedWalletModalAmount from "./modals/fund-wallet/fund-with-connected-wallet-modal-amount"; import StartStackWarningModal from "./modals/start-stack-warning"; import AutomaticClaimsModal from "./modals/automatic-claims"; @@ -91,6 +92,9 @@ const ModalPresenter = () => { {modalState.type === "PEER_ONRAMP_INSTALL" && ( )} + {modalState.type === "PEER_ONRAMP" && ( + + )} {modalState.type === "AUTOMATIC_CLAIMS" && ( )} diff --git a/src/hooks/use-peer-onramp.ts b/src/hooks/use-peer-onramp.ts new file mode 100644 index 00000000..cc7cfd4d --- /dev/null +++ b/src/hooks/use-peer-onramp.ts @@ -0,0 +1,493 @@ +import { useEffect, useRef, useState } from "react"; +import { base } from "viem/chains"; +import { + Address, + createWalletClient, + encodeFunctionData, + formatEther, + Hex, + http, + zeroAddress, +} from "viem"; +import { usePublicClient } from "wagmi"; +import { useSendTransaction } from "@privy-io/react-auth"; +import { + createPeerExtensionSdk, + Zkp2pClient, + type BuyerTeePaymentProofInput, + type GetQuoteSingleResponse, + type PeerBuyerTeePaymentCapture, + type PeerMetadataRow, + type PreparedTransaction, +} from "@zkp2p/sdk"; +import { clientEnv } from "@/lib/env"; +import { breadAbi } from "@/lib/abis/bread-abi"; +import { useSponsoredTx } from "./use-sponsored-tx"; +import { useWaitForTxReceipt } from "./use-wait-for-tx-receipt"; + +export const PEER_PLATFORMS = ["wise", "revolut", "venmo"] as const; + +export type PeerPlatform = (typeof PEER_PLATFORMS)[number]; + +type PlatformConfig = { + label: string; + actionType: string; + /** Only Venmo, Cash App, Revolut and Zelle send the metadata index param */ + includeMetadataIndex: boolean; + currencies: string[]; +}; + +export const PEER_PLATFORM_CONFIG: Record = { + wise: { + label: "Wise", + actionType: "transfer_wise", + includeMetadataIndex: false, + currencies: ["USD", "EUR", "GBP"], + }, + revolut: { + label: "Revolut", + actionType: "transfer_revolut", + includeMetadataIndex: true, + currencies: ["USD", "EUR", "GBP"], + }, + venmo: { + label: "Venmo", + actionType: "transfer_venmo", + includeMetadataIndex: true, + currencies: ["USD"], + }, +}; + +const ATTESTATION_SERVICE_URL = "https://attestation-service.zkp2p.xyz"; +const MIN_EXTENSION_VERSION = [0, 6, 3] as const; +const BRIDGE_TIMEOUT_MS = 15 * 60 * 1000; + +export type PeerOnrampQuote = { + platform: PeerPlatform; + currency: string; + /** Fiat amount the user must send, e.g. "10.00" */ + fiatAmount: string; + fiatAmountFormatted: string; + tokenAmountFormatted: string; + /** Maker's payment handle on the platform (email/username) */ + payeeHandle: string; +}; + +export type PeerOnrampStep = + | { step: "form"; error?: string } + | { step: "quoting" } + | { step: "signaling"; quote: PeerOnrampQuote } + | { step: "awaiting_payment"; quote: PeerOnrampQuote } + | { step: "capturing"; quote: PeerOnrampQuote } + | { step: "fulfilling"; quote: PeerOnrampQuote } + | { step: "bridging"; quote: PeerOnrampQuote } + | { step: "minting"; quote: PeerOnrampQuote } + | { step: "done"; quote: PeerOnrampQuote; breadAmount: string }; + +export type PeerOnrampResult = { + prevBalance: bigint; + newBalance: bigint; + breadAmount: string; +}; + +type OnrampSession = { + client: Zkp2pClient; + platform: PeerPlatform; + currency: string; + fiatAmount: string; + intentHash: Hex; + quote: PeerOnrampQuote; +}; + +function isExtensionVersionSupported(version: string) { + const parts = version.split(".").map(Number); + const [major = 0, minor = 0, patch = 0] = parts; + const [reqMajor, reqMinor, reqPatch] = MIN_EXTENSION_VERSION; + + if (major !== reqMajor) return major > reqMajor; + if (minor !== reqMinor) return minor > reqMinor; + return patch >= reqPatch; +} + +function isBuyerTeeParams( + value: unknown +): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.values(value).every( + (entry) => + typeof entry === "string" || + typeof entry === "number" || + typeof entry === "boolean" + ) + ); +} + +function selectPaymentRow( + rows: PeerMetadataRow[], + expected: { amount: string; currency: string } +): PeerMetadataRow | null { + const expectedAmount = Math.abs(parseFloat(expected.amount)); + const expectedCurrency = expected.currency.toUpperCase(); + + const visibleRows = rows.filter( + (row) => !row.hidden && isBuyerTeeParams(row.params) + ); + + return ( + visibleRows.find((row) => { + const rowAmount = row.amount ? Math.abs(parseFloat(row.amount)) : NaN; + const rowCurrency = row.currency?.toUpperCase(); + + return ( + rowAmount === expectedAmount && + (!rowCurrency || rowCurrency === expectedCurrency) + ); + }) ?? null + ); +} + +function buildBuyerTeeProof( + row: PeerMetadataRow, + capture: PeerBuyerTeePaymentCapture | null | undefined, + config: PlatformConfig, + platform: PeerPlatform +): BuyerTeePaymentProofInput { + if (!capture?.encryptedSessionMaterial || !isBuyerTeeParams(row.params)) { + throw new Error("Selected payment row is missing Buyer TEE capture data"); + } + + if (config.includeMetadataIndex && !Number.isInteger(row.originalIndex)) { + throw new Error( + "Selected payment row is missing its provider metadata index" + ); + } + + return { + proofType: "buyerTee", + encryptedSessionMaterial: capture.encryptedSessionMaterial, + params: { + ...row.params, + ...(config.includeMetadataIndex ? { index: row.originalIndex } : {}), + }, + actionPlatform: platform, + actionType: config.actionType, + }; +} + +/** + * Drives the headless Peer (ZKP2P) onramp end to end: quote -> signal intent + * (sponsored, on Base) -> user pays fiat -> extension Buyer TEE capture -> + * fulfill intent (sponsored, on Base) -> wait for the bridged xDAI to land on + * Gnosis -> sponsored BREAD mint from the received amount. + */ +export function usePeerOnramp( + address: Address, + onCompleted?: (result: PeerOnrampResult) => void | Promise +) { + const [stepState, setStepState] = useState({ step: "form" }); + const basePublicClient = usePublicClient({ chainId: base.id }); + const gnosisPublicClient = usePublicClient({ + chainId: clientEnv.NEXT_PUBLIC_CHAIN_ID, + }); + const { sendTransaction } = useSendTransaction(); + const { sendSponsoredTransaction } = useSponsoredTx(); + const { waitForTxReceipt } = useWaitForTxReceipt(); + + const peerSdkRef = useRef | null>( + null + ); + const sessionRef = useRef(null); + const unsubscribeRef = useRef<(() => void) | null>(null); + const cancelledRef = useRef(false); + + useEffect(() => { + cancelledRef.current = false; + peerSdkRef.current = createPeerExtensionSdk({ window }); + + return () => { + cancelledRef.current = true; + unsubscribeRef.current?.(); + }; + }, []); + + const fail = (error: unknown, fallback: string) => { + console.error("[usePeerOnramp]:", error); + setStepState({ + step: "form", + error: error instanceof Error ? error.message : fallback, + }); + }; + + const sendPreparedTx = async (prepared: PreparedTransaction) => { + const { hash } = await sendTransaction( + { + to: prepared.to, + data: prepared.data, + value: prepared.value, + chainId: prepared.chainId, + }, + { sponsor: true, uiOptions: { showWalletUIs: false } } + ); + + if (!basePublicClient) throw new Error("Base RPC client unavailable"); + await basePublicClient.waitForTransactionReceipt({ hash: hash as Hex }); + + return hash as Hex; + }; + + const ensurePeerReady = async () => { + const sdk = peerSdkRef.current; + if (!sdk) throw new Error("Peer SDK is not ready"); + + const state = await sdk.getState(); + + if (state === "needs_install") { + throw new Error("The Peer browser extension is required"); + } + + if (state === "needs_connection") { + const approved = await sdk.requestConnection(); + if (!approved) throw new Error("Peer extension connection was declined"); + } + + const version = await sdk.getVersion(); + if (!isExtensionVersionSupported(version)) { + throw new Error( + `Peer extension ${MIN_EXTENSION_VERSION.join(".")} or newer is required (found ${version})` + ); + } + + return sdk; + }; + + const waitForBridgedXdai = async (prevBalance: bigint) => { + if (!gnosisPublicClient) throw new Error("Gnosis RPC client unavailable"); + + const timeoutAt = Date.now() + BRIDGE_TIMEOUT_MS; + + while (Date.now() < timeoutAt) { + if (cancelledRef.current) throw new Error("Onramp was cancelled"); + + const balance = await gnosisPublicClient.getBalance({ address }); + if (balance > prevBalance) return balance; + + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + + throw new Error( + "Your payment was verified, but the bridged funds have not arrived yet. They are still on the way - check your balance in a few minutes." + ); + }; + + const mintBread = async (value: bigint) => { + const { hash } = await sendSponsoredTransaction( + { + to: clientEnv.NEXT_PUBLIC_BREAD_TOKEN_ADDRESS, + data: encodeFunctionData({ + abi: breadAbi, + functionName: "mint", + args: [address], + }), + value, + }, + { uiOptions: { showWalletUIs: false } } + ); + + await waitForTxReceipt(hash); + }; + + const findIntentHash = async (client: Zkp2pClient, depositId: string) => { + // The signal tx only returns its own hash; recover the intent hash from + // the on-chain view once the receipt has landed. + for (let attempt = 0; attempt < 10; attempt++) { + const intents = await client.getAccountIntents(address); + const match = intents + .filter((view) => view.intent.depositId.toString() === depositId) + .sort((a, b) => Number(b.intent.timestamp - a.intent.timestamp))[0]; + + if (match) return match.intentHash as Hex; + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + throw new Error("Could not locate the signaled intent on-chain"); + }; + + const startOnramp = async ({ + platform, + currency, + fiatAmount, + }: { + platform: PeerPlatform; + currency: string; + fiatAmount: string; + }) => { + try { + setStepState({ step: "quoting" }); + + await ensurePeerReady(); + + const client = new Zkp2pClient({ + walletClient: createWalletClient({ + account: address, + chain: base, + transport: http(), + }), + chainId: base.id, + }); + + const quoteResponse = await client.getQuote({ + paymentPlatforms: [platform], + fiatCurrency: currency, + user: address, + recipient: address, + destinationChainId: clientEnv.NEXT_PUBLIC_CHAIN_ID, + destinationToken: zeroAddress, + amount: fiatAmount, + isExactFiat: true, + }); + + const bestQuote: GetQuoteSingleResponse | undefined = + quoteResponse.responseObject.quotes[0]; + + if (!bestQuote) { + throw new Error( + `No ${PEER_PLATFORM_CONFIG[platform].label} liquidity available for this amount right now` + ); + } + + const quote: PeerOnrampQuote = { + platform, + currency, + fiatAmount: bestQuote.fiatAmount, + fiatAmountFormatted: bestQuote.fiatAmountFormatted, + tokenAmountFormatted: bestQuote.tokenAmountFormatted, + payeeHandle: bestQuote.payeeData?.offchainId ?? "", + }; + + setStepState({ step: "signaling", quote }); + + const intent = bestQuote.intent; + + const prepared = await client.signalIntent.prepare({ + depositId: String(intent.depositId), + amount: intent.amount, + toAddress: intent.toAddress as Address, + processorName: intent.processorName, + payeeDetails: intent.payeeDetails, + fiatCurrencyCode: intent.fiatCurrencyCode, + conversionRate: bestQuote.conversionRate, + escrowAddress: intent.escrowAddress as Address, + }); + + await sendPreparedTx(prepared); + + const intentHash = await findIntentHash(client, String(intent.depositId)); + + sessionRef.current = { + client, + platform, + currency, + fiatAmount: quote.fiatAmount, + intentHash, + quote, + }; + + setStepState({ step: "awaiting_payment", quote }); + } catch (error) { + fail(error, "Could not start the Peer onramp"); + } + }; + + const confirmPaid = async () => { + const session = sessionRef.current; + if (!session) return; + + try { + const sdk = await ensurePeerReady(); + const config = PEER_PLATFORM_CONFIG[session.platform]; + + setStepState({ step: "capturing", quote: session.quote }); + + // Register before authenticate() per SDK requirements. + unsubscribeRef.current?.(); + unsubscribeRef.current = sdk.onMetadataMessage(async (message) => { + try { + if (message.errorMessage) throw new Error(message.errorMessage); + + const row = selectPaymentRow(message.metadata, { + amount: session.fiatAmount, + currency: session.currency, + }); + + if (!row) { + throw new Error( + "None of your recent payments matched the expected amount" + ); + } + + const proof = buildBuyerTeeProof( + row, + message.buyerTeeCapture, + config, + session.platform + ); + + setStepState({ step: "fulfilling", quote: session.quote }); + + // Snapshot before fulfilling: the bridged xDAI can only arrive + // after the fulfill tx releases funds on Base. + if (!gnosisPublicClient) + throw new Error("Gnosis RPC client unavailable"); + const prevBalance = await gnosisPublicClient.getBalance({ address }); + + const prepared = await session.client.fulfillIntent.prepare({ + intentHash: session.intentHash, + proof, + attestationServiceUrl: ATTESTATION_SERVICE_URL, + }); + + await sendPreparedTx(prepared); + + setStepState({ step: "bridging", quote: session.quote }); + const newBalance = await waitForBridgedXdai(prevBalance); + + setStepState({ step: "minting", quote: session.quote }); + await mintBread(newBalance - prevBalance); + + const breadAmount = formatEther(newBalance - prevBalance); + setStepState({ step: "done", quote: session.quote, breadAmount }); + + await onCompleted?.({ prevBalance, newBalance, breadAmount }); + } catch (error) { + fail(error, "Could not verify your payment"); + } finally { + unsubscribeRef.current?.(); + unsubscribeRef.current = null; + } + }); + + sdk.authenticate({ + actionType: config.actionType, + attestationActionType: config.actionType, + attestationServiceUrl: ATTESTATION_SERVICE_URL, + captureMode: "buyerTee", + platform: session.platform, + }); + } catch (error) { + fail(error, "Could not start the payment capture"); + } + }; + + const reset = () => { + unsubscribeRef.current?.(); + unsubscribeRef.current = null; + sessionRef.current = null; + setStepState({ step: "form" }); + }; + + return { stepState, startOnramp, confirmPaid, reset }; +} From cfe36ae1ac665dcb5f9b3325e2f068e99f8241f9 Mon Sep 17 00:00:00 2001 From: Olaleye Blessing Date: Sat, 4 Jul 2026 21:33:52 +0100 Subject: [PATCH 2/5] feat: gate the Peer funding option behind the fundWithPeer flag Wrap FundWithPeer in FeatureGate so deployments control its visibility via NEXT_PUBLIC_FEATURES, e.g. enabled everywhere but restricted to an allowlisted wallet while the new onramp flow is verified live: {"fundWithPeer":{"enabled":true,"addresses":["0xcCBaD3a42F8b406E2838981f947f449E2471aac7"]}} Local still renders the button for everyone. Co-Authored-By: Claude Fable 5 --- .env.local.example | 2 +- .../modal/modals/fund-wallet/fund-wallet.tsx | 13 ++++++++----- src/lib/features.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.env.local.example b/.env.local.example index a20000c7..a331a447 100644 --- a/.env.local.example +++ b/.env.local.example @@ -50,5 +50,5 @@ AUTOMATIC_FUNDING_PRIVATE_KEY= # Feature gating (JSON, per deployment). Feature key absent or enabled=false -> hidden; # addresses present -> only those wallets see it. Local always shows every feature. -# e.g. {"automaticClaim":{"enabled":true,"addresses":["0x6532665Cde5Cc99bbABf4Be4bbF178bD13CC849d"]}} +# e.g. {"automaticClaim":{"enabled":true},"fundWithPeer":{"enabled":true,"addresses":["0xcCBaD3a42F8b406E2838981f947f449E2471aac7"]}} NEXT_PUBLIC_FEATURES= diff --git a/src/components/modal/modals/fund-wallet/fund-wallet.tsx b/src/components/modal/modals/fund-wallet/fund-wallet.tsx index daf1ca8c..7b79dc07 100644 --- a/src/components/modal/modals/fund-wallet/fund-wallet.tsx +++ b/src/components/modal/modals/fund-wallet/fund-wallet.tsx @@ -8,6 +8,7 @@ import { useWatchFundedXdai } from "@/hooks/use-watch-funded-xdai"; import { Body, useConnectedUser } from "@breadcoop/ui"; import Alert from "@/components/alert"; import FundWithPeer from "./fund-with-peer"; +import { FeatureGate } from "@/components/feature-gate"; const FundWallet = ({ modalState }: { modalState: FundWalletModalState }) => { const { setModal } = useModal(); @@ -45,11 +46,13 @@ const FundWallet = ({ modalState }: { modalState: FundWalletModalState }) => {
- + + +
every user sees it * - local always renders every feature regardless of config */ -export const FEATURES = ["automaticClaim"] as const; +export const FEATURES = ["automaticClaim", "fundWithPeer"] as const; export type Feature = (typeof FEATURES)[number]; From f5e10e07c64f080608bc02b0d035d4f8f15f26d0 Mon Sep 17 00:00:00 2001 From: Olaleye Blessing Date: Sun, 5 Jul 2026 06:30:27 +0100 Subject: [PATCH 3/5] feat: bridge Peer onramp proceeds from Base USDC to Gnosis xDAI via LI.FI Peer's quote API only accepts Base (8453) as a destination chain - quoting straight to Gnosis fails with unsupported_destination_chain. Onramp to USDC on Base instead, then bridge silently with LI.FI (no widget): wait for the USDC delta from the fulfill tx, quote via the new lib/lifi helper, sponsored approve when allowance is short, sponsored bridge tx, then the existing xDAI arrival wait and BREAD mint. Verified live: LI.FI routes Base USDC -> native xDAI via Relay (~6s). Known limitation: a failure after fulfillment leaves USDC on Base and retrying will not bridge it (bridge amount is delta-based); needs a recovery path before rollout beyond the allowlisted wallet. Co-Authored-By: Claude Fable 5 --- .../modals/fund-wallet/peer-onramp-modal.tsx | 6 +- src/hooks/use-peer-onramp.ts | 96 ++++++++++++++++++- src/lib/lifi.ts | 49 ++++++++++ 3 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 src/lib/lifi.ts diff --git a/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx b/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx index b32fe744..a03137cf 100644 --- a/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx +++ b/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx @@ -162,9 +162,9 @@ const PeerOnrampModal = ({ {stepState.quote.payeeHandle || "See your payment app"} - You will receive ~{stepState.quote.tokenAmountFormatted} xDAI, - automatically baked into BREAD. Send exactly the amount shown, then - confirm below. + You will receive ~{stepState.quote.tokenAmountFormatted} USDC, + automatically converted to xDAI and baked into BREAD. Send exactly + the amount shown, then confirm below.
diff --git a/src/hooks/use-peer-onramp.ts b/src/hooks/use-peer-onramp.ts index cc7cfd4d..17572c0a 100644 --- a/src/hooks/use-peer-onramp.ts +++ b/src/hooks/use-peer-onramp.ts @@ -4,6 +4,7 @@ import { Address, createWalletClient, encodeFunctionData, + erc20Abi, formatEther, Hex, http, @@ -22,6 +23,7 @@ import { } from "@zkp2p/sdk"; import { clientEnv } from "@/lib/env"; import { breadAbi } from "@/lib/abis/bread-abi"; +import { getBridgeQuote } from "@/lib/lifi"; import { useSponsoredTx } from "./use-sponsored-tx"; import { useWaitForTxReceipt } from "./use-wait-for-tx-receipt"; @@ -61,6 +63,9 @@ export const PEER_PLATFORM_CONFIG: Record = { const ATTESTATION_SERVICE_URL = "https://attestation-service.zkp2p.xyz"; const MIN_EXTENSION_VERSION = [0, 6, 3] as const; const BRIDGE_TIMEOUT_MS = 15 * 60 * 1000; +// Peer's quote API only supports Base as a destination, so the onramp lands +// USDC on Base and we bridge it to Gnosis xDAI ourselves via LI.FI. +const BASE_USDC_ADDRESS: Address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; export type PeerOnrampQuote = { platform: PeerPlatform; @@ -263,6 +268,86 @@ export function usePeerOnramp( return sdk; }; + const readUsdcBalance = async () => { + if (!basePublicClient) throw new Error("Base RPC client unavailable"); + + return basePublicClient.readContract({ + address: BASE_USDC_ADDRESS, + abi: erc20Abi, + functionName: "balanceOf", + args: [address], + }); + }; + + // The fulfill tx releases USDC to the recipient on Base in the same block; + // the short retry loop only covers RPC lag. + const waitForUsdcReceived = async (prevBalance: bigint) => { + for (let attempt = 0; attempt < 15; attempt++) { + if (cancelledRef.current) throw new Error("Onramp was cancelled"); + + const balance = await readUsdcBalance(); + if (balance > prevBalance) return balance - prevBalance; + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + throw new Error( + "Your payment was verified, but the USDC has not shown up on Base yet - check back in a few minutes." + ); + }; + + const bridgeUsdcToXdai = async (amount: bigint) => { + if (!basePublicClient) throw new Error("Base RPC client unavailable"); + + const lifiQuote = await getBridgeQuote({ + fromChainId: base.id, + toChainId: clientEnv.NEXT_PUBLIC_CHAIN_ID, + fromToken: BASE_USDC_ADDRESS, + toToken: zeroAddress, + fromAmount: amount, + fromAddress: address, + toAddress: address, + }); + + const txRequest = lifiQuote.transactionRequest; + const approvalAddress = lifiQuote.estimate.approvalAddress as + | Address + | undefined; + + if (!txRequest?.to || !txRequest.data) { + throw new Error("LI.FI returned no executable bridge transaction"); + } + + if (approvalAddress) { + const allowance = await basePublicClient.readContract({ + address: BASE_USDC_ADDRESS, + abi: erc20Abi, + functionName: "allowance", + args: [address, approvalAddress], + }); + + if (allowance < amount) { + await sendPreparedTx({ + to: BASE_USDC_ADDRESS, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [approvalAddress, amount], + }), + value: BigInt(0), + chainId: base.id, + }); + } + } + + await sendPreparedTx({ + to: txRequest.to as Address, + data: txRequest.data as Hex, + value: txRequest.value ? BigInt(txRequest.value) : BigInt(0), + chainId: base.id, + }); + }; + const waitForBridgedXdai = async (prevBalance: bigint) => { if (!gnosisPublicClient) throw new Error("Gnosis RPC client unavailable"); @@ -344,8 +429,8 @@ export function usePeerOnramp( fiatCurrency: currency, user: address, recipient: address, - destinationChainId: clientEnv.NEXT_PUBLIC_CHAIN_ID, - destinationToken: zeroAddress, + destinationChainId: base.id, + destinationToken: BASE_USDC_ADDRESS, amount: fiatAmount, isExactFiat: true, }); @@ -438,10 +523,11 @@ export function usePeerOnramp( setStepState({ step: "fulfilling", quote: session.quote }); - // Snapshot before fulfilling: the bridged xDAI can only arrive - // after the fulfill tx releases funds on Base. + // Snapshot both sides before fulfilling: USDC lands on Base with + // the fulfill tx, xDAI lands on Gnosis after our LI.FI bridge. if (!gnosisPublicClient) throw new Error("Gnosis RPC client unavailable"); + const prevUsdcBalance = await readUsdcBalance(); const prevBalance = await gnosisPublicClient.getBalance({ address }); const prepared = await session.client.fulfillIntent.prepare({ @@ -453,6 +539,8 @@ export function usePeerOnramp( await sendPreparedTx(prepared); setStepState({ step: "bridging", quote: session.quote }); + const usdcReceived = await waitForUsdcReceived(prevUsdcBalance); + await bridgeUsdcToXdai(usdcReceived); const newBalance = await waitForBridgedXdai(prevBalance); setStepState({ step: "minting", quote: session.quote }); diff --git a/src/lib/lifi.ts b/src/lib/lifi.ts new file mode 100644 index 00000000..c97e0463 --- /dev/null +++ b/src/lib/lifi.ts @@ -0,0 +1,49 @@ +import { createConfig, getQuote, type LiFiStep } from "@lifi/sdk"; + +export const LIFI_INTEGRATOR = "Stacks"; + +let configured = false; + +function ensureLifiConfig() { + if (configured) return; + + createConfig({ integrator: LIFI_INTEGRATOR }); + configured = true; +} + +export type BridgeQuoteParams = { + fromChainId: number; + toChainId: number; + fromToken: string; + toToken: string; + fromAmount: bigint; + fromAddress: string; + toAddress?: string; +}; + +/** + * Fetches an executable LI.FI quote (single step, includes + * `transactionRequest` and `estimate.approvalAddress`) for bridging/swapping + * without the widget UI. The caller signs and submits the transaction itself. + */ +export async function getBridgeQuote({ + fromChainId, + toChainId, + fromToken, + toToken, + fromAmount, + fromAddress, + toAddress, +}: BridgeQuoteParams): Promise { + ensureLifiConfig(); + + return getQuote({ + fromChain: fromChainId, + toChain: toChainId, + fromToken, + toToken, + fromAmount: fromAmount.toString(), + fromAddress, + toAddress, + }); +} From 418a27d621713a1cb83ee42e04a21041de4b8a15 Mon Sep 17 00:00:00 2001 From: Olaleye Blessing Date: Sun, 5 Jul 2026 10:14:59 +0100 Subject: [PATCH 4/5] chore: bump @zkp2p/sdk to 0.7.2 Track the latest SDK line (the one the Peer docs document). No code changes needed: the headless integration surface (Zkp2pClient, getQuote, signalIntent/fulfillIntent prepare, extension onMetadataMessage/ authenticate) is unchanged since 0.5.x and nothing we call is deprecated. Adds @zkp2p/sdk to minimumReleaseAgeExclude since Peer ships breaking API changes rapidly; this opts the package out of the 10-day release-age gate. Co-Authored-By: Claude Fable 5 --- package.json | 2 +- pnpm-lock.yaml | 38 +++++++++++++++++++------------------- pnpm-workspace.yaml | 2 ++ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index afceedf7..2b053f23 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@tanstack/react-query": "^5.85.6", "@upstash/redis": "^1.36.2", "@wagmi/core": "2.x.x", - "@zkp2p/sdk": "^0.5.2", + "@zkp2p/sdk": "^0.7.2", "clsx": "^2.1.1", "embla-carousel-react": "^8.3.1", "next": "15.4.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0532967d..76117d93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: 2.x.x version: 2.22.1(@tanstack/query-core@5.91.2)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6)) "@zkp2p/sdk": - specifier: ^0.5.2 - version: 0.5.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6) + specifier: ^0.7.2 + version: 0.7.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -4846,25 +4846,25 @@ packages: integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==, } - "@zkp2p/contracts-v2@0.2.2": + "@zkp2p/contracts-v2@0.2.4": resolution: { - integrity: sha512-mI7v491SkHjFHqXnSTFkzNaFPSiVli1Czb1D943tNWAMLCERZaBGQTZe12HJP5kZQpDVndrI1GnnPrB0BU7lFQ==, + integrity: sha512-2MIPKWdKYCcgA/BjoasJh0VdTvP25UYyDi0TyTMx/8Z+6Qc/wzWnJTRMOzUP07P069XoQ2sTCcgS246Rb9AiHw==, } peerDependencies: ethers: ^5.0.0 || ^6.0.0 - "@zkp2p/indexer-schema@0.2.3": + "@zkp2p/indexer-schema@0.5.0": resolution: { - integrity: sha512-SUVEhdPh0sgLikUEwsf0MxKevuYiCep/WhWh8OkC0GKx+C3OBaH/ueHYvgWoI7b7rczzthDCvvIJ0JlAbvHAvQ==, + integrity: sha512-L2ShEBv7HjghRElZSYtGcBNdnRVchVW7c/wnN3S5VLZQA4V2IUmVv8BPZzYYiQ53y+7F/qPKtUQOW+p9ZbZEiQ==, } engines: { node: ">=16" } - "@zkp2p/sdk@0.5.2": + "@zkp2p/sdk@0.7.2": resolution: { - integrity: sha512-RmFzlxYW526oK1GFQ6fj4Aa56tccO0UgQlDnD1WoFH00FwIDxo44bFaCg7FDJSqrqsz5lqhfmwcl3mdCdvGTVw==, + integrity: sha512-ZtRMfnI4LOUHZ+pYGjTi6rxQsch1kDFeTLIeRd7ytARWW1GT3+7qeXmT7FSxvq8sJCyrNkY6xmnLirtsC6De5Q==, } engines: { node: ">=22" } peerDependencies: @@ -4874,10 +4874,10 @@ packages: react: optional: true - "@zkp2p/zkp2p-attestation@1.5.1": + "@zkp2p/zkp2p-attestation@1.6.3": resolution: { - integrity: sha512-s4JkhGQCPONomQxfKa9CNAgFrHiNnUO/sgdz4brxu2qrvbhF0hYuM8pGbsWDnXt24Yjqv2i75z4BGzNEj1Cb2Q==, + integrity: sha512-gGBNrbULr/livdR7wkyDto+zRVsAxIBUx6YBT+VEFW5stgZBwxv6sEuQ4AguBnmSe9WKFrWZWYC3fLgdFcPT6g==, } engines: { node: ">=20" } hasBin: true @@ -16886,7 +16886,7 @@ snapshots: "@walletconnect/window-getters": 1.0.1 tslib: 1.14.1 - "@zkp2p/contracts-v2@0.2.2(ethers@6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6)": + "@zkp2p/contracts-v2@0.2.4(ethers@6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6)": dependencies: abitype: 1.0.6(typescript@5.9.3)(zod@4.3.6) ethers: 6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) @@ -16894,13 +16894,13 @@ snapshots: - typescript - zod - "@zkp2p/indexer-schema@0.2.3": {} + "@zkp2p/indexer-schema@0.5.0": {} - "@zkp2p/sdk@0.5.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6)": + "@zkp2p/sdk@0.7.2(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6)": dependencies: - "@zkp2p/contracts-v2": 0.2.2(ethers@6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6) - "@zkp2p/indexer-schema": 0.2.3 - "@zkp2p/zkp2p-attestation": 1.5.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + "@zkp2p/contracts-v2": 0.2.4(ethers@6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(zod@4.3.6) + "@zkp2p/indexer-schema": 0.5.0 + "@zkp2p/zkp2p-attestation": 1.6.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) ethers: 6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) ox: 0.11.3(typescript@5.9.3)(zod@4.3.6) viem: 2.47.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6) @@ -16912,7 +16912,7 @@ snapshots: - utf-8-validate - zod - "@zkp2p/zkp2p-attestation@1.5.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)": + "@zkp2p/zkp2p-attestation@1.6.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)": dependencies: "@peculiar/x509": 1.14.0 ethers: 6.13.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) @@ -19290,7 +19290,7 @@ snapshots: ox@0.6.7(typescript@5.9.3)(zod@3.25.76): dependencies: - "@adraffy/ens-normalize": 1.10.1 + "@adraffy/ens-normalize": 1.11.0 "@noble/curves": 1.9.1 "@noble/hashes": 1.5.0 "@scure/bip32": 1.5.0 @@ -19304,7 +19304,7 @@ snapshots: ox@0.6.7(typescript@5.9.3)(zod@4.3.6): dependencies: - "@adraffy/ens-normalize": 1.10.1 + "@adraffy/ens-normalize": 1.11.0 "@noble/curves": 1.9.1 "@noble/hashes": 1.5.0 "@scure/bip32": 1.5.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 37958e98..378d0696 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,8 @@ blockExoticSubdeps: true minimumReleaseAge: 864000 # 10 days minimumReleaseAgeExclude: - "@breadcoop/ui" + # Peer ships breaking API changes rapidly; track latest despite the age gate + - "@zkp2p/sdk" strictDepBuilds: true trustPolicy: no-downgrade From bd168a1a446d9d656be09817415df976fbb21c2e Mon Sep 17 00:00:00 2001 From: Olaleye Blessing Date: Sun, 5 Jul 2026 18:43:20 +0100 Subject: [PATCH 5/5] fix: send Peer quote amounts in precise units and fetch payee details Two fixes found while debugging no_quotes_found in production: - The quote API takes exactFiatAmount in 6-decimal precise units ("200000000" = 200.00 USD); we were sending the human-readable amount, which read as fractions of a cent and matched no deposits. Keep the human amount locally for payment-row matching. - Quote responses only inline payeeData for authenticated callers, but the payee lookup endpoint is public - fetch the maker's handle via apiGetPayeeDetails so the payment instructions screen works without a curator API key. (Verified the gating-signature endpoint is also public, so no API key is needed anywhere in the flow.) Also drop doubled symbols in the modal: fiatAmountFormatted and tokenAmountFormatted already embed "USD"/"USDC". Co-Authored-By: Claude Fable 5 --- .../modals/fund-wallet/peer-onramp-modal.tsx | 9 ++--- src/hooks/use-peer-onramp.ts | 36 ++++++++++++++++--- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx b/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx index a03137cf..bb7864a8 100644 --- a/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx +++ b/src/components/modal/modals/fund-wallet/peer-onramp-modal.tsx @@ -149,11 +149,8 @@ const PeerOnrampModal = ({ {stepState.step === "awaiting_payment" && (
- Send{" "} - - {stepState.quote.fiatAmountFormatted} {stepState.quote.currency} - {" "} - with {PEER_PLATFORM_CONFIG[stepState.quote.platform].label} to: + Send {stepState.quote.fiatAmountFormatted} with{" "} + {PEER_PLATFORM_CONFIG[stepState.quote.platform].label} to: - You will receive ~{stepState.quote.tokenAmountFormatted} USDC, + You will receive ~{stepState.quote.tokenAmountFormatted}, automatically converted to xDAI and baked into BREAD. Send exactly the amount shown, then confirm below. diff --git a/src/hooks/use-peer-onramp.ts b/src/hooks/use-peer-onramp.ts index 17572c0a..2455bc0d 100644 --- a/src/hooks/use-peer-onramp.ts +++ b/src/hooks/use-peer-onramp.ts @@ -8,11 +8,13 @@ import { formatEther, Hex, http, + parseUnits, zeroAddress, } from "viem"; import { usePublicClient } from "wagmi"; import { useSendTransaction } from "@privy-io/react-auth"; import { + apiGetPayeeDetails, createPeerExtensionSdk, Zkp2pClient, type BuyerTeePaymentProofInput, @@ -61,6 +63,7 @@ export const PEER_PLATFORM_CONFIG: Record = { }; const ATTESTATION_SERVICE_URL = "https://attestation-service.zkp2p.xyz"; +const PEER_API_BASE_URL = "https://api.zkp2p.xyz"; const MIN_EXTENSION_VERSION = [0, 6, 3] as const; const BRIDGE_TIMEOUT_MS = 15 * 60 * 1000; // Peer's quote API only supports Base as a destination, so the onramp lands @@ -431,7 +434,9 @@ export function usePeerOnramp( recipient: address, destinationChainId: base.id, destinationToken: BASE_USDC_ADDRESS, - amount: fiatAmount, + // The quote API takes fiat amounts in 6-decimal precise units + // ("200000000" = 200.00), not human-readable strings. + amount: parseUnits(fiatAmount, 6).toString(), isExactFiat: true, }); @@ -444,19 +449,40 @@ export function usePeerOnramp( ); } + const intent = bestQuote.intent; + + // Quote responses only inline payeeData for authenticated callers, but + // the payee lookup itself is public - fetch the maker's handle so the + // payment instructions can show where to send the fiat. + let payeeHandle = bestQuote.payeeData?.offchainId ?? ""; + if (!payeeHandle) { + try { + const payee = await apiGetPayeeDetails( + { + hashedOnchainId: intent.payeeDetails, + processorName: intent.processorName, + }, + PEER_API_BASE_URL + ); + payeeHandle = payee.responseObject?.offchainId ?? ""; + } catch (error) { + console.warn("[usePeerOnramp]: payee details lookup failed", error); + } + } + const quote: PeerOnrampQuote = { platform, currency, - fiatAmount: bestQuote.fiatAmount, + // Keep the human-readable amount (the API echoes precise units); + // it is matched against the captured payment rows later. + fiatAmount, fiatAmountFormatted: bestQuote.fiatAmountFormatted, tokenAmountFormatted: bestQuote.tokenAmountFormatted, - payeeHandle: bestQuote.payeeData?.offchainId ?? "", + payeeHandle, }; setStepState({ step: "signaling", quote }); - const intent = bestQuote.intent; - const prepared = await client.signalIntent.prepare({ depositId: String(intent.depositId), amount: intent.amount,