From 65a829a65bff37385ea712ecb33faa9d40bebaf8 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 27 Jul 2026 19:09:37 +0200 Subject: [PATCH 01/16] docs(repo): plan optional Privy embedded wallets --- .../privy-embedded-wallet-integration.md | 364 ++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 docs/plans/privy-embedded-wallet-integration.md diff --git a/docs/plans/privy-embedded-wallet-integration.md b/docs/plans/privy-embedded-wallet-integration.md new file mode 100644 index 000000000..1a8b91e61 --- /dev/null +++ b/docs/plans/privy-embedded-wallet-integration.md @@ -0,0 +1,364 @@ +# Optional Privy Embedded Wallets + +## Status + +- Owner: Vortex +- Base branch: `staging` +- Implementation branch: `codex/privy-embedded-wallets` +- Scope: dashboard, widget, API, tests, and operational documentation +- Privy credentials: not required for local unit/integration tests; required for a live sandbox smoke test +- Implementation: phases 0–5 complete behind disabled-by-default flags +- Remaining validation: credentialed staging wallet creation, restoration, export, and live signing smoke test + +## Objective + +Allow a Vortex user who does not have or does not want to use a browser-extension wallet to create and use an +embedded EVM wallet supplied by Privy. This must remain optional: users who bring an existing wallet continue to +use the current Reown/Wagmi flow without being enrolled in or authenticated with Privy. + +The implementation must preserve the existing Vortex identity and ramp trust boundaries: + +1. Supabase email OTP remains the canonical Vortex account and API session. +2. `profiles.id` remains the Supabase user UUID. +3. Normal Vortex API requests continue to use the Supabase bearer token. +4. Privy is mounted and receives the Supabase JWT only after explicit embedded-wallet opt-in. +5. Privy wallet creation is manual (`createOnLogin: "off"`). +6. Existing-wallet users remain on Reown/Wagmi. +7. Vortex's ephemeral ramp keys remain client-side and separate from the user wallet. +8. The server continues to validate every user transaction hash, receipt, signer, destination, calldata, and value. +9. Vortex does not receive or store the embedded wallet's private key. +10. Wallet mode cannot change while a ramp is nonterminal. + +## Non-goals + +- Replacing Supabase authentication with Privy authentication. +- Replacing Reown for existing wallets. +- Creating a Privy wallet for every Vortex user. +- Server-controlled, delegated, or custodial user-wallet signing. +- Adding Privy support for Substrate/AssetHub transactions. +- Weakening the current one-active-ramp-per-profile invariant. +- Making arbitrary, unknown iframe parent origins trusted Privy origins. + +## User experience + +### Progressive wallet choice + +Authentication and wallet selection remain separate: + +1. The user authenticates to Vortex with the existing email OTP. +2. The user may browse, complete KYC/KYB, and use flows that do not need a wallet. +3. When a destination or signer is required, Vortex offers: + - **Create a Vortex wallet** — explicitly opts into Privy and creates/restores an embedded EVM wallet. + - **Connect my existing wallet** — uses the current Reown modal. + - **Not now** — available where a wallet is not yet required. + +Onramps may use the embedded wallet, a connected external wallet, or a manually entered address when the corridor +allows it. Offramps require an actual signing wallet. AssetHub routes continue to require the current Polkadot wallet +flow. + +### Wallet management + +The dashboard and widget show the selected wallet mode and address. Embedded-wallet users can: + +- copy the address; +- display a receive QR code; +- view relevant token balances; +- export the wallet through Privy's secure export flow; +- switch to an external wallet when no ramp is active. + +Switching modes never deletes a wallet or transfers funds. + +## Architecture + +### Canonical identity + +Supabase remains the identity provider. Privy custom authentication consumes the current Supabase access token and +uses the JWT `sub` claim (the Supabase UUID) as its stable identity. Development, staging, and production each use a +separate Privy application. Within an environment, dashboard and widget share one Privy application so the same +Supabase user restores the same embedded wallet. + +The Supabase project must expose an asymmetric signing key through: + +```text +https://.supabase.co/auth/v1/.well-known/jwks.json +``` + +This is an operational prerequisite and is verified during the live Phase 0 smoke test. + +### Wallet modes + +The frontend wallet domain has three modes: + +```ts +type WalletMode = "external" | "privy_embedded" | null; +``` + +A wallet-neutral EVM adapter exposes the minimum operations needed by Vortex: + +```ts +interface EvmWalletAdapter { + mode: "external" | "privy_embedded"; + ready: boolean; + address?: `0x${string}`; + chainId?: number; + signTypedData(input: VortexTypedData): Promise<`0x${string}`>; + sendTransaction(input: VortexTransactionRequest): Promise<`0x${string}`>; + waitForReceipt(hash: `0x${string}`, chainId: number): Promise<`0x${string}`>; +} +``` + +The external implementation wraps the current Wagmi actions. The embedded implementation uses Privy's native React +wallet hooks and always targets the selected embedded-wallet address explicitly. + +Business components and ramp actors consume the adapter. They do not select a wallet by array position and do not +import the global Reown Wagmi configuration for embedded-wallet operations. + +### Lazy Privy boundary + +Privy's package and provider are dynamically loaded only after the profile selects `privy_embedded`. The provider: + +- uses `VITE_PRIVY_APP_ID` and `VITE_PRIVY_CLIENT_ID`; +- receives the latest Supabase access token through custom authentication; +- sets `embeddedWallets.ethereum.createOnLogin` to `"off"`; +- creates a wallet only in response to the explicit user action; +- selects a wallet with `walletClientType === "privy"` and the registered address; +- tears down when the Vortex session ends or wallet mode changes. + +Neither a missing Privy configuration nor a Privy outage may break the external-wallet path. + +### API persistence + +`profiles.wallet_mode` records the cross-application preference: + +- `NULL`: ask when needed; +- `external`: prefer Reown; +- `privy_embedded`: restore the Privy boundary. + +`profile_wallets` records verified embedded-wallet metadata: + +| Column | Purpose | +| --- | --- | +| `id` | UUID primary key | +| `profile_id` | Owning Supabase/Vortex profile | +| `provider` | `privy` in v1 | +| `provider_wallet_id` | Privy's wallet identifier | +| `address` | Checksummed or normalized EVM address | +| `chain_type` | `ethereum` in v1 | +| `status` | `active` or `archived` | +| timestamps | Creation, update, and last-used audit fields | + +The API surface is: + +- `GET /v1/wallets` +- `POST /v1/wallets/privy` +- `PATCH /v1/wallets/mode` + +All routes require the existing Supabase bearer authentication. Privy metadata registration is verified server-side; +an address supplied by the browser is never trusted on its own. The Privy app secret is server-only and is not used +to sign user transactions. + +The wallet registry is UX metadata, not authorization to move money. Existing signatures and transaction receipt +validation remain authoritative. + +## Implementation phases + +### Phase 0 — configuration and compatibility + +- Add typed, disabled-by-default Privy configuration to dashboard, widget, and API. +- Add feature flags for provisioning, dashboard onramp, dashboard offramp, widget, and gas sponsorship. +- Add environment examples without credentials. +- Add an observable Supabase session bridge so token refresh and logout reach the optional Privy boundary. +- Add a no-credentials fake embedded-wallet adapter for local tests. +- Document the live smoke-test matrix: + - Supabase JWKS/custom-auth; + - dashboard/widget wallet continuity; + - known and unknown iframe parents; + - all EVM user-signing transaction shapes; + - gas policy behavior. + +### Phase 1 — wallet domain and persistence + +- Add the framework-neutral wallet types. +- Add dashboard and widget wallet providers. +- Preserve the current Reown adapter. +- Add the lazy Privy embedded adapter. +- Add the profile wallet-mode migration, wallet metadata migration, model, service, controller, and routes. +- Prevent mode changes while a ramp is nonterminal. +- Verify Privy wallet ownership before persistence when server credentials are configured. +- Fail closed for live registration if ownership cannot be verified. + +### Phase 2 — dashboard embedded onramp + +- Replace the header's connect-only button with a mode-aware wallet control. +- Add the explicit wallet chooser. +- Add embedded-wallet provisioning and recovery states. +- Offer embedded, external, and manual onramp destinations. +- Add wallet settings, copy/receive UI, and export action. +- Keep all current external-wallet behavior unchanged. + +### Phase 3 — dashboard embedded offramp + +- Refactor signing services to accept an injected EVM adapter. +- Refactor the app-lifetime dashboard transfer actor to inject wallet dependencies without persisting functions. +- Bind the selected address to the server-issued signer before every signature or broadcast. +- Implement Privy typed-data signing and EVM transaction sending. +- Add an explicit gas policy (`user_pays` or `sponsored`) behind a kill switch. +- Preserve receipt/hash submission and the current API validation. + +### Phase 4 — widget integration + +- Extend the ramp machine with a wallet-requirement decision before registration/signing. +- Do not gate authentication, invitations, or KYC on wallet creation. +- Use the wallet-neutral EVM adapter in `useVortexAccount` and user signing. +- Keep Polkadot/AssetHub paths on the existing wallet provider. +- Enable Privy only for top-level or explicitly allowlisted parent origins. +- Fail safely with a first-party handoff requirement for unsupported iframe parents. + +### Phase 5 — hardening and rollout + +- Add narrow CSP/origin documentation. +- Add feature-flagged rollout and kill switches. +- Add privacy-safe telemetry for mode choice, provisioning, signing, and gas failures. +- Add wallet export/recovery language. +- Update architecture and security specifications. +- Add internal, staging, partner, and gradual production rollout checklists. + +## Gas and signing policy + +Onramps do not require the destination wallet to sign. They ship before embedded-wallet offramps. + +For offramps, the embedded adapter signs the same server-issued EIP-712 and raw EVM transaction shapes as an external +wallet. The implementation must cover: + +- EIP-712 permit signatures; +- `squidRouterApprove`; +- `squidRouterSwap`; +- `squidRouterNoPermitTransfer`; +- `squidRouterNoPermitApprove`; +- `squidRouterNoPermitSwap`. + +The implementation initially defaults gas sponsorship off. Enabling user-pays or sponsored gas requires the live +Privy smoke test to confirm: + +- the transaction receipt `from` remains the Vortex-issued signer; +- EIP-712 signatures pass Vortex recovery and full-field validation; +- EIP-7702/ERC-1271 behavior is compatible with the selected token/permit route; +- chain and token are supported; +- rate limits, spend limits, and a global kill switch are active. + +## Iframe policy + +Privy requires the widget origin and every iframe parent origin to be allowlisted. V1 enables the embedded path only +when the widget is: + +- top-level on a configured Vortex origin; or +- framed by an explicitly configured allowed parent origin. + +Unknown parents see the external/manual options and a message explaining that the embedded wallet must be opened on +a Vortex-owned page. A scalable first-party popup/handoff may be added without sending wallet keys or auth tokens +through `postMessage`; messages must be origin-checked and nonce-bound. + +## Tests + +### Unit tests + +- wallet-mode parsing and guards; +- Privy configuration defaults; +- no Privy initialization for `none` or `external`; +- manual/idempotent provisioning; +- deterministic embedded-wallet selection; +- signer mismatch rejection; +- typed-data signature formatting; +- transaction adapter routing; +- iframe parent-origin policy; +- auth token refresh propagation; +- logout teardown; +- dashboard and widget state-machine wallet branches. + +### API integration tests + +- wallet routes require Supabase authentication; +- a profile can read only its wallets; +- invalid modes are rejected; +- mode switching is rejected during a nonterminal ramp; +- duplicate wallet registration is idempotent; +- a wallet owned by another profile is rejected; +- live-mode registration fails closed without ownership verification; +- wallet metadata never grants ramp ownership. + +### Contract/scenario tests + +- embedded and external adapters submit identical ramp request shapes; +- each EVM user-wallet phase produces the expected hashes/signatures; +- server-issued signer/address binding remains enforced; +- existing onramp/offramp corridor scenarios continue to pass; +- the one-active-ramp invariant remains unchanged. + +### Playwright tests + +- existing-wallet dashboard flow is unchanged; +- no-wallet user chooses and provisions an embedded wallet; +- embedded address is selected as an onramp destination; +- embedded offramp handles signing success, rejection, and insufficient balance; +- logout/login restores the wallet; +- wallet switching is blocked during an active ramp; +- widget defers wallet choice until needed; +- AssetHub does not offer Privy; +- allowed iframe parent succeeds; +- unsupported iframe parent fails safely. + +Provider ownership calls are mocked in deterministic API tests, and both wallet kinds use fake adapters in signing +contract tests. Playwright has default-disabled and Privy-enabled choice configurations. End-to-end Privy SDK wallet +creation and live signing remain an opt-in staging smoke test and never use production credentials. + +## Operational configuration + +Public client configuration: + +```text +VITE_PRIVY_ENABLED=false +VITE_PRIVY_APP_ID= +VITE_PRIVY_CLIENT_ID= +VITE_PRIVY_PROVISIONING_ENABLED=false +VITE_PRIVY_ONRAMP_ENABLED=false +VITE_PRIVY_OFFRAMP_ENABLED=false +VITE_PRIVY_GAS_POLICY=user_pays +VITE_PRIVY_WIDGET_PARENT_ORIGINS= +``` + +Server-only configuration: + +```text +PRIVY_APP_ID= +PRIVY_APP_SECRET= +PRIVY_WALLET_REGISTRATION_ENABLED=false +``` + +Production startup must reject an enabled server-side registration configuration with missing credentials. Client +applications degrade to the existing wallet flow if public Privy configuration is incomplete. + +## Acceptance criteria + +- A user can retain the current Reown wallet flow without a Privy identity or wallet. +- A user can authenticate through Vortex OTP and explicitly create an embedded EVM wallet. +- The same Supabase user restores the same wallet in dashboard and widget. +- An embedded wallet can receive an onramp without an extension. +- An embedded wallet can sign all supported EVM offramp transaction shapes. +- Users can export their client-created embedded wallet. +- Privy is hidden on Substrate/AssetHub routes. +- Unknown iframe parents do not initialize Privy. +- Supabase remains the only Vortex API principal. +- All existing ramp ownership, ephemeral-key, presign, receipt, and one-active-ramp invariants continue to pass. + +## Credentialed smoke-test checklist + +The following steps remain pending until non-production Privy credentials and dashboard access are available: + +- configure Supabase JWKS custom authentication; +- configure development/staging allowed origins and app client; +- verify lazy custom-auth enrollment and Privy MAU behavior; +- verify dashboard/widget wallet continuity; +- exercise wallet export; +- exercise each EVM signature/transaction path in the Vortex sandbox; +- validate the selected gas mode; +- validate one known iframe parent and one rejected parent. From d6ea5ec0b4f683ac16d2624c827be234c295078d Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 27 Jul 2026 19:09:51 +0200 Subject: [PATCH 02/16] chore(repo): add Privy client dependencies and test scripts --- apps/dashboard/package.json | 2 + apps/frontend/package.json | 2 + bun.lock | 584 ++++++++++++++++++++++++++++-------- 3 files changed, 470 insertions(+), 118 deletions(-) diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index bddfdfeed..ae25410b7 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@hookform/resolvers": "^4.1.3", + "@privy-io/react-auth": "^3.35.2", "@reown/appkit": "^1.8.8", "@reown/appkit-adapter-wagmi": "^1.8.8", "@tanstack/react-query": "^5.101.2", @@ -49,6 +50,7 @@ "preview": "vite preview --port 5174", "test": "bun test src", "test:e2e": "playwright test", + "test:e2e:privy": "playwright test --config playwright.privy.config.ts", "typecheck": "tsc --noEmit", "verify": "biome check --no-errors-on-unmatched" }, diff --git a/apps/frontend/package.json b/apps/frontend/package.json index dc5679606..bf1b2f432 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -18,6 +18,7 @@ "@polkadot/types": "catalog:", "@polkadot/util": "catalog:", "@polkadot/util-crypto": "catalog:", + "@privy-io/react-auth": "^3.35.2", "@reown/appkit": "^1.8.8", "@reown/appkit-adapter-wagmi": "^1.8.8", "@safe-global/api-kit": "^2.5.9", @@ -137,6 +138,7 @@ "test": "vitest", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", + "test:e2e:privy": "playwright test --config playwright.privy.config.ts", "typecheck": "tsc --noEmit", "verify": "bun test" }, diff --git a/bun.lock b/bun.lock index df1d623b1..10b9dd310 100644 --- a/bun.lock +++ b/bun.lock @@ -104,6 +104,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^4.1.3", + "@privy-io/react-auth": "^3.35.2", "@reown/appkit": "^1.8.8", "@reown/appkit-adapter-wagmi": "^1.8.8", "@tanstack/react-query": "^5.101.2", @@ -167,6 +168,7 @@ "@polkadot/types": "catalog:", "@polkadot/util": "catalog:", "@polkadot/util-crypto": "catalog:", + "@privy-io/react-auth": "^3.35.2", "@reown/appkit": "^1.8.8", "@reown/appkit-adapter-wagmi": "^1.8.8", "@safe-global/api-kit": "^2.5.9", @@ -707,7 +709,11 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@base-org/account": ["@base-org/account@2.4.0", "", { "dependencies": { "@coinbase/cdp-sdk": "^1.0.0", "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.31.7", "zustand": "5.0.3" } }, "sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug=="], + "@base-org/account": ["@base-org/account@1.1.1", "", { "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.31.7", "zustand": "5.0.3" } }, "sha512-IfVJPrDPhHfqXRDb89472hXkpvJuQQR7FDI9isLPHEqSYt/45whIoBxSPgZ0ssTt379VhQo4+87PWI1DoLSfAQ=="], + + "@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="], + + "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], @@ -733,7 +739,7 @@ "@coinbase/cdp-sdk": ["@coinbase/cdp-sdk@1.51.2", "", { "dependencies": { "@solana-program/system": "^0.10.0", "@solana-program/token": "^0.9.0", "@solana/kit": "^5.5.1", "abitype": "1.0.6", "axios": "1.16.0", "axios-retry": "^4.5.0", "bs58": "^6.0.0", "jose": "^6.2.0", "md5": "^2.3.0", "uncrypto": "^0.1.3", "viem": "^2.47.0", "zod": "^3.25.76" } }, "sha512-o4IEwXbyAjfhPQWoFBuqnV1JQGLk4NlUVMzH/ur4voPSjYZvlYFVuOoE/eEcsoPFN28xaWTBvqebwncQL8h8fQ=="], - "@coinbase/wallet-sdk": ["@coinbase/wallet-sdk@4.3.6", "", { "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.27.2", "zustand": "5.0.3" } }, "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA=="], + "@coinbase/wallet-sdk": ["@coinbase/wallet-sdk@4.3.2", "", { "dependencies": { "@noble/hashes": "^1.4.0", "clsx": "^1.2.1", "eventemitter3": "^5.0.1", "preact": "^10.24.2" } }, "sha512-hOLA2YONq8Z9n8f6oVP6N//FEEHOen7nq+adG/cReol6juFTHUelVN5GnA5zTIxiLFMDcrhDwwgCA6Tdb5jubw=="], "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], @@ -903,6 +909,8 @@ "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + "@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], @@ -1007,6 +1015,12 @@ "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + "@hcaptcha/loader": ["@hcaptcha/loader@2.4.2", "", {}, "sha512-+Pf0Vin6iJlJj5u/mq6XMgbJT1mi/Sg5LWHxxqO4YHt2YvdZgecp5V2sbC6vRaNPJUqQGk2HOu1K/i12mqs5vw=="], + + "@hcaptcha/react-hcaptcha": ["@hcaptcha/react-hcaptcha@1.17.4", "", { "dependencies": { "@babel/runtime": "^7.17.9", "@hcaptcha/loader": "^2.3.0" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-rIvgesG1N7SS9sAYYHFoWm+nXqRrxq7RcA9z2pKkDWV+S1GdfmrTNYA1aPyVWVe3eowphTCwyDJvl97Swwy0mw=="], + + "@headlessui/react": ["@headlessui/react@2.2.10", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA=="], + "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], "@hookform/resolvers": ["@hookform/resolvers@4.1.3", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ=="], @@ -1029,6 +1043,12 @@ "@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="], + "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], + + "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], + + "@internationalized/string": ["@internationalized/string@3.2.9", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -1055,6 +1075,8 @@ "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@1.0.11", "", { "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", "node-fetch": "^2.6.7", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.11" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ=="], + "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.5.4", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-2+ulBzQPYcC5jZ4Pghlcc6a6+CQ6L0cgTxtbavZbcHcG5/wSQsTAM+vDudF94L9AUPG4uSNQF1kotmvx0Ns/QA=="], + "@metamask/eth-json-rpc-provider": ["@metamask/eth-json-rpc-provider@1.0.1", "", { "dependencies": { "@metamask/json-rpc-engine": "^7.0.0", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^5.0.1" } }, "sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA=="], "@metamask/json-rpc-engine": ["@metamask/json-rpc-engine@8.0.2", "", { "dependencies": { "@metamask/rpc-errors": "^6.2.1", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^8.3.0" } }, "sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA=="], @@ -1375,6 +1397,28 @@ "@polkadot/x-ws": ["@polkadot/x-ws@14.0.3", "", { "dependencies": { "@polkadot/x-global": "14.0.3", "tslib": "^2.8.0", "ws": "^8.18.0" } }, "sha512-tOPdkMye3iuXnuFtdNg5+iSu7Cz9LRL8z5psMuZpUpThMYChGsS2pDFtNvXOKU8ohhO+frY9VdJ9VBg1WL9Iug=="], + "@privy-io/api-base": ["@privy-io/api-base@1.9.5", "", { "dependencies": { "zod": "^3.25.76" } }, "sha512-q3bfy/BFF8kktodfsVzpR2UprSN1RZkGwenrMtJwFxgwSKEUdG3BKjNUulH66F+FSa7TajxmKzZeXgVhxA9sXg=="], + + "@privy-io/api-types": ["@privy-io/api-types@0.17.0", "", {}, "sha512-d2N+99mMTz4q16cPG4QKxSuE6yF2196rddp2KMSdiUqP4LuWfXcw2ltirO7wm/3w/jWCBDDbzrTpw45+58WTOQ=="], + + "@privy-io/are-addresses-equal": ["@privy-io/are-addresses-equal@0.0.10", "", { "dependencies": { "viem": "2.55.5" } }, "sha512-FSdKZFpJcSVGcY9bf911gtIFPnQSivf3T3aiCxoaWGeuPXFmwYe60kBoc5pp0TfvGRhB8DrRIkB5DNH6Irnukg=="], + + "@privy-io/chains": ["@privy-io/chains@0.5.1", "", {}, "sha512-QW/txwiuldVd/FkrsGEIJzgOV6dCp0ZIpS+RXh0c4CX4fZTcfRjUsC2t8k6t07DODafZmY3Yx9/8jqpxUkqh6Q=="], + + "@privy-io/encoding": ["@privy-io/encoding@0.2.2", "", { "dependencies": { "@scure/base": "^1.2.6" } }, "sha512-bQHM5AB+F3/MIEaJ0WcFbCqAyuls7nUbQ34AlyctibPK1tmYL0tc9D2voeHo9q2S31nlgIKLEDJ65VZc+pcv6Q=="], + + "@privy-io/ethereum": ["@privy-io/ethereum@0.2.1", "", { "peerDependencies": { "viem": "2.55.5" } }, "sha512-G5TKQ1qJ/n/9UTn7e3zsv0V4deTh+z6/8ArQ2WoXFCv6SgXBAjXvHIo6b7baUbPWENVP7O39Tnjz5sxeJIPm/A=="], + + "@privy-io/js-sdk-core": ["@privy-io/js-sdk-core@0.68.4", "", { "dependencies": { "@privy-io/api-base": "1.9.5", "@privy-io/api-types": "0.17.0", "@privy-io/chains": "0.5.1", "@privy-io/encoding": "0.2.2", "@privy-io/ethereum": "0.2.1", "@privy-io/routes": "0.2.8", "canonicalize": "^2.0.0", "eventemitter3": "^5.0.1", "fetch-retry": "^6.0.0", "jose": "^4.15.5", "js-cookie": "^3.0.5", "libphonenumber-js": "^1.12.10", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "permissionless": "^0.2.47", "viem": "2.55.5" }, "optionalPeers": ["permissionless", "viem"] }, "sha512-/0FcFxOexXuVyUUEoKHgzaQWDfw9aXl25YkLh7xSXFrtqAwHecxMWGvqXBQu+lRK1hyyfnfISM4GyteFgxpTDQ=="], + + "@privy-io/popup": ["@privy-io/popup@0.0.5", "", {}, "sha512-lIwErJA86rHmLkSyapv86Z6hVzoLl544iZsVgk4tcVCHxbLBlLI4UmgLDmviNTZdp+uY0VEZe8fjUQIPpa6tZg=="], + + "@privy-io/react-auth": ["@privy-io/react-auth@3.35.2", "", { "dependencies": { "@base-org/account": "^1.1.0", "@base-ui/react": "^1.6.0", "@coinbase/wallet-sdk": "4.3.2", "@floating-ui/react": "^0.26.22", "@hcaptcha/react-hcaptcha": "^1.14.0", "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@marsidev/react-turnstile": "^1.3.1", "@privy-io/api-base": "1.9.5", "@privy-io/api-types": "0.17.0", "@privy-io/are-addresses-equal": "0.0.10", "@privy-io/chains": "0.5.1", "@privy-io/encoding": "0.2.2", "@privy-io/ethereum": "0.2.1", "@privy-io/js-sdk-core": "0.68.4", "@privy-io/popup": "0.0.5", "@privy-io/routes": "0.2.8", "@privy-io/urls": "0.0.5", "@scure/base": "^1.2.5", "@simplewebauthn/browser": "^13.2.2", "@tanstack/react-virtual": "^3.13.10", "@wallet-standard/app": "^1.0.1", "@walletconnect/ethereum-provider": "2.22.4", "@walletconnect/universal-provider": "2.22.4", "eventemitter3": "^5.0.1", "fast-password-entropy": "^1.1.1", "jose": "^4.15.5", "js-cookie": "^3.0.5", "lucide-react": "^0.554.0", "mipd": "^0.0.7", "ofetch": "^1.3.4", "pino-pretty": "^10.0.0", "qrcode": "^1.5.1", "react-device-detect": "^2.2.2", "secure-password-utilities": "^0.2.1", "styled-components": "^6.1.13", "stylis": "^4.3.4", "tinycolor2": "^1.6.0", "viem": "2.55.5", "x402": "^0.7.1", "zustand": "^5.0.4" }, "peerDependencies": { "@abstract-foundation/agw-client": "^1.0.0", "@farcaster/mini-app-solana": "^1.0.0", "@solana-program/memo": ">=0.8.0", "@solana-program/system": ">=0.8.0", "@solana-program/token": ">=0.6.0", "@solana/kit": ">=3.0.3", "@stripe/crypto": ">=1.1.1", "permissionless": "^0.2.47", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@abstract-foundation/agw-client", "@farcaster/mini-app-solana", "@solana-program/memo", "@solana-program/system", "@solana-program/token", "@solana/kit", "@stripe/crypto", "permissionless"] }, "sha512-9OzK/zVwxYlIsmLUp0y3WjV8KsWzEs7MvHjXiEZRwLL+bX54iFVG1cFoyHO5Gi6SpzLgo8tZmamn5NucNvm4yQ=="], + + "@privy-io/routes": ["@privy-io/routes@0.2.8", "", { "dependencies": { "@privy-io/api-types": "0.17.0" } }, "sha512-hGLjMC6AuZ2E7xUqyS+U15Y49fi//hXhjWjkMWWqMDsgLN48u8KTNv6C7YSt6CVC8CIHvwq3luu0P9nNuq0wMw=="], + + "@privy-io/urls": ["@privy-io/urls@0.0.5", "", {}, "sha512-cy4SQY60I9aGm+Er/gL4P+Rrsw2B7RoD6GKw5ZlwEN3jCm3tfclWeZI+xlJSKIwIu71YV9fWpO0bjLTs0tMLgQ=="], + "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], @@ -1495,6 +1539,12 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + "@react-aria/focus": ["@react-aria/focus@3.22.1", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-aria": "^3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-CPxtkyrBi/HYY5P3lE/57sQ6qfa0lN8E55TOm89H0kNGv0lKt+/0zP7lWERzBjRr5IxBVrQX4gFEowBN52LPaA=="], + + "@react-aria/interactions": ["@react-aria/interactions@3.28.1", "", { "dependencies": { "@react-types/shared": "^3.34.0", "@swc/helpers": "^0.5.0", "react-aria": "^3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Bqb+HrD5I5MHS2SKBhISYqo2SW8Y2dfzgF/Y1lIJq7xqLxheo9vzxPGEHhz+XzkgGfoqEJx8A6a3C7uiqS3HWA=="], + + "@react-types/shared": ["@react-types/shared@3.36.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ=="], + "@reown/appkit": ["@reown/appkit@1.8.22", "", { "dependencies": { "@reown/appkit-common": "1.8.22", "@reown/appkit-controllers": "1.8.22", "@reown/appkit-pay": "1.8.22", "@reown/appkit-polyfills": "1.8.22", "@reown/appkit-scaffold-ui": "1.8.22", "@reown/appkit-ui": "1.8.22", "@reown/appkit-utils": "1.8.22", "@reown/appkit-wallet": "1.8.22", "@walletconnect/universal-provider": "2.23.7", "bs58": "6.0.0", "semver": "7.7.2", "valtio": "2.1.7", "viem": ">=2.45.0" }, "optionalDependencies": { "@lit/react": "1.0.8" } }, "sha512-h8GfieZ/b5VRY0zxdCsjOyNDk26NM/qgtxl4ip7HThbHJ+HLILe2zGHlYfX567wwOyL+YPg0TYApwWA+pzvpCg=="], "@reown/appkit-adapter-wagmi": ["@reown/appkit-adapter-wagmi@1.8.22", "", { "dependencies": { "@reown/appkit": "1.8.22", "@reown/appkit-common": "1.8.22", "@reown/appkit-controllers": "1.8.22", "@reown/appkit-polyfills": "1.8.22", "@reown/appkit-scaffold-ui": "1.8.22", "@reown/appkit-utils": "1.8.22", "@reown/appkit-wallet": "1.8.22", "@walletconnect/universal-provider": "2.23.7", "valtio": "2.1.7" }, "optionalDependencies": { "@wagmi/connectors": ">=5.9.9" }, "peerDependencies": { "@wagmi/core": ">=2.21.2", "viem": ">=2.45.0", "wagmi": ">=2.19.5" } }, "sha512-uAHy1KJZRf/QheF5lddKjXh2ypTQkXG2jXlSK82trnlzCr91dgUcqMZpCGu4k9mztJhPDallvQgOGo5XHR902g=="], @@ -1655,6 +1705,8 @@ "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.3.0", "", {}, "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ=="], + "@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], "@smithy/core": ["@smithy/core@3.29.1", "", { "dependencies": { "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A=="], @@ -1681,10 +1733,14 @@ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@solana-program/compute-budget": ["@solana-program/compute-budget@0.11.0", "", { "peerDependencies": { "@solana/kit": "^5.0" } }, "sha512-7f1ePqB/eURkTwTOO9TNIdUXZcyrZoX3Uy2hNo7cXMfNhPFWp9AVgIyRNBc2jf15sdUa9gNpW+PfP2iV8AYAaw=="], + "@solana-program/system": ["@solana-program/system@0.10.0", "", { "peerDependencies": { "@solana/kit": "^5.0" } }, "sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g=="], "@solana-program/token": ["@solana-program/token@0.9.0", "", { "peerDependencies": { "@solana/kit": "^5.0" } }, "sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA=="], + "@solana-program/token-2022": ["@solana-program/token-2022@0.6.1", "", { "peerDependencies": { "@solana/kit": "^5.0", "@solana/sysvars": "^5.0" } }, "sha512-Ex02cruDMGfBMvZZCrggVR45vdQQSI/unHVpt/7HPt/IwFYB4eTlXtO8otYZyqV/ce5GqZ8S6uwyRf0zy6fdbA=="], + "@solana/accounts": ["@solana/accounts@5.5.1", "", { "dependencies": { "@solana/addresses": "5.5.1", "@solana/codecs-core": "5.5.1", "@solana/codecs-strings": "5.5.1", "@solana/errors": "5.5.1", "@solana/rpc-spec": "5.5.1", "@solana/rpc-types": "5.5.1" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ=="], "@solana/addresses": ["@solana/addresses@5.5.1", "", { "dependencies": { "@solana/assertions": "5.5.1", "@solana/codecs-core": "5.5.1", "@solana/codecs-strings": "5.5.1", "@solana/errors": "5.5.1", "@solana/nominal-types": "5.5.1" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA=="], @@ -1763,6 +1819,8 @@ "@solana/transactions": ["@solana/transactions@5.5.1", "", { "dependencies": { "@solana/addresses": "5.5.1", "@solana/codecs-core": "5.5.1", "@solana/codecs-data-structures": "5.5.1", "@solana/codecs-numbers": "5.5.1", "@solana/codecs-strings": "5.5.1", "@solana/errors": "5.5.1", "@solana/functional": "5.5.1", "@solana/instructions": "5.5.1", "@solana/keys": "5.5.1", "@solana/nominal-types": "5.5.1", "@solana/rpc-types": "5.5.1", "@solana/transaction-messages": "5.5.1" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA=="], + "@solana/wallet-standard-features": ["@solana/wallet-standard-features@1.4.0", "", { "dependencies": { "@wallet-standard/base": "^1.1.0", "@wallet-standard/features": "^1.1.0" } }, "sha512-f0tAdqwM2aL6CiFbIgt9h5zKFp+mgY/iNGwoxPMTj9VSTeQj7d1GGSmWhZw0XWoZ4N/1tnKTKmYFq+Dyq08jRw=="], + "@solidity-parser/parser": ["@solidity-parser/parser@0.20.2", "", {}, "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA=="], "@spruceid/siwe-parser": ["@spruceid/siwe-parser@2.1.2", "", { "dependencies": { "@noble/hashes": "^1.1.2", "apg-js": "^4.3.0", "uri-js": "^4.4.1", "valid-url": "^1.0.9" } }, "sha512-d/r3S1LwJyMaRAKQ0awmo9whfXeE88Qt00vRj91q5uv5ATtWIQEGJ67Yr5eSZw5zp1/fZCXZYuEckt8lSkereQ=="], @@ -1853,6 +1911,8 @@ "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + "@swc/types": ["@swc/types@0.1.27", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], @@ -2165,15 +2225,19 @@ "@wagmi/core": ["@wagmi/core@2.22.1", "", { "dependencies": { "eventemitter3": "5.0.1", "mipd": "0.0.7", "zustand": "5.0.0" }, "peerDependencies": { "@tanstack/query-core": ">=5.0.0", "typescript": ">=5.0.4", "viem": "2.x" }, "optionalPeers": ["@tanstack/query-core", "typescript"] }, "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg=="], + "@wallet-standard/app": ["@wallet-standard/app@1.1.1", "", { "dependencies": { "@wallet-standard/base": "^1.1.1" } }, "sha512-WDGwoByhP5gwHH01r5EaLgQdLVkACPCdOMQhmhn8rsm10h/siSgTorShzBxrn0ExSPof+Lu+C3TfgqBrPa1xoQ=="], + "@wallet-standard/base": ["@wallet-standard/base@1.1.1", "", {}, "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ=="], + "@wallet-standard/features": ["@wallet-standard/features@1.1.1", "", { "dependencies": { "@wallet-standard/base": "^1.1.1" } }, "sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA=="], + "@wallet-standard/wallet": ["@wallet-standard/wallet@1.1.0", "", { "dependencies": { "@wallet-standard/base": "^1.1.0" } }, "sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg=="], "@walletconnect/core": ["@walletconnect/core@2.23.10", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.23.10", "@walletconnect/utils": "2.23.10", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.45.1", "events": "3.3.0", "uint8arrays": "3.1.1" } }, "sha512-Qq2btHEoCgruvkZCWLSrVsvg/dYbM9Z045qeClwhJR4meL32jbIRT0mKWjf0HkRc2LA82MsnszVnfuZl3yWl5A=="], "@walletconnect/environment": ["@walletconnect/environment@1.0.1", "", { "dependencies": { "tslib": "1.14.1" } }, "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg=="], - "@walletconnect/ethereum-provider": ["@walletconnect/ethereum-provider@2.21.1", "", { "dependencies": { "@reown/appkit": "1.7.8", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/sign-client": "2.21.1", "@walletconnect/types": "2.21.1", "@walletconnect/universal-provider": "2.21.1", "@walletconnect/utils": "2.21.1", "events": "3.3.0" } }, "sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw=="], + "@walletconnect/ethereum-provider": ["@walletconnect/ethereum-provider@2.22.4", "", { "dependencies": { "@reown/appkit": "1.8.9", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "@walletconnect/sign-client": "2.22.4", "@walletconnect/types": "2.22.4", "@walletconnect/universal-provider": "2.22.4", "@walletconnect/utils": "2.22.4", "events": "3.3.0" } }, "sha512-qhBxU95nlndiKGz8lO8z9JlsA4Ai8i1via4VWut2fXsW1fkl6qXG9mYhDRFsbavuynUe3dQ+QLjBVDaaNkcKCA=="], "@walletconnect/events": ["@walletconnect/events@1.0.1", "", { "dependencies": { "keyvaluestorage-interface": "^1.0.0", "tslib": "1.14.1" } }, "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ=="], @@ -2253,6 +2317,8 @@ "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], @@ -2509,8 +2575,12 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001803", "", {}, "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg=="], + "canonicalize": ["canonicalize@2.1.0", "", { "bin": { "canonicalize": "bin/canonicalize.js" } }, "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ=="], + "capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="], "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], @@ -2665,6 +2735,10 @@ "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], + + "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], @@ -2689,6 +2763,8 @@ "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], "death": ["death@1.1.0", "", {}, "sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w=="], @@ -2913,6 +2989,8 @@ "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventemitter2": ["eventemitter2@6.4.9", "", {}, "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="], "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], @@ -2945,6 +3023,8 @@ "extension-port-stream": ["extension-port-stream@3.0.0", "", { "dependencies": { "readable-stream": "^3.6.2 || ^4.4.2", "webextension-polyfill": ">=0.10.0 <1.0" } }, "sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw=="], + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], @@ -2955,6 +3035,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-password-entropy": ["fast-password-entropy@1.1.1", "", {}, "sha512-dxm29/BPFrNgyEDygg/lf9c2xQR0vnQhG7+hZjAI39M/3um9fD4xiqG6F0ZjW6bya5m9CI0u6YryHGRtxCGCiw=="], + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], @@ -2977,6 +3059,8 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fetch-retry": ["fetch-retry@6.0.0", "", {}, "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], @@ -3155,6 +3239,8 @@ "helmet": ["helmet@4.6.0", "", {}, "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], @@ -3197,7 +3283,7 @@ "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "idb-keyval": ["idb-keyval@6.2.6", "", {}, "sha512-FY64UEhw+5liMzMQ1R9Mw6AF0+wyBrg1CIA1z4CjI/EvT5ty/SvQcWZgd8s9sgaNhX10Y8UzScTh89tEAls5nA=="], + "idb-keyval": ["idb-keyval@6.2.1", "", {}, "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -3377,7 +3463,7 @@ "joi": ["joi@17.13.4", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ=="], - "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -3441,6 +3527,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "libphonenumber-js": ["libphonenumber-js@1.13.9", "", {}, "sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -3861,7 +3949,9 @@ "pino": ["pino@10.0.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "slow-redact": "^0.3.0", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA=="], - "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + "pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="], + + "pino-pretty": ["pino-pretty@10.3.1", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^1.0.0", "pump": "^3.0.0", "readable-stream": "^4.0.0", "secure-json-parse": "^2.4.0", "sonic-boom": "^3.0.0", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g=="], "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], @@ -3889,6 +3979,8 @@ "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], @@ -3971,6 +4063,10 @@ "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + "react-aria": ["react-aria@3.50.0", "", { "dependencies": { "@internationalized/date": "^3.12.2", "@internationalized/number": "^3.6.7", "@internationalized/string": "^3.2.9", "@react-types/shared": "^3.36.0", "@swc/helpers": "^0.5.0", "aria-hidden": "^1.2.3", "clsx": "^2.0.0", "react-stately": "3.48.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw=="], + + "react-device-detect": ["react-device-detect@2.2.3", "", { "dependencies": { "ua-parser-js": "^1.0.33" }, "peerDependencies": { "react": ">= 0.14.0", "react-dom": ">= 0.14.0" } }, "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw=="], + "react-docgen": ["react-docgen@8.0.3", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.2", "@types/babel__core": "^7.20.5", "@types/babel__traverse": "^7.20.7", "@types/doctrine": "^0.0.9", "@types/resolve": "^1.20.2", "doctrine": "^3.0.0", "resolve": "^1.22.1", "strip-indent": "^4.0.0" } }, "sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w=="], "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], @@ -3989,6 +4085,8 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-stately": ["react-stately@3.48.0", "", { "dependencies": { "@internationalized/date": "^3.12.2", "@internationalized/number": "^3.6.7", "@internationalized/string": "^3.2.9", "@react-types/shared": "^3.36.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-toastify": ["react-toastify@11.1.0", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg=="], @@ -4041,6 +4139,8 @@ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], + "resolve": ["resolve@1.17.0", "", { "dependencies": { "path-parse": "^1.0.6" } }, "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w=="], "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], @@ -4103,6 +4203,10 @@ "secp256k1": ["secp256k1@4.0.4", "", { "dependencies": { "elliptic": "^6.5.7", "node-addon-api": "^5.0.0", "node-gyp-build": "^4.2.0" } }, "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw=="], + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + + "secure-password-utilities": ["secure-password-utilities@0.2.1", "", {}, "sha512-znUg8ae3cpuAaogiFBhP82gD2daVkSz4Qv/L7OWjB7wWvfbCdeqqQuJkm2/IvhKQPOV0T739YPR6rb7vs0uWaw=="], + "seek-bzip": ["seek-bzip@2.0.0", "", { "dependencies": { "commander": "^6.0.0" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -4131,7 +4235,7 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "set-cookie-parser": ["set-cookie-parser@3.1.1", "", {}, "sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -4197,7 +4301,7 @@ "solidity-coverage": ["solidity-coverage@0.8.17", "", { "dependencies": { "@ethersproject/abi": "^5.0.9", "@solidity-parser/parser": "^0.20.1", "chalk": "^2.4.2", "death": "^1.1.0", "difflib": "^0.2.4", "fs-extra": "^8.1.0", "ghost-testrpc": "^0.0.2", "global-modules": "^2.0.0", "globby": "^10.0.1", "jsonschema": "^1.2.4", "lodash": "^4.17.21", "mocha": "^10.2.0", "node-emoji": "^1.10.0", "pify": "^4.0.1", "recursive-readdir": "^2.2.2", "sc-istanbul": "^0.4.5", "semver": "^7.3.4", "shelljs": "^0.8.3", "web3-utils": "^1.3.6" }, "peerDependencies": { "hardhat": "^2.11.0" }, "bin": { "solidity-coverage": "plugins/bin.js" } }, "sha512-5P8vnB6qVX9tt1MfuONtCTEaEGO/O4WuEidPHIAJjx4sktHHKhO3rFvnE0q8L30nWJPTrcqGQMT7jpE29B2qow=="], - "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], + "sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], @@ -4293,6 +4397,10 @@ "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + "styled-components": ["styled-components@6.4.4", "", { "dependencies": { "@emotion/is-prop-valid": "1.4.0", "css-to-react-native": "3.2.0", "csstype": "3.2.3", "stylis": "4.3.6" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0", "react-native": ">= 0.68.0" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-tJk5CmKUPMDoTsZQ8m0hHInBk9HFKUPSusqmbBuefDX+yUbMBWea2Ob/wdXFyHW8XZqXkprJscysAdKeGWNqlA=="], + + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], "superstruct": ["superstruct@1.0.4", "", {}, "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ=="], @@ -4311,6 +4419,8 @@ "sync-rpc": ["sync-rpc@1.3.7", "", { "dependencies": { "get-port": "^3.1.0" } }, "sha512-YHciI7TUxL8EPqz/bg01sZfwuzQA0odao1wf1Ywdtw7j5vl30aQ6s+bLRTvgPPgzr94cg+WMm6Bxi/P7BJOxgw=="], + "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], + "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], "table-layout": ["table-layout@1.0.2", "", { "dependencies": { "array-back": "^4.0.1", "deep-extend": "~0.6.0", "typical": "^5.2.0", "wordwrapjs": "^4.0.0" } }, "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A=="], @@ -4359,6 +4469,8 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], @@ -4457,6 +4569,8 @@ "typical": ["typical@4.0.0", "", {}, "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw=="], + "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], @@ -4669,6 +4783,8 @@ "ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="], + "x402": ["x402@0.7.3", "", { "dependencies": { "@scure/base": "^1.2.6", "@solana-program/compute-budget": "^0.11.0", "@solana-program/token": "^0.9.0", "@solana-program/token-2022": "^0.6.1", "@solana/kit": "^5.0.0", "@solana/transaction-confirmation": "^5.0.0", "@solana/wallet-standard-features": "^1.3.0", "@wallet-standard/app": "^1.1.0", "@wallet-standard/base": "^1.1.0", "@wallet-standard/features": "^1.1.0", "viem": "^2.21.26", "wagmi": "^2.15.6", "zod": "^3.24.2" } }, "sha512-8CIZsdMTOn52PjMH/ErVke9ebeZ7ErwiZ5FL3tN3Wny7Ynxs3LkuB/0q7IoccRLdVXA7f2lueYBJ2iDrElhXnA=="], + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], @@ -4721,8 +4837,6 @@ "@base-org/account/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "@base-org/account/idb-keyval": ["idb-keyval@6.2.1", "", {}, "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="], - "@base-org/account/ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], "@base-org/account/zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], @@ -4733,17 +4847,9 @@ "@coinbase/cdp-sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@coinbase/wallet-sdk/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - "@coinbase/wallet-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], - "@coinbase/wallet-sdk/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - - "@coinbase/wallet-sdk/idb-keyval": ["idb-keyval@6.2.1", "", {}, "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="], - - "@coinbase/wallet-sdk/ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], - - "@coinbase/wallet-sdk/zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], + "@coinbase/wallet-sdk/preact": ["preact@10.29.5", "", {}, "sha512-zIai7HLEIz9c4GNfqpiuu5K9I1szXNtx0Ykr8f1Vbm1NasSVIvXGRxc8R8alLC+G+zonAt0TiUUBMn4+CiTgaA=="], "@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], @@ -4839,6 +4945,8 @@ "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@11.2.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-eu9h1R3j/wWc4rvmYJF5AKtlwniDzstrZ/c6KSz+HdI+n7I7iog9xyKmBfpUwSbG1TqPNZBzWjFMkzdYOKq6Bg=="], + "@graphql-tools/prisma-loader/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "@graphql-tools/relay-operation-optimizer/@graphql-tools/utils": ["@graphql-tools/utils@11.2.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-eu9h1R3j/wWc4rvmYJF5AKtlwniDzstrZ/c6KSz+HdI+n7I7iog9xyKmBfpUwSbG1TqPNZBzWjFMkzdYOKq6Bg=="], "@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@11.2.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-eu9h1R3j/wWc4rvmYJF5AKtlwniDzstrZ/c6KSz+HdI+n7I7iog9xyKmBfpUwSbG1TqPNZBzWjFMkzdYOKq6Bg=="], @@ -4929,6 +5037,12 @@ "@polkadot/x-ws/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "@privy-io/api-base/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@privy-io/react-auth/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.22.4", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "@walletconnect/sign-client": "2.22.4", "@walletconnect/types": "2.22.4", "@walletconnect/utils": "2.22.4", "es-toolkit": "1.39.3", "events": "3.3.0" } }, "sha512-TF2RNX13qxa0rrBAhVDs5+C2G8CHX7L0PH5hF2uyQHdGyxZ3pFbXf8rxmeW1yKlB76FSbW80XXNrUes6eK/xHg=="], + + "@privy-io/react-auth/lucide-react": ["lucide-react@0.554.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA=="], + "@reown/appkit/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.23.7", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "@walletconnect/sign-client": "2.23.7", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "es-toolkit": "1.44.0", "events": "3.3.0" } }, "sha512-6UicU/Mhr/1bh7MNoajypz7BhigORbHpP1LFTf8FYLQGDqzmqHMqmMH2GDAImtaY2sFTi2jBvc22tLl8VMze/A=="], "@reown/appkit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -4937,6 +5051,10 @@ "@reown/appkit-controllers/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.23.7", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "@walletconnect/sign-client": "2.23.7", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "es-toolkit": "1.44.0", "events": "3.3.0" } }, "sha512-6UicU/Mhr/1bh7MNoajypz7BhigORbHpP1LFTf8FYLQGDqzmqHMqmMH2GDAImtaY2sFTi2jBvc22tLl8VMze/A=="], + "@reown/appkit-utils/@base-org/account": ["@base-org/account@2.4.0", "", { "dependencies": { "@coinbase/cdp-sdk": "^1.0.0", "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.31.7", "zustand": "5.0.3" } }, "sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk": ["@coinbase/wallet-sdk@4.3.6", "", { "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.27.2", "zustand": "5.0.3" } }, "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA=="], + "@reown/appkit-utils/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.23.7", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "@walletconnect/sign-client": "2.23.7", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "es-toolkit": "1.44.0", "events": "3.3.0" } }, "sha512-6UicU/Mhr/1bh7MNoajypz7BhigORbHpP1LFTf8FYLQGDqzmqHMqmMH2GDAImtaY2sFTi2jBvc22tLl8VMze/A=="], "@reown/appkit-wallet/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], @@ -5057,26 +5175,36 @@ "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "@wagmi/connectors/@base-org/account": ["@base-org/account@2.4.0", "", { "dependencies": { "@coinbase/cdp-sdk": "^1.0.0", "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.31.7", "zustand": "5.0.3" } }, "sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug=="], + + "@wagmi/connectors/@coinbase/wallet-sdk": ["@coinbase/wallet-sdk@4.3.6", "", { "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.27.2", "zustand": "5.0.3" } }, "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider": ["@walletconnect/ethereum-provider@2.21.1", "", { "dependencies": { "@reown/appkit": "1.7.8", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/sign-client": "2.21.1", "@walletconnect/types": "2.21.1", "@walletconnect/universal-provider": "2.21.1", "@walletconnect/utils": "2.21.1", "events": "3.3.0" } }, "sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw=="], + "@wagmi/core/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "@wagmi/core/zustand": ["zustand@5.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ=="], "@walletconnect/environment/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "@walletconnect/ethereum-provider/@reown/appkit": ["@reown/appkit@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-pay": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@reown/appkit-scaffold-ui": "1.7.8", "@reown/appkit-ui": "1.7.8", "@reown/appkit-utils": "1.7.8", "@reown/appkit-wallet": "1.7.8", "@walletconnect/types": "2.21.0", "@walletconnect/universal-provider": "2.21.0", "bs58": "6.0.0", "valtio": "1.13.2", "viem": ">=2.29.0" } }, "sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA=="], + "@walletconnect/ethereum-provider/@reown/appkit": ["@reown/appkit@1.8.9", "", { "dependencies": { "@reown/appkit-common": "1.8.9", "@reown/appkit-controllers": "1.8.9", "@reown/appkit-pay": "1.8.9", "@reown/appkit-polyfills": "1.8.9", "@reown/appkit-scaffold-ui": "1.8.9", "@reown/appkit-ui": "1.8.9", "@reown/appkit-utils": "1.8.9", "@reown/appkit-wallet": "1.8.9", "@walletconnect/universal-provider": "2.21.9", "bs58": "6.0.0", "semver": "7.7.2", "valtio": "2.1.7", "viem": ">=2.37.9" }, "optionalDependencies": { "@lit/react": "1.0.8" } }, "sha512-e3N2DAzf3Xv3jnoD8IsUo0/Yfwuhk7npwJBe1+9rDJIRwgPsyYcCLD4gKPDFC5IUIfOLqK7YtGOh9oPEUnIWpw=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.21.1", "", { "dependencies": { "@walletconnect/core": "2.21.1", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.1", "@walletconnect/utils": "2.21.1", "events": "3.3.0" } }, "sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg=="], + "@walletconnect/ethereum-provider/@walletconnect/logger": ["@walletconnect/logger@3.0.0", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "10.0.0" } }, "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg=="], - "@walletconnect/ethereum-provider/@walletconnect/types": ["@walletconnect/types@2.21.1", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ=="], + "@walletconnect/ethereum-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.22.4", "", { "dependencies": { "@walletconnect/core": "2.22.4", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "3.0.0", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.22.4", "@walletconnect/utils": "2.22.4", "events": "3.3.0" } }, "sha512-la+sol0KL33Fyx5DRlupHREIv8wA6W33bRfuLAfLm8pINRTT06j9rz0IHIqJihiALebFxVZNYzJnF65PhV0q3g=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.1", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.1", "@walletconnect/types": "2.21.1", "@walletconnect/utils": "2.21.1", "es-toolkit": "1.33.0", "events": "3.3.0" } }, "sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg=="], + "@walletconnect/ethereum-provider/@walletconnect/types": ["@walletconnect/types@2.22.4", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "events": "3.3.0" } }, "sha512-KJdiS9ezXzx1uASanldYaaenDwb42VOQ6Rj86H7FRwfYddhNnYnyEaDjDKOdToGRGcpt5Uzom6qYUOnrWEbp5g=="], - "@walletconnect/ethereum-provider/@walletconnect/utils": ["@walletconnect/utils@2.21.1", "", { "dependencies": { "@noble/ciphers": "1.2.1", "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.1", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "query-string": "7.1.3", "uint8arrays": "3.1.0", "viem": "2.23.2" } }, "sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA=="], + "@walletconnect/ethereum-provider/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.22.4", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "@walletconnect/sign-client": "2.22.4", "@walletconnect/types": "2.22.4", "@walletconnect/utils": "2.22.4", "es-toolkit": "1.39.3", "events": "3.3.0" } }, "sha512-TF2RNX13qxa0rrBAhVDs5+C2G8CHX7L0PH5hF2uyQHdGyxZ3pFbXf8rxmeW1yKlB76FSbW80XXNrUes6eK/xHg=="], + + "@walletconnect/ethereum-provider/@walletconnect/utils": ["@walletconnect/utils@2.22.4", "", { "dependencies": { "@msgpack/msgpack": "3.1.2", "@noble/ciphers": "1.3.0", "@noble/curves": "1.9.7", "@noble/hashes": "1.8.0", "@scure/base": "1.2.6", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.22.4", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "blakejs": "1.2.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "ox": "0.9.3", "uint8arrays": "3.1.1" } }, "sha512-coAPrNiTiD+snpiXQyXakMVeYcddqVqII7aLU39TeILdPoXeNPc2MAja+MF7cKNM/PA3tespljvvxck/oTm4+Q=="], "@walletconnect/events/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "@walletconnect/jsonrpc-utils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "@walletconnect/keyvaluestorage/idb-keyval": ["idb-keyval@6.2.6", "", {}, "sha512-FY64UEhw+5liMzMQ1R9Mw6AF0+wyBrg1CIA1z4CjI/EvT5ty/SvQcWZgd8s9sgaNhX10Y8UzScTh89tEAls5nA=="], + "@walletconnect/modal-core/valtio": ["valtio@1.11.2", "", { "dependencies": { "proxy-compare": "2.5.1", "use-sync-external-store": "1.2.0" }, "peerDependencies": { "@types/react": ">=16.8", "react": ">=16.8" }, "optionalPeers": ["@types/react", "react"] }, "sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw=="], "@walletconnect/modal-ui/lit": ["lit@2.8.0", "", { "dependencies": { "@lit/reactive-element": "^1.6.0", "lit-element": "^3.3.0", "lit-html": "^2.8.0" } }, "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA=="], @@ -5279,6 +5407,8 @@ "hardhat/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "headers-polyfill/set-cookie-parser": ["set-cookie-parser@3.1.1", "", {}, "sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "http-basic/concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="], @@ -5387,8 +5517,18 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "pino/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino/sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], + + "pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "pino-pretty/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "porto/idb-keyval": ["idb-keyval@6.2.6", "", {}, "sha512-FY64UEhw+5liMzMQ1R9Mw6AF0+wyBrg1CIA1z4CjI/EvT5ty/SvQcWZgd8s9sgaNhX10Y8UzScTh89tEAls5nA=="], + "porto/ox": ["ox@0.9.17", "", { "dependencies": { "@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.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-rKAnhzhRU3Xh3hiko+i1ZxywZ55eWQzeS/Q4HRKLx2PqfHOolisZHErSsJVipGlmQKHW5qwOED/GighEw9dbLg=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -5487,6 +5627,10 @@ "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "styled-components/@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], + + "styled-components/stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "sync-fetch/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -5575,6 +5719,8 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "x402/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "z-schema/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "@base-org/account/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -5583,12 +5729,6 @@ "@base-org/account/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@coinbase/wallet-sdk/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - - "@coinbase/wallet-sdk/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@coinbase/wallet-sdk/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], @@ -5661,6 +5801,16 @@ "@metamask/sdk/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/logger": ["@walletconnect/logger@3.0.0", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "10.0.0" } }, "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.22.4", "", { "dependencies": { "@walletconnect/core": "2.22.4", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "3.0.0", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.22.4", "@walletconnect/utils": "2.22.4", "events": "3.3.0" } }, "sha512-la+sol0KL33Fyx5DRlupHREIv8wA6W33bRfuLAfLm8pINRTT06j9rz0IHIqJihiALebFxVZNYzJnF65PhV0q3g=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/types": ["@walletconnect/types@2.22.4", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "events": "3.3.0" } }, "sha512-KJdiS9ezXzx1uASanldYaaenDwb42VOQ6Rj86H7FRwfYddhNnYnyEaDjDKOdToGRGcpt5Uzom6qYUOnrWEbp5g=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/utils": ["@walletconnect/utils@2.22.4", "", { "dependencies": { "@msgpack/msgpack": "3.1.2", "@noble/ciphers": "1.3.0", "@noble/curves": "1.9.7", "@noble/hashes": "1.8.0", "@scure/base": "1.2.6", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.22.4", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "blakejs": "1.2.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "ox": "0.9.3", "uint8arrays": "3.1.1" } }, "sha512-coAPrNiTiD+snpiXQyXakMVeYcddqVqII7aLU39TeILdPoXeNPc2MAja+MF7cKNM/PA3tespljvvxck/oTm4+Q=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.39.3", "", {}, "sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww=="], + "@reown/appkit-adapter-wagmi/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.23.7", "", { "dependencies": { "@walletconnect/core": "2.23.7", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "3.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "events": "3.3.0" } }, "sha512-SX61lzb1bTl/LijlcHQttnoHPBzzoY5mW9ArR6qhFtDNDTS7yr2rcH7rCngxHlYeb4rAYcWLHgbiGSrdKxl/mg=="], "@reown/appkit-adapter-wagmi/@walletconnect/universal-provider/@walletconnect/types": ["@walletconnect/types@2.23.7", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "events": "3.3.0" } }, "sha512-6PAKK+iR2IntmlkCFLMAHjYeIaerCJJYRDmdRimhon0u+aNmQT+HyGM6zxDAth0rdpBD7qEvKP5IXZTE7KFUhw=="], @@ -5677,6 +5827,26 @@ "@reown/appkit-controllers/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + "@reown/appkit-utils/@base-org/account/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], + + "@reown/appkit-utils/@base-org/account/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + + "@reown/appkit-utils/@base-org/account/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "@reown/appkit-utils/@base-org/account/ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], + + "@reown/appkit-utils/@base-org/account/zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], + "@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.23.7", "", { "dependencies": { "@walletconnect/core": "2.23.7", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "3.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "events": "3.3.0" } }, "sha512-SX61lzb1bTl/LijlcHQttnoHPBzzoY5mW9ArR6qhFtDNDTS7yr2rcH7rCngxHlYeb4rAYcWLHgbiGSrdKxl/mg=="], "@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/types": ["@walletconnect/types@2.23.7", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "events": "3.3.0" } }, "sha512-6PAKK+iR2IntmlkCFLMAHjYeIaerCJJYRDmdRimhon0u+aNmQT+HyGM6zxDAth0rdpBD7qEvKP5IXZTE7KFUhw=="], @@ -5751,45 +5921,65 @@ "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-common": ["@reown/appkit-common@1.7.8", "", { "dependencies": { "big.js": "6.2.2", "dayjs": "1.11.13", "viem": ">=2.29.0" } }, "sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ=="], + "@wagmi/connectors/@base-org/account/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], + + "@wagmi/connectors/@base-org/account/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + + "@wagmi/connectors/@base-org/account/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "@wagmi/connectors/@base-org/account/ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-controllers": ["@reown/appkit-controllers@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-wallet": "1.7.8", "@walletconnect/universal-provider": "2.21.0", "valtio": "1.13.2", "viem": ">=2.29.0" } }, "sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA=="], + "@wagmi/connectors/@base-org/account/zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-pay": ["@reown/appkit-pay@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-ui": "1.7.8", "@reown/appkit-utils": "1.7.8", "lit": "3.3.0", "valtio": "1.13.2" } }, "sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw=="], + "@wagmi/connectors/@coinbase/wallet-sdk/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-polyfills": ["@reown/appkit-polyfills@1.7.8", "", { "dependencies": { "buffer": "6.0.3" } }, "sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA=="], + "@wagmi/connectors/@coinbase/wallet-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-scaffold-ui": ["@reown/appkit-scaffold-ui@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-ui": "1.7.8", "@reown/appkit-utils": "1.7.8", "@reown/appkit-wallet": "1.7.8", "lit": "3.3.0" } }, "sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ=="], + "@wagmi/connectors/@coinbase/wallet-sdk/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-ui": ["@reown/appkit-ui@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-wallet": "1.7.8", "lit": "3.3.0", "qrcode": "1.5.3" } }, "sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow=="], + "@wagmi/connectors/@coinbase/wallet-sdk/ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils": ["@reown/appkit-utils@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@reown/appkit-wallet": "1.7.8", "@walletconnect/logger": "2.1.2", "@walletconnect/universal-provider": "2.21.0", "valtio": "1.13.2", "viem": ">=2.29.0" } }, "sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw=="], + "@wagmi/connectors/@coinbase/wallet-sdk/zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet": ["@reown/appkit-wallet@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@walletconnect/logger": "2.1.2", "zod": "3.22.4" } }, "sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit": ["@reown/appkit@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-pay": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@reown/appkit-scaffold-ui": "1.7.8", "@reown/appkit-ui": "1.7.8", "@reown/appkit-utils": "1.7.8", "@reown/appkit-wallet": "1.7.8", "@walletconnect/types": "2.21.0", "@walletconnect/universal-provider": "2.21.0", "bs58": "6.0.0", "valtio": "1.13.2", "viem": ">=2.29.0" } }, "sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types": ["@walletconnect/types@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.21.1", "", { "dependencies": { "@walletconnect/core": "2.21.1", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.1", "@walletconnect/utils": "2.21.1", "events": "3.3.0" } }, "sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.0", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "es-toolkit": "1.33.0", "events": "3.3.0" } }, "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types": ["@walletconnect/types@2.21.1", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ=="], - "@walletconnect/ethereum-provider/@reown/appkit/valtio": ["valtio@1.13.2", "", { "dependencies": { "derive-valtio": "0.1.0", "proxy-compare": "2.6.0", "use-sync-external-store": "1.2.0" }, "peerDependencies": { "@types/react": ">=16.8", "react": ">=16.8" }, "optionalPeers": ["@types/react", "react"] }, "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.1", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.1", "@walletconnect/types": "2.21.1", "@walletconnect/utils": "2.21.1", "es-toolkit": "1.33.0", "events": "3.3.0" } }, "sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.21.1", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.1", "@walletconnect/utils": "2.21.1", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/utils": ["@walletconnect/utils@2.21.1", "", { "dependencies": { "@noble/ciphers": "1.2.1", "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.1", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "query-string": "7.1.3", "uint8arrays": "3.1.0", "viem": "2.23.2" } }, "sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-common": ["@reown/appkit-common@1.8.9", "", { "dependencies": { "big.js": "6.2.2", "dayjs": "1.11.13", "viem": ">=2.37.9" } }, "sha512-drseYLBDqcQR2WvhfAwrKRiDJdTmsmwZsRBg72sxQDvAwxfKNSmiqsqURq5c/Q9SeeTwclge58Dyq7Ijo6TeeQ=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-controllers": ["@reown/appkit-controllers@1.8.9", "", { "dependencies": { "@reown/appkit-common": "1.8.9", "@reown/appkit-wallet": "1.8.9", "@walletconnect/universal-provider": "2.21.9", "valtio": "2.1.7", "viem": ">=2.37.9" } }, "sha512-/8hgFAgiYCTDG3gSxJr8hXy6GnO28UxN8JOXFUEi5gOODy7d3+3Jwm+7OEghf7hGKrShDedibsXdXKdX1PUT+g=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-pay": ["@reown/appkit-pay@1.8.9", "", { "dependencies": { "@reown/appkit-common": "1.8.9", "@reown/appkit-controllers": "1.8.9", "@reown/appkit-ui": "1.8.9", "@reown/appkit-utils": "1.8.9", "lit": "3.3.0", "valtio": "2.1.7" } }, "sha512-AEmaPqxnzjawSRFenyiTtq0vjKM5IPb2CTD9wa+OMXFpe6FissO+1Eg1H47sfdrycZCvUizSRmQmYqkJaI8BCw=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-polyfills": ["@reown/appkit-polyfills@1.8.9", "", { "dependencies": { "buffer": "6.0.3" } }, "sha512-33YCU8dxe4UkpNf9qCAaHx5crSoEu6tbmZxE/0eEPCYRDRXoiH9VGiN7xwTDOVduacg/U8H6/32ibmYZKnRk5Q=="], - "@walletconnect/ethereum-provider/@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-scaffold-ui": ["@reown/appkit-scaffold-ui@1.8.9", "", { "dependencies": { "@reown/appkit-common": "1.8.9", "@reown/appkit-controllers": "1.8.9", "@reown/appkit-ui": "1.8.9", "@reown/appkit-utils": "1.8.9", "@reown/appkit-wallet": "1.8.9", "lit": "3.3.0" } }, "sha512-F7PSM1nxvlvj2eu8iL355GzvCNiL8RKiCqT1zag8aB4QpxjU24l+vAF6debtkg4HY8nJOyDifZ7Z1jkKrHlIDQ=="], - "@walletconnect/ethereum-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-ui": ["@reown/appkit-ui@1.8.9", "", { "dependencies": { "@phosphor-icons/webcomponents": "2.1.5", "@reown/appkit-common": "1.8.9", "@reown/appkit-controllers": "1.8.9", "@reown/appkit-wallet": "1.8.9", "lit": "3.3.0", "qrcode": "1.5.3" } }, "sha512-WR17ql77KOMKfyDh7RW4oSfmj+p5gIl0u8Wmopzbx5Hd0HcPVZ5HmTDpwOM9WCSxYcin0fsSAoI+nVdvrhWNtw=="], - "@walletconnect/ethereum-provider/@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils": ["@reown/appkit-utils@1.8.9", "", { "dependencies": { "@reown/appkit-common": "1.8.9", "@reown/appkit-controllers": "1.8.9", "@reown/appkit-polyfills": "1.8.9", "@reown/appkit-wallet": "1.8.9", "@wallet-standard/wallet": "1.1.0", "@walletconnect/logger": "2.1.2", "@walletconnect/universal-provider": "2.21.9", "valtio": "2.1.7", "viem": ">=2.37.9" } }, "sha512-U9hx4h7tIE7ha/QWKjZpZc/imaLumdwe0QNdku9epjp/npXVjGuwUrW5mj8yWNSkjtQpY/BEItNdDAUKZ7rrjw=="], - "@walletconnect/ethereum-provider/@walletconnect/utils/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet": ["@reown/appkit-wallet@1.8.9", "", { "dependencies": { "@reown/appkit-common": "1.8.9", "@reown/appkit-polyfills": "1.8.9", "@walletconnect/logger": "2.1.2", "zod": "3.22.4" } }, "sha512-rcAXvkzOVG4941eZVCGtr2dSJAMOclzZGSe+8hnOUnhK4zxa5svxiP6K9O5SMBp3MrAS3WNsRj5hqx6+JHb7iA=="], + + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.9", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.9", "@walletconnect/types": "2.21.9", "@walletconnect/utils": "2.21.9", "es-toolkit": "1.39.3", "events": "3.3.0" } }, "sha512-dVA9DWSz9jYe37FW5GSRV5zlY9E7rX1kktcDGI7i1/9oG/z9Pk5UKp5r/DFys4Zjml9wZc46R/jlEgeBXTT06A=="], + + "@walletconnect/ethereum-provider/@reown/appkit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.22.4", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.22.4", "@walletconnect/utils": "2.22.4", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.39.3", "events": "3.3.0", "uint8arrays": "3.1.1" } }, "sha512-ZQnyDDpqDPAk5lyLV19BRccQ3wwK3LmAwibuIv3X+44aT/dOs2kQGu9pla3iW2LgZ5qRMYvgvvfr5g3WlDGceQ=="], + + "@walletconnect/ethereum-provider/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.39.3", "", {}, "sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww=="], + + "@walletconnect/ethereum-provider/@walletconnect/utils/@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="], + + "@walletconnect/ethereum-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@walletconnect/ethereum-provider/@walletconnect/utils/ox": ["ox@0.9.3", "", { "dependencies": { "@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.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg=="], "@walletconnect/modal-core/valtio/proxy-compare": ["proxy-compare@2.5.1", "", {}, "sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA=="], @@ -5997,6 +6187,8 @@ "solidity-coverage/web3-utils/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], + "styled-components/@emotion/is-prop-valid/@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + "test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], @@ -6189,6 +6381,14 @@ "@metamask/providers/@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.22.4", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.0", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.22.4", "@walletconnect/utils": "2.22.4", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.39.3", "events": "3.3.0", "uint8arrays": "3.1.1" } }, "sha512-ZQnyDDpqDPAk5lyLV19BRccQ3wwK3LmAwibuIv3X+44aT/dOs2kQGu9pla3iW2LgZ5qRMYvgvvfr5g3WlDGceQ=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/utils/@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/utils/ox": ["ox@0.9.3", "", { "dependencies": { "@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.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg=="], + "@reown/appkit-adapter-wagmi/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.23.7", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.44.0", "events": "3.3.0", "uint8arrays": "3.1.1" } }, "sha512-yTyymn9mFaDZkUfLfZ3E9VyaSDPeHAXlrPxQRmNx2zFsEt/25GmTU2A848aomimLxZnAG2jNLhxbJ8I0gyNV+w=="], "@reown/appkit-adapter-wagmi/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -6201,6 +6401,18 @@ "@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/ox": ["ox@0.9.3", "", { "dependencies": { "@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.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg=="], + "@reown/appkit-utils/@base-org/account/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@reown/appkit-utils/@base-org/account/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@reown/appkit-utils/@base-org/account/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@reown/appkit-utils/@coinbase/wallet-sdk/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + "@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.23.7", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.44.0", "events": "3.3.0", "uint8arrays": "3.1.1" } }, "sha512-yTyymn9mFaDZkUfLfZ3E9VyaSDPeHAXlrPxQRmNx2zFsEt/25GmTU2A848aomimLxZnAG2jNLhxbJ8I0gyNV+w=="], "@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -6231,35 +6443,81 @@ "@types/minimatch/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@wagmi/connectors/@base-org/account/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@wagmi/connectors/@base-org/account/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@wagmi/connectors/@base-org/account/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@wagmi/connectors/@coinbase/wallet-sdk/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@wagmi/connectors/@coinbase/wallet-sdk/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@wagmi/connectors/@coinbase/wallet-sdk/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-common": ["@reown/appkit-common@1.7.8", "", { "dependencies": { "big.js": "6.2.2", "dayjs": "1.11.13", "viem": ">=2.29.0" } }, "sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-controllers": ["@reown/appkit-controllers@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-wallet": "1.7.8", "@walletconnect/universal-provider": "2.21.0", "valtio": "1.13.2", "viem": ">=2.29.0" } }, "sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-pay": ["@reown/appkit-pay@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-ui": "1.7.8", "@reown/appkit-utils": "1.7.8", "lit": "3.3.0", "valtio": "1.13.2" } }, "sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-polyfills": ["@reown/appkit-polyfills@1.7.8", "", { "dependencies": { "buffer": "6.0.3" } }, "sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-scaffold-ui": ["@reown/appkit-scaffold-ui@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-ui": "1.7.8", "@reown/appkit-utils": "1.7.8", "@reown/appkit-wallet": "1.7.8", "lit": "3.3.0" } }, "sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-ui": ["@reown/appkit-ui@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-wallet": "1.7.8", "lit": "3.3.0", "qrcode": "1.5.3" } }, "sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils": ["@reown/appkit-utils@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@reown/appkit-wallet": "1.7.8", "@walletconnect/logger": "2.1.2", "@walletconnect/universal-provider": "2.21.0", "valtio": "1.13.2", "viem": ">=2.29.0" } }, "sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet": ["@reown/appkit-wallet@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@walletconnect/logger": "2.1.2", "zod": "3.22.4" } }, "sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types": ["@walletconnect/types@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.0", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "es-toolkit": "1.33.0", "events": "3.3.0" } }, "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/valtio": ["valtio@1.13.2", "", { "dependencies": { "derive-valtio": "0.1.0", "proxy-compare": "2.6.0", "use-sync-external-store": "1.2.0" }, "peerDependencies": { "@types/react": ">=16.8", "react": ">=16.8" }, "optionalPeers": ["@types/react", "react"] }, "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.21.1", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.1", "@walletconnect/utils": "2.21.1", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/utils/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.21.0", "", { "dependencies": { "@walletconnect/core": "2.21.0", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "events": "3.3.0" } }, "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils": ["@walletconnect/utils@2.21.0", "", { "dependencies": { "@noble/ciphers": "1.2.1", "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "query-string": "7.1.3", "uint8arrays": "3.1.0", "viem": "2.23.2" } }, "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.21.9", "", { "dependencies": { "@walletconnect/core": "2.21.9", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.9", "@walletconnect/utils": "2.21.9", "events": "3.3.0" } }, "sha512-EKLDS97o1rk/0XilD0nQdSR9SNgRsVoIK5M5HpS9sDTvHPv2EF5pIqu6Xr2vLsKcQ0KnCx+D5bnpav8Yh4NVZg=="], - "@walletconnect/ethereum-provider/@reown/appkit/valtio/proxy-compare": ["proxy-compare@2.6.0", "", {}, "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="], + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/types": ["@walletconnect/types@2.21.9", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-+82TRNX3lGRO96WyLISaBs/FkLts7y4hVgmOI4we84I7XdBu1xsjgiJj0JwYXnurz+X94lTqzOkzPps+wadWKw=="], - "@walletconnect/ethereum-provider/@reown/appkit/valtio/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils": ["@walletconnect/utils@2.21.9", "", { "dependencies": { "@msgpack/msgpack": "3.1.2", "@noble/ciphers": "1.3.0", "@noble/curves": "1.9.7", "@noble/hashes": "1.8.0", "@scure/base": "1.2.6", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.9", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "blakejs": "1.2.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "uint8arrays": "3.1.1", "viem": "2.36.0" } }, "sha512-FHagysDvp7yQl+74veIeuqwZZnMiTyTW3Lw0NXsbIKnlmlSQu5pma+4EnRD/CnSzbN6PV39k2t1KBaaZ4PjDgg=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core/es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.39.3", "", {}, "sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], + "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core/es-toolkit": ["es-toolkit@1.39.3", "", {}, "sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], + "@walletconnect/ethereum-provider/@walletconnect/utils/ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], + "@walletconnect/ethereum-provider/@walletconnect/utils/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], + "@walletconnect/ethereum-provider/@walletconnect/utils/ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], @@ -6379,6 +6637,12 @@ "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/utils/ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/utils/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@privy-io/react-auth/@walletconnect/universal-provider/@walletconnect/utils/ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "@reown/appkit-adapter-wagmi/@walletconnect/universal-provider/@walletconnect/utils/ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], "@reown/appkit-adapter-wagmi/@walletconnect/universal-provider/@walletconnect/utils/ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], @@ -6409,85 +6673,127 @@ "@storybook/react-vite/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger": ["@walletconnect/logger@2.1.2", "", { "dependencies": { "@walletconnect/safe-json": "^1.0.2", "pino": "7.11.0" } }, "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.21.0", "", { "dependencies": { "@walletconnect/core": "2.21.0", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "events": "3.3.0" } }, "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils": ["@walletconnect/utils@2.21.0", "", { "dependencies": { "@noble/ciphers": "1.2.1", "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "query-string": "7.1.3", "uint8arrays": "3.1.0", "viem": "2.23.2" } }, "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/valtio/proxy-compare": ["proxy-compare@2.6.0", "", {}, "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/valtio/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core/es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/core/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], + "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.21.0", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw=="], + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.21.9", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.9", "@walletconnect/utils": "2.21.9", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.39.3", "events": "3.3.0", "uint8arrays": "3.1.1" } }, "sha512-SlSknLvbO4i9Y4y8zU0zeCuJv1klQIUX3HRSBs1BaYvQKVVkrdiWPgRj4jcrL2wEOINa9NXw6HXp6x5XCXOolA=="], + + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="], + + "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "command-line-usage/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="], + "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + "ghost-testrpc/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "graphql-config/@graphql-tools/url-loader/@graphql-tools/wrap/@graphql-tools/delegate/@graphql-tools/batch-execute": ["@graphql-tools/batch-execute@10.0.9", "", { "dependencies": { "@graphql-tools/utils": "^11.0.0", "@whatwg-node/promise-helpers": "^1.3.2", "dataloader": "^2.2.3", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-khIgAPlyaWJ3dVX6SsqOkABZCH1Gii32WHn3xMzavupsxPCfb/9G3zjdswptzTFrOcZ92dWo7MXvwNFkRfNN4w=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], + "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + "solidity-coverage/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + "solidity-coverage/web3-utils/ethereum-cryptography/@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + "solidity-coverage/web3-utils/ethereum-cryptography/@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - "@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.21.0", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], - "@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/sign-client/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], - "@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], - "command-line-usage/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], - "ghost-testrpc/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/types/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], - "graphql-config/@graphql-tools/url-loader/@graphql-tools/wrap/@graphql-tools/delegate/@graphql-tools/batch-execute": ["@graphql-tools/batch-execute@10.0.9", "", { "dependencies": { "@graphql-tools/utils": "^11.0.0", "@whatwg-node/promise-helpers": "^1.3.2", "dataloader": "^2.2.3", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-khIgAPlyaWJ3dVX6SsqOkABZCH1Gii32WHn3xMzavupsxPCfb/9G3zjdswptzTFrOcZ92dWo7MXvwNFkRfNN4w=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], - "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], - "solidity-coverage/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], - "solidity-coverage/web3-utils/ethereum-cryptography/@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - "solidity-coverage/web3-utils/ethereum-cryptography/@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@walletconnect/universal-provider/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], @@ -6517,20 +6823,6 @@ "@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], - - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], @@ -6545,10 +6837,66 @@ "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], - "@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-utils/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@reown/appkit-wallet/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/types/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/logger/pino/thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + + "@wagmi/connectors/@walletconnect/ethereum-provider/@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core/uint8arrays": ["uint8arrays@3.1.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog=="], } } From ef005aa28418f26d7c75ffdc38a09039d8ca9f6e Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 27 Jul 2026 19:10:01 +0200 Subject: [PATCH 03/16] feat(api): add verified Privy wallet registry --- apps/api/.env.example | 5 + .../src/api/controllers/wallets.controller.ts | 119 ++++++++++ apps/api/src/api/routes/v1/index.ts | 9 + apps/api/src/api/routes/v1/wallets.route.ts | 12 + .../services/wallets/privyWallet.service.ts | 85 ++++++++ .../services/wallets/profileWallet.service.ts | 129 +++++++++++ apps/api/src/config/vars.test.ts | 21 ++ apps/api/src/config/vars.ts | 14 ++ .../055-add-wallet-mode-to-profiles.ts | 21 ++ .../migrations/056-create-profile-wallets.ts | 102 +++++++++ apps/api/src/models/index.ts | 4 + apps/api/src/models/profileWallet.model.ts | 116 ++++++++++ apps/api/src/models/user.model.ts | 9 +- .../api/src/tests/wallets.integration.test.ts | 205 ++++++++++++++++++ 14 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/api/controllers/wallets.controller.ts create mode 100644 apps/api/src/api/routes/v1/wallets.route.ts create mode 100644 apps/api/src/api/services/wallets/privyWallet.service.ts create mode 100644 apps/api/src/api/services/wallets/profileWallet.service.ts create mode 100644 apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts create mode 100644 apps/api/src/database/migrations/056-create-profile-wallets.ts create mode 100644 apps/api/src/models/profileWallet.model.ts create mode 100644 apps/api/src/tests/wallets.integration.test.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index c0b4318e0..2cfc632e5 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -21,6 +21,11 @@ SUPABASE_URL=https://your-project-id.supabase.co SUPABASE_ANON_KEY=your-anon-key-here SUPABASE_SERVICE_KEY=your-service-role-key-here +# Optional Privy embedded-wallet metadata verification. Keep the secret server-side. +PRIVY_WALLET_REGISTRATION_ENABLED=false +PRIVY_APP_ID= +PRIVY_APP_SECRET= + # Database DB_HOST=localhost DB_PORT=5432 diff --git a/apps/api/src/api/controllers/wallets.controller.ts b/apps/api/src/api/controllers/wallets.controller.ts new file mode 100644 index 000000000..1112d0177 --- /dev/null +++ b/apps/api/src/api/controllers/wallets.controller.ts @@ -0,0 +1,119 @@ +import { Request, Response } from "express"; +import httpStatus from "http-status"; +import { UniqueConstraintError } from "sequelize"; +import logger from "../../config/logger"; +import { sequelize } from "../../models"; +import { PrivyWalletVerificationError } from "../services/wallets/privyWallet.service"; +import { + listProfileWallets, + registerPrivyWallet, + setWalletMode, + type WalletMode, + WalletModeConflictError, + WalletRegistrationConflictError +} from "../services/wallets/profileWallet.service"; + +function sendError(res: Response, status: number, code: string, message: string): void { + res.status(status).json({ error: { code, message, status } }); +} + +function requireUserId(req: Request, res: Response): string | null { + if (!req.userId) { + sendError(res, httpStatus.UNAUTHORIZED, "AUTHENTICATION_REQUIRED", "Authentication required"); + return null; + } + return req.userId; +} + +function sendWalletModeConflict(res: Response, error: WalletModeConflictError): void { + sendError(res, httpStatus.CONFLICT, error.kind === "active_ramp" ? "ACTIVE_RAMP" : "WALLET_NOT_REGISTERED", error.message); +} + +function serializeWallet(wallet: Awaited>) { + return { + address: wallet.address, + chainType: wallet.chainType, + createdAt: wallet.createdAt, + id: wallet.id, + lastUsedAt: wallet.lastUsedAt, + provider: wallet.provider, + providerWalletId: wallet.providerWalletId, + status: wallet.status + }; +} + +export async function getWallets(req: Request, res: Response): Promise { + const profileId = requireUserId(req, res); + if (!profileId) return; + + try { + const result = await listProfileWallets(profileId); + res.status(httpStatus.OK).json({ + mode: result.mode, + wallets: result.wallets.map(serializeWallet) + }); + } catch (error) { + logger.error("Failed to list profile wallets", error); + sendError(res, httpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "Failed to list wallets"); + } +} + +export async function updateWalletMode(req: Request, res: Response): Promise { + const profileId = requireUserId(req, res); + if (!profileId) return; + + const { mode } = (req.body ?? {}) as { mode?: unknown }; + if (mode !== null && mode !== "external" && mode !== "privy_embedded") { + sendError(res, httpStatus.BAD_REQUEST, "INVALID_WALLET_MODE", "mode must be external, privy_embedded, or null"); + return; + } + + try { + const updatedMode = await setWalletMode(profileId, mode as WalletMode); + res.status(httpStatus.OK).json({ mode: updatedMode }); + } catch (error) { + if (error instanceof WalletModeConflictError) { + sendWalletModeConflict(res, error); + return; + } + logger.error("Failed to update wallet mode", error); + sendError(res, httpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "Failed to update wallet mode"); + } +} + +export async function createPrivyWallet(req: Request, res: Response): Promise { + const profileId = requireUserId(req, res); + if (!profileId) return; + + const { address, providerWalletId } = (req.body ?? {}) as { address?: unknown; providerWalletId?: unknown }; + if (typeof address !== "string" || typeof providerWalletId !== "string") { + sendError(res, httpStatus.BAD_REQUEST, "INVALID_WALLET", "address and providerWalletId are required"); + return; + } + + try { + const wallet = await sequelize.transaction(async transaction => { + const registered = await registerPrivyWallet(profileId, { address, providerWalletId }, transaction); + await setWalletMode(profileId, "privy_embedded", transaction); + return registered; + }); + res.status(httpStatus.OK).json({ mode: "privy_embedded", wallet: serializeWallet(wallet) }); + } catch (error) { + if (error instanceof WalletModeConflictError) { + sendWalletModeConflict(res, error); + return; + } + if (error instanceof WalletRegistrationConflictError || error instanceof UniqueConstraintError) { + sendError(res, httpStatus.CONFLICT, "WALLET_CONFLICT", error.message); + return; + } + if (error instanceof PrivyWalletVerificationError) { + const status = + error.kind === "disabled" || error.kind === "unavailable" ? httpStatus.SERVICE_UNAVAILABLE : httpStatus.FORBIDDEN; + sendError(res, status, "PRIVY_WALLET_NOT_VERIFIED", error.message); + return; + } + logger.error("Failed to register Privy wallet", error); + sendError(res, httpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "Failed to register embedded wallet"); + } +} diff --git a/apps/api/src/api/routes/v1/index.ts b/apps/api/src/api/routes/v1/index.ts index 781feeeba..14bc84451 100644 --- a/apps/api/src/api/routes/v1/index.ts +++ b/apps/api/src/api/routes/v1/index.ts @@ -31,6 +31,7 @@ import recipientsRoutes from "./recipients.route"; import sessionRoutes from "./session.route"; import siweRoutes from "./siwe.route"; import storageRoutes from "./storage.route"; +import walletsRoutes from "./wallets.route"; import webhookRoutes from "./webhook.route"; type ChainStatus = { @@ -214,6 +215,14 @@ router.use("/onboarding", onboardingRoutes); */ router.use("/api-keys", apiKeysRoutes); +/** + * Optional user wallet preference and verified embedded-wallet metadata. + * GET /v1/wallets + * PATCH /v1/wallets/mode + * POST /v1/wallets/privy + */ +router.use("/wallets", walletsRoutes); + /** * Admin routes for partner API key management * Uses partner name (not ID) to manage keys for all partner configurations diff --git a/apps/api/src/api/routes/v1/wallets.route.ts b/apps/api/src/api/routes/v1/wallets.route.ts new file mode 100644 index 000000000..e711c549e --- /dev/null +++ b/apps/api/src/api/routes/v1/wallets.route.ts @@ -0,0 +1,12 @@ +import { Request, Response, Router } from "express"; +import { createPrivyWallet, getWallets, updateWalletMode } from "../../controllers/wallets.controller"; +import { requireAuth } from "../../middlewares/supabaseAuth"; + +const router: Router = Router({ mergeParams: true }); + +router.use(requireAuth); +router.get("/", getWallets as unknown as (req: Request, res: Response) => void); +router.patch("/mode", updateWalletMode as unknown as (req: Request, res: Response) => void); +router.post("/privy", createPrivyWallet as unknown as (req: Request, res: Response) => void); + +export default router; diff --git a/apps/api/src/api/services/wallets/privyWallet.service.ts b/apps/api/src/api/services/wallets/privyWallet.service.ts new file mode 100644 index 000000000..36bdd09d9 --- /dev/null +++ b/apps/api/src/api/services/wallets/privyWallet.service.ts @@ -0,0 +1,85 @@ +import { getAddress, isAddress } from "viem"; +import { config } from "../../../config/vars"; + +interface PrivyLinkedAccount { + id?: string; + address?: string; + type?: string; + chain_type?: string; + wallet_client_type?: string; +} + +interface PrivyUserResponse { + id: string; + linked_accounts: PrivyLinkedAccount[]; +} + +export class PrivyWalletVerificationError extends Error { + constructor( + message: string, + readonly kind: "disabled" | "not_found" | "ownership_mismatch" | "unavailable" + ) { + super(message); + this.name = "PrivyWalletVerificationError"; + } +} + +function isEmbeddedEvmWallet(account: PrivyLinkedAccount): boolean { + return ( + account.type === "wallet" && + account.chain_type === "ethereum" && + (account.wallet_client_type === "privy" || account.wallet_client_type === "privy-v2") && + typeof account.address === "string" && + isAddress(account.address) + ); +} + +export async function verifyPrivyWalletOwnership(input: { + profileId: string; + providerWalletId: string; + address: string; + signal?: AbortSignal; +}): Promise<{ address: string; privyUserId: string }> { + if (!config.privy.walletRegistrationEnabled) { + throw new PrivyWalletVerificationError("Privy wallet registration is disabled", "disabled"); + } + + const authorization = Buffer.from(`${config.privy.appId}:${config.privy.appSecret}`).toString("base64"); + let response: Response; + try { + response = await fetch("https://api.privy.io/v1/users/custom_auth/id", { + body: JSON.stringify({ custom_user_id: input.profileId }), + headers: { + Authorization: `Basic ${authorization}`, + "Content-Type": "application/json", + "privy-app-id": config.privy.appId + }, + method: "POST", + signal: input.signal ?? AbortSignal.timeout(10000) + }); + } catch (error) { + throw new PrivyWalletVerificationError( + `Privy ownership verification failed: ${error instanceof Error ? error.message : String(error)}`, + "unavailable" + ); + } + + if (response.status === 404) { + throw new PrivyWalletVerificationError("Privy user was not found for this Vortex profile", "not_found"); + } + if (!response.ok) { + throw new PrivyWalletVerificationError(`Privy ownership verification returned ${response.status}`, "unavailable"); + } + + const user = (await response.json()) as PrivyUserResponse; + const requestedAddress = getAddress(input.address); + const wallet = user.linked_accounts + .filter(isEmbeddedEvmWallet) + .find(account => account.id === input.providerWalletId && getAddress(account.address as string) === requestedAddress); + + if (!wallet) { + throw new PrivyWalletVerificationError("The Privy wallet does not belong to this Vortex profile", "ownership_mismatch"); + } + + return { address: requestedAddress, privyUserId: user.id }; +} diff --git a/apps/api/src/api/services/wallets/profileWallet.service.ts b/apps/api/src/api/services/wallets/profileWallet.service.ts new file mode 100644 index 000000000..03e4360d0 --- /dev/null +++ b/apps/api/src/api/services/wallets/profileWallet.service.ts @@ -0,0 +1,129 @@ +import { Op, Transaction } from "sequelize"; +import { getAddress, isAddress } from "viem"; +import ProfileWallet from "../../../models/profileWallet.model"; +import RampState from "../../../models/rampState.model"; +import User from "../../../models/user.model"; +import { verifyPrivyWalletOwnership } from "./privyWallet.service"; + +export type WalletMode = "external" | "privy_embedded" | null; + +const TERMINAL_RAMP_PHASES = ["complete", "failed", "timedOut"]; + +export class WalletModeConflictError extends Error { + constructor( + message: string, + readonly kind: "active_ramp" | "missing_wallet" + ) { + super(message); + this.name = "WalletModeConflictError"; + } +} +export class WalletRegistrationConflictError extends Error {} + +export async function listProfileWallets(profileId: string): Promise<{ + mode: WalletMode; + wallets: ProfileWallet[]; +}> { + const [profile, wallets] = await Promise.all([ + User.findByPk(profileId, { attributes: ["walletMode"] }), + ProfileWallet.findAll({ + order: [["createdAt", "ASC"]], + where: { profileId, status: "active" } + }) + ]); + return { mode: profile?.walletMode ?? null, wallets }; +} + +export async function setWalletMode(profileId: string, mode: WalletMode, transaction?: Transaction): Promise { + const activeRamp = await RampState.findOne({ + attributes: ["id"], + transaction, + where: { + currentPhase: { [Op.notIn]: TERMINAL_RAMP_PHASES }, + userId: profileId + } + }); + if (activeRamp) { + throw new WalletModeConflictError("Wallet mode cannot change while a ramp is active", "active_ramp"); + } + + if (mode === "privy_embedded") { + const embeddedWallet = await ProfileWallet.findOne({ + attributes: ["id"], + transaction, + where: { + chainType: "ethereum", + profileId, + provider: "privy", + status: "active" + } + }); + if (!embeddedWallet) { + throw new WalletModeConflictError("An active verified Privy wallet is required for embedded mode", "missing_wallet"); + } + } + + const profile = await User.findByPk(profileId, { transaction }); + if (!profile) { + throw new Error("Profile not found"); + } + await profile.update({ walletMode: mode }, { transaction }); + return profile.walletMode; +} + +export async function registerPrivyWallet( + profileId: string, + input: { providerWalletId: string; address: string }, + transaction?: Transaction +): Promise { + if (!input.providerWalletId.trim() || !isAddress(input.address)) { + throw new WalletRegistrationConflictError("A valid Privy wallet ID and EVM address are required"); + } + + const verified = await verifyPrivyWalletOwnership({ + address: input.address, + profileId, + providerWalletId: input.providerWalletId + }); + + const conflictingWallet = await ProfileWallet.findOne({ + transaction, + where: { + [Op.or]: [ + { provider: "privy", providerWalletId: input.providerWalletId }, + { address: getAddress(verified.address), chainType: "ethereum" } + ] + } + }); + if (conflictingWallet && conflictingWallet.profileId !== profileId) { + throw new WalletRegistrationConflictError("This embedded wallet is already registered to another profile"); + } + + const existingForProfile = await ProfileWallet.findOne({ + transaction, + where: { chainType: "ethereum", profileId, provider: "privy", status: "active" } + }); + if (existingForProfile) { + if ( + existingForProfile.providerWalletId !== input.providerWalletId || + getAddress(existingForProfile.address) !== verified.address + ) { + throw new WalletRegistrationConflictError("This profile already has a different active Privy wallet"); + } + await existingForProfile.update({ lastUsedAt: new Date() }, { transaction }); + return existingForProfile; + } + + return ProfileWallet.create( + { + address: verified.address, + chainType: "ethereum", + lastUsedAt: new Date(), + profileId, + provider: "privy", + providerWalletId: input.providerWalletId, + status: "active" + }, + { transaction } + ); +} diff --git a/apps/api/src/config/vars.test.ts b/apps/api/src/config/vars.test.ts index 6c5fd710f..97801af07 100644 --- a/apps/api/src/config/vars.test.ts +++ b/apps/api/src/config/vars.test.ts @@ -108,4 +108,25 @@ describe("vars deployment environment validation", () => { expect(result.exitCode).toBe(1); expect(result.stderr).toContain("MONERIUM_CLIENT_ID"); }); + + it("requires both Privy server credentials when wallet registration is enabled", async () => { + const result = await importVarsWithEnv({ + NODE_ENV: "test", + PRIVY_WALLET_REGISTRATION_ENABLED: "true" + }); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("PRIVY_APP_ID and PRIVY_APP_SECRET"); + }); + + it("allows Privy wallet registration when both server credentials are present", async () => { + const result = await importVarsWithEnv({ + NODE_ENV: "test", + PRIVY_APP_ID: "test-privy-app", + PRIVY_APP_SECRET: "test-privy-secret", + PRIVY_WALLET_REGISTRATION_ENABLED: "true" + }); + + expect(result).toEqual({ exitCode: 0, stderr: "", stdout: "ok\n" }); + }); }); diff --git a/apps/api/src/config/vars.ts b/apps/api/src/config/vars.ts index 5b3886069..1b53d1358 100644 --- a/apps/api/src/config/vars.ts +++ b/apps/api/src/config/vars.ts @@ -192,6 +192,11 @@ interface Config { defaults: { vortexEvmPayoutAddress: string | undefined; }; + privy: { + appId: string; + appSecret: string; + walletRegistrationEnabled: boolean; + }; } export const config: Config = { @@ -265,6 +270,11 @@ export const config: Config = { partnerApiKey: process.env.TRANSAK_API_KEY } }, + privy: { + appId: process.env.PRIVY_APP_ID || "", + appSecret: process.env.PRIVY_APP_SECRET || "", + walletRegistrationEnabled: process.env.PRIVY_WALLET_REGISTRATION_ENABLED === "true" + }, quote: { deltaDBasisPoints: parseFloat(process.env.DELTA_D_BASIS_POINTS || "0.3"), discountStateTimeoutMinutes: parseInt(process.env.DISCOUNT_STATE_TIMEOUT_MINUTES || "10", 10) @@ -318,6 +328,10 @@ if (config.deploymentEnv === "sandbox" && !config.sandboxEnabled) { throw new Error("DEPLOYMENT_ENV=sandbox requires SANDBOX_ENABLED=true"); } +if (config.privy.walletRegistrationEnabled && (!config.privy.appId || !config.privy.appSecret)) { + throw new Error("PRIVY_APP_ID and PRIVY_APP_SECRET are required when PRIVY_WALLET_REGISTRATION_ENABLED=true"); +} + if (config.env === "production") { const missing: string[] = []; diff --git a/apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts b/apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts new file mode 100644 index 000000000..e976469e5 --- /dev/null +++ b/apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts @@ -0,0 +1,21 @@ +import { DataTypes, Op, QueryInterface } from "sequelize"; + +export async function up(queryInterface: QueryInterface): Promise { + await queryInterface.addColumn("profiles", "wallet_mode", { + allowNull: true, + type: DataTypes.STRING(32) + }); + await queryInterface.addConstraint("profiles", { + fields: ["wallet_mode"], + name: "profiles_wallet_mode_check", + type: "check", + where: { + wallet_mode: { [Op.in]: ["external", "privy_embedded"] } + } + }); +} + +export async function down(queryInterface: QueryInterface): Promise { + await queryInterface.removeConstraint("profiles", "profiles_wallet_mode_check"); + await queryInterface.removeColumn("profiles", "wallet_mode"); +} diff --git a/apps/api/src/database/migrations/056-create-profile-wallets.ts b/apps/api/src/database/migrations/056-create-profile-wallets.ts new file mode 100644 index 000000000..ca346c976 --- /dev/null +++ b/apps/api/src/database/migrations/056-create-profile-wallets.ts @@ -0,0 +1,102 @@ +import { DataTypes, Op, QueryInterface } from "sequelize"; + +export async function up(queryInterface: QueryInterface): Promise { + await queryInterface.createTable("profile_wallets", { + address: { + allowNull: false, + type: DataTypes.STRING(42) + }, + chain_type: { + allowNull: false, + defaultValue: "ethereum", + type: DataTypes.STRING(32) + }, + created_at: { + allowNull: false, + defaultValue: DataTypes.NOW, + type: DataTypes.DATE + }, + id: { + allowNull: false, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + type: DataTypes.UUID + }, + last_used_at: { + allowNull: false, + defaultValue: DataTypes.NOW, + type: DataTypes.DATE + }, + profile_id: { + allowNull: false, + onDelete: "CASCADE", + onUpdate: "CASCADE", + references: { + key: "id", + model: "profiles" + }, + type: DataTypes.UUID + }, + provider: { + allowNull: false, + defaultValue: "privy", + type: DataTypes.STRING(32) + }, + provider_wallet_id: { + allowNull: false, + type: DataTypes.STRING(255) + }, + status: { + allowNull: false, + defaultValue: "active", + type: DataTypes.STRING(32) + }, + updated_at: { + allowNull: false, + defaultValue: DataTypes.NOW, + type: DataTypes.DATE + } + }); + + await queryInterface.addConstraint("profile_wallets", { + fields: ["provider"], + name: "profile_wallets_provider_check", + type: "check", + where: { provider: { [Op.in]: ["privy"] } } + }); + await queryInterface.addConstraint("profile_wallets", { + fields: ["chain_type"], + name: "profile_wallets_chain_type_check", + type: "check", + where: { chain_type: { [Op.in]: ["ethereum"] } } + }); + await queryInterface.addConstraint("profile_wallets", { + fields: ["status"], + name: "profile_wallets_status_check", + type: "check", + where: { status: { [Op.in]: ["active", "archived"] } } + }); + await queryInterface.addConstraint("profile_wallets", { + fields: ["provider", "provider_wallet_id"], + name: "uniq_profile_wallets_provider_wallet", + type: "unique" + }); + await queryInterface.addIndex("profile_wallets", ["profile_id", "provider", "chain_type"], { + name: "idx_profile_wallets_profile_provider_chain" + }); + await queryInterface.sequelize.query(` + CREATE UNIQUE INDEX uniq_profile_wallets_active_provider_chain + ON profile_wallets (profile_id, provider, chain_type) + WHERE status = 'active'; + `); + await queryInterface.sequelize.query(` + CREATE UNIQUE INDEX uniq_profile_wallets_evm_address + ON profile_wallets (chain_type, LOWER(address)); + `); +} + +export async function down(queryInterface: QueryInterface): Promise { + await queryInterface.sequelize.query("DROP INDEX IF EXISTS uniq_profile_wallets_evm_address;"); + await queryInterface.sequelize.query("DROP INDEX IF EXISTS uniq_profile_wallets_active_provider_chain;"); + await queryInterface.dropTable("profile_wallets"); +} diff --git a/apps/api/src/models/index.ts b/apps/api/src/models/index.ts index fe15590d8..df2bf40f6 100644 --- a/apps/api/src/models/index.ts +++ b/apps/api/src/models/index.ts @@ -11,6 +11,7 @@ import Partner from "./partner.model"; import PartnerPricingConfig from "./partnerPricingConfig.model"; import ProfilePartnerAssignment from "./profilePartnerAssignment.model"; import ProfileRole from "./profileRole.model"; +import ProfileWallet from "./profileWallet.model"; import ProviderCustomer from "./providerCustomer.model"; import QuoteTicket from "./quoteTicket.model"; import RampState from "./rampState.model"; @@ -47,6 +48,8 @@ ProfilePartnerAssignment.belongsTo(User, { as: "user", foreignKey: "userId" }); User.hasMany(ProfileRole, { as: "roles", foreignKey: "userId" }); ProfileRole.belongsTo(User, { as: "user", foreignKey: "userId" }); +User.hasMany(ProfileWallet, { as: "wallets", foreignKey: "profileId" }); +ProfileWallet.belongsTo(User, { as: "profile", foreignKey: "profileId" }); ProfilePartnerAssignment.belongsTo(Partner, { as: "buyPartner", foreignKey: "buyPartnerId" }); ProfilePartnerAssignment.belongsTo(Partner, { as: "sellPartner", foreignKey: "sellPartnerId" }); Partner.hasMany(ProfilePartnerAssignment, { as: "buyProfileAssignments", foreignKey: "buyPartnerId" }); @@ -110,6 +113,7 @@ const models = { PartnerPricingConfig, ProfilePartnerAssignment, ProfileRole, + ProfileWallet, ProviderCustomer, QuoteTicket, RampState, diff --git a/apps/api/src/models/profileWallet.model.ts b/apps/api/src/models/profileWallet.model.ts new file mode 100644 index 000000000..0a195509d --- /dev/null +++ b/apps/api/src/models/profileWallet.model.ts @@ -0,0 +1,116 @@ +import { DataTypes, Model, Optional } from "sequelize"; +import sequelize from "../config/database"; + +export type ProfileWalletProvider = "privy"; +export type ProfileWalletChainType = "ethereum"; +export type ProfileWalletStatus = "active" | "archived"; + +export interface ProfileWalletAttributes { + id: string; + profileId: string; + provider: ProfileWalletProvider; + providerWalletId: string; + address: string; + chainType: ProfileWalletChainType; + status: ProfileWalletStatus; + lastUsedAt: Date; + createdAt: Date; + updatedAt: Date; +} + +type ProfileWalletCreationAttributes = Optional< + ProfileWalletAttributes, + "id" | "provider" | "chainType" | "status" | "lastUsedAt" | "createdAt" | "updatedAt" +>; + +class ProfileWallet extends Model implements ProfileWalletAttributes { + declare id: string; + declare profileId: string; + declare provider: ProfileWalletProvider; + declare providerWalletId: string; + declare address: string; + declare chainType: ProfileWalletChainType; + declare status: ProfileWalletStatus; + declare lastUsedAt: Date; + declare createdAt: Date; + declare updatedAt: Date; +} + +ProfileWallet.init( + { + address: { + allowNull: false, + type: DataTypes.STRING(42) + }, + chainType: { + allowNull: false, + defaultValue: "ethereum", + field: "chain_type", + type: DataTypes.STRING(32) + }, + createdAt: { + allowNull: false, + defaultValue: DataTypes.NOW, + field: "created_at", + type: DataTypes.DATE + }, + id: { + allowNull: false, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + type: DataTypes.UUID + }, + lastUsedAt: { + allowNull: false, + defaultValue: DataTypes.NOW, + field: "last_used_at", + type: DataTypes.DATE + }, + profileId: { + allowNull: false, + field: "profile_id", + onDelete: "CASCADE", + onUpdate: "CASCADE", + references: { + key: "id", + model: "profiles" + }, + type: DataTypes.UUID + }, + provider: { + allowNull: false, + defaultValue: "privy", + type: DataTypes.STRING(32) + }, + providerWalletId: { + allowNull: false, + field: "provider_wallet_id", + type: DataTypes.STRING(255) + }, + status: { + allowNull: false, + defaultValue: "active", + type: DataTypes.STRING(32) + }, + updatedAt: { + allowNull: false, + defaultValue: DataTypes.NOW, + field: "updated_at", + type: DataTypes.DATE + } + }, + { + indexes: [ + { + fields: ["profile_id", "provider", "chain_type"], + name: "idx_profile_wallets_profile_provider_chain" + } + ], + modelName: "ProfileWallet", + sequelize, + tableName: "profile_wallets", + timestamps: true + } +); + +export default ProfileWallet; diff --git a/apps/api/src/models/user.model.ts b/apps/api/src/models/user.model.ts index eeb0e2e90..19cd04363 100644 --- a/apps/api/src/models/user.model.ts +++ b/apps/api/src/models/user.model.ts @@ -5,16 +5,18 @@ export interface UserAttributes { id: string; // UUID from Supabase Auth email: string; activeCustomerEntityId: string | null; + walletMode: "external" | "privy_embedded" | null; createdAt: Date; updatedAt: Date; } -type UserCreationAttributes = Optional; +type UserCreationAttributes = Optional; class User extends Model implements UserAttributes { declare id: string; declare email: string; declare activeCustomerEntityId: string | null; + declare walletMode: "external" | "privy_embedded" | null; declare createdAt: Date; declare updatedAt: Date; } @@ -54,6 +56,11 @@ User.init( defaultValue: DataTypes.NOW, field: "updated_at", type: DataTypes.DATE + }, + walletMode: { + allowNull: true, + field: "wallet_mode", + type: DataTypes.STRING(32) } }, { diff --git a/apps/api/src/tests/wallets.integration.test.ts b/apps/api/src/tests/wallets.integration.test.ts new file mode 100644 index 000000000..05b4b3443 --- /dev/null +++ b/apps/api/src/tests/wallets.integration.test.ts @@ -0,0 +1,205 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import { config } from "../config/vars"; +import ProfileWallet from "../models/profileWallet.model"; +import { resetTestDatabase, setupTestDatabase } from "../test-utils/db"; +import { createTestRampState, createTestUser } from "../test-utils/factories"; +import { type FakeSupabaseAuth, installFakeSupabaseAuth, testUserToken } from "../test-utils/fake-world/fake-auth"; +import { startTestApp, type TestApp } from "../test-utils/test-app"; + +const WALLET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; +const WALLET_ID = "wallet_privy_test_1"; + +let api: TestApp; +let fakeAuth: FakeSupabaseAuth; +const guardedFetch = globalThis.fetch; +const originalPrivyConfig = { ...config.privy }; + +function headers(token: string): Record { + return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; +} + +function installPrivyResponse(address = WALLET_ADDRESS, walletId = WALLET_ID): void { + globalThis.fetch = (async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + if (url === "https://api.privy.io/v1/users/custom_auth/id") { + const body = JSON.parse(String(init?.body)) as { custom_user_id: string }; + return Response.json({ + id: `privy-user-${body.custom_user_id}`, + linked_accounts: [ + { + address, + chain_type: "ethereum", + id: walletId, + type: "wallet", + wallet_client_type: "privy" + } + ] + }); + } + return guardedFetch(input, init); + }) as typeof globalThis.fetch; +} + +beforeAll(async () => { + await setupTestDatabase(); + fakeAuth = installFakeSupabaseAuth(); + api = await startTestApp(); +}); + +afterAll(async () => { + globalThis.fetch = guardedFetch; + Object.assign(config.privy, originalPrivyConfig); + if (api) await api.close(); + if (fakeAuth) fakeAuth.restore(); +}); + +beforeEach(async () => { + await resetTestDatabase(); + Object.assign(config.privy, { + appId: "test-privy-app", + appSecret: "test-privy-secret", + walletRegistrationEnabled: true + }); + installPrivyResponse(); +}); + +describe("wallet API", () => { + it("requires Supabase authentication", async () => { + const response = await api.request("/v1/wallets"); + expect(response.status).toBe(401); + }); + + it("lists only the authenticated profile's wallet metadata", async () => { + const first = await createTestUser({ email: "wallet-first@example.com" }); + const second = await createTestUser({ email: "wallet-second@example.com" }); + await ProfileWallet.create({ + address: WALLET_ADDRESS, + profileId: first.id, + providerWalletId: WALLET_ID + }); + await ProfileWallet.create({ + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + profileId: second.id, + providerWalletId: "wallet_privy_test_2" + }); + + const response = await api.request("/v1/wallets", { + headers: headers(testUserToken(first.id, first.email)) + }); + expect(response.status).toBe(200); + const body = (await response.json()) as { mode: null; wallets: Array<{ providerWalletId: string }> }; + expect(body.mode).toBeNull(); + expect(body.wallets.map(wallet => wallet.providerWalletId)).toEqual([WALLET_ID]); + }); + + it("rejects invalid modes and mode changes during a nonterminal ramp", async () => { + const user = await createTestUser({ email: "wallet-mode@example.com" }); + const token = testUserToken(user.id, user.email); + const invalid = await api.request("/v1/wallets/mode", { + body: JSON.stringify({ mode: "automatic" }), + headers: headers(token), + method: "PATCH" + }); + expect(invalid.status).toBe(400); + + const unverifiedEmbedded = await api.request("/v1/wallets/mode", { + body: JSON.stringify({ mode: "privy_embedded" }), + headers: headers(token), + method: "PATCH" + }); + expect(unverifiedEmbedded.status).toBe(409); + expect(((await unverifiedEmbedded.json()) as { error: { code: string } }).error.code).toBe( + "WALLET_NOT_REGISTERED" + ); + + await createTestRampState({ userId: user.id }); + const conflict = await api.request("/v1/wallets/mode", { + body: JSON.stringify({ mode: "external" }), + headers: headers(token), + method: "PATCH" + }); + expect(conflict.status).toBe(409); + expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe("ACTIVE_RAMP"); + }); + + it("verifies and idempotently registers a Privy wallet", async () => { + const user = await createTestUser({ email: "wallet-register@example.com" }); + const token = testUserToken(user.id, user.email); + const request = () => + api.request("/v1/wallets/privy", { + body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + headers: headers(token), + method: "POST" + }); + + const first = await request(); + const second = await request(); + expect(first.status).toBe(200); + expect(second.status).toBe(200); + expect(await ProfileWallet.count({ where: { profileId: user.id } })).toBe(1); + await user.reload(); + expect(user.walletMode).toBe("privy_embedded"); + }); + + it("rejects a wallet already registered to another profile", async () => { + const first = await createTestUser({ email: "wallet-owner@example.com" }); + const second = await createTestUser({ email: "wallet-stranger@example.com" }); + const register = (user: typeof first) => + api.request("/v1/wallets/privy", { + body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + headers: headers(testUserToken(user.id, user.email)), + method: "POST" + }); + + expect((await register(first)).status).toBe(200); + const conflict = await register(second); + expect(conflict.status).toBe(409); + expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe("WALLET_CONFLICT"); + }); + + it("rejects mismatched Privy ownership without persisting metadata", async () => { + const user = await createTestUser({ email: "wallet-mismatch@example.com" }); + installPrivyResponse("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "a-different-wallet"); + + const response = await api.request("/v1/wallets/privy", { + body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + headers: headers(testUserToken(user.id, user.email)), + method: "POST" + }); + + expect(response.status).toBe(403); + expect(((await response.json()) as { error: { code: string } }).error.code).toBe( + "PRIVY_WALLET_NOT_VERIFIED" + ); + expect(await ProfileWallet.count()).toBe(0); + }); + + it("atomically rolls back registration when a ramp is active", async () => { + const user = await createTestUser({ email: "wallet-active-ramp@example.com" }); + await createTestRampState({ userId: user.id }); + + const response = await api.request("/v1/wallets/privy", { + body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + headers: headers(testUserToken(user.id, user.email)), + method: "POST" + }); + + expect(response.status).toBe(409); + expect(((await response.json()) as { error: { code: string } }).error.code).toBe("ACTIVE_RAMP"); + expect(await ProfileWallet.count()).toBe(0); + await user.reload(); + expect(user.walletMode).toBeNull(); + }); + + it("fails closed when server-side Privy ownership verification is disabled", async () => { + const user = await createTestUser({ email: "wallet-disabled@example.com" }); + config.privy.walletRegistrationEnabled = false; + const response = await api.request("/v1/wallets/privy", { + body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + headers: headers(testUserToken(user.id, user.email)), + method: "POST" + }); + expect(response.status).toBe(503); + expect(await ProfileWallet.count()).toBe(0); + }); +}); From ef0f619ad18ab5e35cef67cb4115d8ecad1d2721 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 27 Jul 2026 19:10:20 +0200 Subject: [PATCH 04/16] feat(dashboard): add optional Privy embedded wallets --- apps/dashboard/.env.example | 13 + apps/dashboard/e2e/support/mockBackend.ts | 15 +- apps/dashboard/e2e/wallet-optionality.spec.ts | 15 ++ .../dashboard/e2e/wallet-privy-choice.spec.ts | 24 ++ apps/dashboard/playwright.privy.config.ts | 29 +++ .../components/layout/ConnectWalletButton.tsx | 93 ++++++- .../components/transfer/FundingMethods.tsx | 44 +++- .../src/components/transfer/OnrampForm.tsx | 5 +- apps/dashboard/src/main.tsx | 5 +- apps/dashboard/src/routes/_app/settings.tsx | 81 +++++- .../dashboard/src/services/api/wallets.api.ts | 26 ++ apps/dashboard/src/services/auth.test.ts | 46 ++++ apps/dashboard/src/services/auth.ts | 14 ++ .../src/services/transactions/userSigning.ts | 55 ++--- .../src/wallets/PrivyWalletRuntime.tsx | 231 ++++++++++++++++++ .../src/wallets/WalletExperienceContext.ts | 29 +++ .../src/wallets/WalletExperienceProvider.tsx | 106 ++++++++ apps/dashboard/src/wallets/config.test.ts | 45 ++++ apps/dashboard/src/wallets/config.ts | 34 +++ .../src/wallets/externalSigningAdapter.ts | 61 +++++ apps/dashboard/src/wallets/signingAdapter.ts | 31 +++ .../wallets/walletSigning.contract.test.ts | 111 +++++++++ 22 files changed, 1045 insertions(+), 68 deletions(-) create mode 100644 apps/dashboard/e2e/wallet-optionality.spec.ts create mode 100644 apps/dashboard/e2e/wallet-privy-choice.spec.ts create mode 100644 apps/dashboard/playwright.privy.config.ts create mode 100644 apps/dashboard/src/services/api/wallets.api.ts create mode 100644 apps/dashboard/src/services/auth.test.ts create mode 100644 apps/dashboard/src/wallets/PrivyWalletRuntime.tsx create mode 100644 apps/dashboard/src/wallets/WalletExperienceContext.ts create mode 100644 apps/dashboard/src/wallets/WalletExperienceProvider.tsx create mode 100644 apps/dashboard/src/wallets/config.test.ts create mode 100644 apps/dashboard/src/wallets/config.ts create mode 100644 apps/dashboard/src/wallets/externalSigningAdapter.ts create mode 100644 apps/dashboard/src/wallets/signingAdapter.ts create mode 100644 apps/dashboard/src/wallets/walletSigning.contract.test.ts diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index 3f24555fc..15a936c55 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -9,3 +9,16 @@ VITE_ALCHEMY_API_KEY= # http://127.0.0.1:5173; production builds fall back to the page origin. Set this only # when the widget is not served on the same origin as the dashboard. VITE_WIDGET_URL= + +# Optional Privy embedded-wallet path. Existing-wallet users stay on Reown and +# Privy is not initialized unless they explicitly choose an embedded wallet. +VITE_PRIVY_ENABLED=false +VITE_PRIVY_APP_ID= +VITE_PRIVY_CLIENT_ID= +VITE_PRIVY_PROVISIONING_ENABLED=false +VITE_PRIVY_ONRAMP_ENABLED=false +VITE_PRIVY_OFFRAMP_ENABLED=false + +# user_pays is the safe default. Set to sponsored only after Privy gas sponsorship +# policies and Vortex transaction allowlists have been configured and reviewed. +VITE_PRIVY_GAS_POLICY=user_pays diff --git a/apps/dashboard/e2e/support/mockBackend.ts b/apps/dashboard/e2e/support/mockBackend.ts index 593a4e092..db81858b9 100644 --- a/apps/dashboard/e2e/support/mockBackend.ts +++ b/apps/dashboard/e2e/support/mockBackend.ts @@ -251,6 +251,7 @@ export function buildSellUnsignedTxs(evmEphemeral: string) { } interface MockBackendOptions { + appOrigin?: string; onboardingState?: OnboardingState; companyMode?: boolean; selectionRequired?: boolean; @@ -384,6 +385,7 @@ function answerRpc(chainIdHex: string) { * changed default RPC URL fails the suite instead of silently reaching the network. */ export async function mockBackend(page: Page, options: MockBackendOptions = {}) { + const appOrigin = options.appOrigin ?? APP_ORIGIN; const requestOtpRequests: Array> = []; const verifyOtpRequests: Array> = []; const quoteRequests: Array> = []; @@ -437,7 +439,7 @@ export async function mockBackend(page: Page, options: MockBackendOptions = {}) // handlers in reverse registration order. await page.route("**/*", async route => { const url = route.request().url(); - if (url.startsWith(APP_ORIGIN) || url.startsWith("data:") || url.startsWith("blob:")) { + if (url.startsWith(appOrigin) || url.startsWith("data:") || url.startsWith("blob:")) { await route.continue(); return; } @@ -480,6 +482,15 @@ export async function mockBackend(page: Page, options: MockBackendOptions = {}) await fulfillJson({ access_token: "e2e-access-token", refresh_token: "e2e-refresh-token", success: true }); return; } + if (path === "/v1/wallets" && method === "GET") { + await fulfillJson({ mode: null, wallets: [] }); + return; + } + if (path === "/v1/wallets/mode" && method === "PATCH") { + const body = request.postDataJSON() as { mode: string | null }; + await fulfillJson({ mode: body.mode }); + return; + } if (path === "/v1/onboarding/active-entity" && method === "PUT") { if (options.selectActiveEntityError) { @@ -577,7 +588,7 @@ export async function mockBackend(page: Page, options: MockBackendOptions = {}) if (path === "/v1/monerium/oauth/start" && method === "POST" && options.moneriumKyc) { monerium.startRequests.push(request.postDataJSON() as Record); await fulfillJson({ - authorizationUrl: `${APP_ORIGIN}/monerium/callback?code=e2e-code&state=e2e-state` + authorizationUrl: `${appOrigin}/monerium/callback?code=e2e-code&state=e2e-state` }); return; } diff --git a/apps/dashboard/e2e/wallet-optionality.spec.ts b/apps/dashboard/e2e/wallet-optionality.spec.ts new file mode 100644 index 000000000..1f8efc853 --- /dev/null +++ b/apps/dashboard/e2e/wallet-optionality.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from "@playwright/test"; +import { mockBackend } from "./support/mockBackend"; +import { seedSession } from "./support/session"; + +test("existing-wallet users keep the external connect path when Privy flags are off", async ({ page }) => { + const backend = await mockBackend(page); + await seedSession(page); + + await page.goto("/overview"); + + await expect(page.getByRole("button", { name: "Connect wallet" })).toBeVisible({ timeout: 20_000 }); + await expect(page.getByText("Create an embedded wallet")).toHaveCount(0); + expect(backend.unmatchedRequests).toEqual([]); + expect(backend.unexpectedExternalRequests).toEqual([]); +}); diff --git a/apps/dashboard/e2e/wallet-privy-choice.spec.ts b/apps/dashboard/e2e/wallet-privy-choice.spec.ts new file mode 100644 index 000000000..1b77e7c7b --- /dev/null +++ b/apps/dashboard/e2e/wallet-privy-choice.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from "@playwright/test"; +import { mockBackend } from "./support/mockBackend"; +import { seedSession } from "./support/session"; + +test("Privy enablement adds an optional choice without removing external wallets", async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== "privy-choice", "Runs with the opt-in Privy Playwright configuration"); + + const backend = await mockBackend(page, { appOrigin: "http://127.0.0.1:5175" }); + await seedSession(page); + + await page.goto("/overview"); + await page.getByRole("button", { name: "Choose wallet" }).click(); + + await expect(page.getByRole("heading", { name: "Choose how to use a wallet" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Connect an existing wallet" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Create an embedded wallet" })).toBeVisible(); + await expect(page.getByText("Embedded wallets are optional.")).toBeVisible(); + + await page.getByRole("button", { name: "Connect an existing wallet" }).click(); + await expect(page.getByText("Connect Wallet", { exact: true })).toBeVisible(); + + expect(backend.unmatchedRequests).toEqual([]); + expect(backend.unexpectedExternalRequests).toEqual([]); +}); diff --git a/apps/dashboard/playwright.privy.config.ts b/apps/dashboard/playwright.privy.config.ts new file mode 100644 index 000000000..3bad4f110 --- /dev/null +++ b/apps/dashboard/playwright.privy.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + forbidOnly: !!process.env.CI, + projects: [{ name: "privy-choice", use: { ...devices["Desktop Chrome"] } }], + reporter: process.env.CI ? [["list"], ["github"]] : [["list"]], + retries: process.env.CI ? 2 : 0, + testDir: "./e2e", + testMatch: "wallet-privy-choice.spec.ts", + timeout: 60_000, + use: { + baseURL: "http://127.0.0.1:5175", + trace: "on-first-retry" + }, + webServer: { + command: "bun x --bun vite --port 5175 --strictPort --host 127.0.0.1", + env: { + VITE_ALCHEMY_API_KEY: "e2e-mock-key", + VITE_PRIVY_APP_ID: "e2e-public-app-id", + VITE_PRIVY_ENABLED: "true", + VITE_PRIVY_OFFRAMP_ENABLED: "true", + VITE_PRIVY_ONRAMP_ENABLED: "true", + VITE_PRIVY_PROVISIONING_ENABLED: "true" + }, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + url: "http://127.0.0.1:5175/" + } +}); diff --git a/apps/dashboard/src/components/layout/ConnectWalletButton.tsx b/apps/dashboard/src/components/layout/ConnectWalletButton.tsx index 81fc970dd..0cf37b201 100644 --- a/apps/dashboard/src/components/layout/ConnectWalletButton.tsx +++ b/apps/dashboard/src/components/layout/ConnectWalletButton.tsx @@ -1,26 +1,90 @@ import { useAppKit, useAppKitAccount } from "@reown/appkit/react"; -import { Wallet } from "lucide-react"; +import { KeyRound, Loader2, Wallet } from "lucide-react"; +import { useState } from "react"; import { useAccount } from "wagmi"; import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { shortenAddress } from "@/domain/transfer"; import { wagmiConfig } from "@/lib/wagmi"; +import { useWalletExperience } from "@/wallets/WalletExperienceContext"; export function ConnectWalletButton() { - const { address, chainId } = useAccount(); - const { isConnected } = useAppKitAccount(); + const { chainId } = useAccount(); + const { isConnected: isExternalConnected } = useAppKitAccount(); const { open } = useAppKit(); + const wallet = useWalletExperience(); + const [chooserOpen, setChooserOpen] = useState(false); const isOnSupportedNetwork = wagmiConfig.chains.some(chain => chain.id === chainId); - if (!isConnected) { + if (!wallet.connected) { return ( - + <> + + + + + Choose how to use a wallet + + Use a wallet you already manage, or let Vortex create an embedded wallet for this account. + + +
+ + +

+ Embedded wallets are optional. Choosing one does not give Vortex access to sign without your approval. +

+
+
+
+ ); } - if (!isOnSupportedNetwork) { + if (wallet.mode !== "privy_embedded" && isExternalConnected && !isOnSupportedNetwork) { // AppKit reports the wallet's current (unsupported) network as caipNetwork here, so // switchNetwork(caipNetwork) would be a no-op — let the user pick a supported one. return ( @@ -31,9 +95,16 @@ export function ConnectWalletButton() { } return ( - ); } diff --git a/apps/dashboard/src/components/transfer/FundingMethods.tsx b/apps/dashboard/src/components/transfer/FundingMethods.tsx index 3a31bac7a..e920c7677 100644 --- a/apps/dashboard/src/components/transfer/FundingMethods.tsx +++ b/apps/dashboard/src/components/transfer/FundingMethods.tsx @@ -1,12 +1,11 @@ -import { useAppKit, useAppKitAccount } from "@reown/appkit/react"; import type { QuoteResponse } from "@vortexfi/shared"; -import { Check, Loader2, TriangleAlert, Wallet } from "lucide-react"; -import { useAccount } from "wagmi"; +import { Check, KeyRound, Loader2, TriangleAlert, Wallet } from "lucide-react"; import { Button } from "@/components/ui/button"; import type { RampTokenOption } from "@/domain/onramp"; import { shortenAddress } from "@/domain/transfer"; import { useTokenPortfolio } from "@/hooks/useTokenPortfolio"; import { getTokenBalance, hasSufficientTokenBalance } from "@/services/balance.service"; +import { useWalletExperience } from "@/wallets/WalletExperienceContext"; export type FundingSource = "wallet"; @@ -29,23 +28,41 @@ interface FundingMethodsProps { * crypto deposits are not supported. */ export function FundingMethods({ disabled, quote, submitting, token, onSubmit }: FundingMethodsProps) { - const { address } = useAccount(); - const { isConnected } = useAppKitAccount(); - const { open } = useAppKit(); + const wallet = useWalletExperience(); + const address = wallet.address; const portfolioQuery = useTokenPortfolio(address, token.network); const balance = portfolioQuery.data ? getTokenBalance(portfolioQuery.data, token.token) : undefined; const hasEnoughBalance = balance ? hasSufficientTokenBalance(balance, quote.inputAmount) : false; const checkingBalance = portfolioQuery.isPending || portfolioQuery.isFetching; - if (!isConnected || !address) { + if (wallet.mode === "privy_embedded" && !wallet.canSignOfframp) { + return ( +
+

Embedded-wallet payouts are not enabled in this environment.

+ +
+ ); + } + + if (!wallet.connected || !address) { return (
-

Connect your wallet.

- + {wallet.canUseEmbeddedWallet && ( + + )} + {wallet.error &&

{wallet.error}

}
); @@ -95,7 +112,12 @@ export function FundingMethods({ disabled, quote, submitting, token, onSubmit }: disabled={disabled || checkingBalance || !!portfolioQuery.error || !hasEnoughBalance} onClick={() => { if (hasEnoughBalance) { - onSubmit({ destAddress: address, label: "Connected wallet", source: "wallet" }); + wallet.activateSigner(); + onSubmit({ + destAddress: address, + label: wallet.mode === "privy_embedded" ? "Embedded wallet" : "Connected wallet", + source: "wallet" + }); } }} type="button" diff --git a/apps/dashboard/src/components/transfer/OnrampForm.tsx b/apps/dashboard/src/components/transfer/OnrampForm.tsx index 810640539..dcb385084 100644 --- a/apps/dashboard/src/components/transfer/OnrampForm.tsx +++ b/apps/dashboard/src/components/transfer/OnrampForm.tsx @@ -5,7 +5,6 @@ import { Lock, TriangleAlert } from "lucide-react"; import { useEffect, useSyncExternalStore } from "react"; import { useForm } from "react-hook-form"; import { isAddress } from "viem"; -import { useAccount } from "wagmi"; import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; @@ -18,6 +17,7 @@ import type { CorridorId, SenderAccount } from "@/domain/types"; import { useApprovedCorridors } from "@/hooks/useApprovedCorridors"; import { transferActor } from "@/machines/transferActor"; import { useQuote } from "@/services/api/hooks"; +import { useWalletExperience } from "@/wallets/WalletExperienceContext"; import { OnrampPaymentInstructions } from "./OnrampPaymentInstructions"; import { QuoteSummary } from "./QuoteSummary"; import { TokenCombobox } from "./TokenCombobox"; @@ -46,7 +46,8 @@ interface OnrampPrefill { } export function OnrampForm({ account, prefill }: { account: SenderAccount; prefill?: OnrampPrefill }) { - const { address } = useAccount(); + const wallet = useWalletExperience(); + const address = wallet.mode === "privy_embedded" && !wallet.canUseAsOnrampDestination ? undefined : wallet.address; const { approved, isLoading: isLoadingApprovals } = useApprovedCorridors(); useSyncExternalStore(subscribeEvmTokensLoaded, getEvmTokensLoadedSnapshot, () => false); const tokenOptions = getRampTokenOptions(RampDirection.BUY); diff --git a/apps/dashboard/src/main.tsx b/apps/dashboard/src/main.tsx index 91b1afe3f..32f451897 100644 --- a/apps/dashboard/src/main.tsx +++ b/apps/dashboard/src/main.tsx @@ -7,6 +7,7 @@ import "@/App.css"; import { queryClient } from "@/lib/queryClient"; import { wagmiConfig } from "@/lib/wagmi"; import { getRouter } from "@/router"; +import { WalletExperienceProvider } from "@/wallets/WalletExperienceProvider"; const router = getRouter(); @@ -21,7 +22,9 @@ if (!root) { createRoot(root).render( - + + + ); diff --git a/apps/dashboard/src/routes/_app/settings.tsx b/apps/dashboard/src/routes/_app/settings.tsx index cc48b6e90..b83fcec12 100644 --- a/apps/dashboard/src/routes/_app/settings.tsx +++ b/apps/dashboard/src/routes/_app/settings.tsx @@ -1,13 +1,16 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Building2, User } from "lucide-react"; +import { Building2, Copy, KeyRound, User, Wallet } from "lucide-react"; +import { QRCodeSVG } from "qrcode.react"; import { Stagger, StaggerItem } from "@/components/motion/Stagger"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useActiveAccount } from "@/hooks/useActiveAccount"; import { useAuthStore } from "@/stores/auth.store"; +import { useWalletExperience } from "@/wallets/WalletExperienceContext"; const NOTIFICATION_PREFS = [ { @@ -37,6 +40,7 @@ export const Route = createFileRoute("/_app/settings")({ function SettingsPage() { const user = useAuthStore(state => state.user); const account = useActiveAccount(); + const wallet = useWalletExperience(); return ( @@ -45,6 +49,81 @@ function SettingsPage() {

Your profile and linked sender accounts.

+ + + + Wallet + An embedded wallet is optional. You can keep using a wallet you already control. + + +
+ + {wallet.mode === "privy_embedded" ? : } + +
+ + {wallet.mode === "privy_embedded" ? "Vortex embedded wallet" : "Existing wallet"} + + + {wallet.address ?? (wallet.connected ? "Connected" : "Not connected")} + +
+ {wallet.mode === "privy_embedded" ? "Embedded" : "External"} +
+ {wallet.mode === "privy_embedded" && wallet.address && ( +
+
+ +
+

+ Scan to receive supported EVM assets at this address. +

+
+ )} +
+ {wallet.mode === "privy_embedded" ? ( + <> + {wallet.address && ( + <> + + + + )} + + + ) : ( + <> + + {wallet.canUseEmbeddedWallet && ( + + )} + + )} +
+ {wallet.error &&

{wallet.error}

} +
+
+
+ diff --git a/apps/dashboard/src/services/api/wallets.api.ts b/apps/dashboard/src/services/api/wallets.api.ts new file mode 100644 index 000000000..71d32502d --- /dev/null +++ b/apps/dashboard/src/services/api/wallets.api.ts @@ -0,0 +1,26 @@ +import { apiClient } from "./api-client"; + +export type WalletMode = "external" | "privy_embedded" | null; + +export interface ProfileWallet { + address: `0x${string}`; + chainType: "ethereum"; + createdAt: string; + id: string; + lastUsedAt: string; + provider: "privy"; + providerWalletId: string; + status: "active"; +} + +export interface WalletsResponse { + mode: WalletMode; + wallets: ProfileWallet[]; +} + +export const WalletsAPI = { + getWallets: (signal?: AbortSignal) => apiClient.get("/wallets", { signal }), + registerPrivyWallet: (input: { address: string; providerWalletId: string }) => + apiClient.post<{ mode: "privy_embedded"; wallet: ProfileWallet }>("/wallets/privy", input), + setMode: (mode: WalletMode) => apiClient.patch<{ mode: WalletMode }>("/wallets/mode", { mode }) +}; diff --git a/apps/dashboard/src/services/auth.test.ts b/apps/dashboard/src/services/auth.test.ts new file mode 100644 index 000000000..690ea7d69 --- /dev/null +++ b/apps/dashboard/src/services/auth.test.ts @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { after, beforeEach, describe, it } from "node:test"; +import { AuthService } from "./auth"; + +const originalLocalStorage = globalThis.localStorage; +const values = new Map(); +const localStorageMock: Storage = { + clear: () => values.clear(), + getItem: key => values.get(key) ?? null, + key: index => [...values.keys()][index] ?? null, + get length() { + return values.size; + }, + removeItem: key => values.delete(key), + setItem: (key, value) => values.set(key, value) +}; + +Object.defineProperty(globalThis, "localStorage", { configurable: true, value: localStorageMock }); + +describe("dashboard auth session bridge", () => { + beforeEach(() => values.clear()); + + after(() => { + Object.defineProperty(globalThis, "localStorage", { configurable: true, value: originalLocalStorage }); + }); + + it("notifies subscribers for login, token refresh storage, and logout", () => { + let notifications = 0; + const unsubscribe = AuthService.subscribe(() => { + notifications += 1; + }); + const tokens = { + accessToken: "access-one", + refreshToken: "refresh-one", + userId: "user-one" + }; + + AuthService.storeTokens(tokens); + AuthService.storeTokens({ ...tokens, accessToken: "access-two" }); + AuthService.clearTokens(); + unsubscribe(); + AuthService.storeTokens(tokens); + + assert.equal(notifications, 3); + }); +}); diff --git a/apps/dashboard/src/services/auth.ts b/apps/dashboard/src/services/auth.ts index 6e2cb9487..5b3273ed0 100644 --- a/apps/dashboard/src/services/auth.ts +++ b/apps/dashboard/src/services/auth.ts @@ -16,6 +16,18 @@ export class AuthService { private static readonly REFRESH_TOKEN_KEY = "vortex_dashboard_refresh_token"; private static readonly USER_ID_KEY = "vortex_dashboard_user_id"; private static readonly USER_EMAIL_KEY = "vortex_dashboard_user_email"; + private static readonly listeners = new Set<() => void>(); + + private static notifyListeners(): void { + for (const listener of this.listeners) { + listener(); + } + } + + static subscribe(listener: () => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } static storeTokens(tokens: AuthTokens): void { localStorage.setItem(this.ACCESS_TOKEN_KEY, tokens.accessToken); @@ -24,6 +36,7 @@ export class AuthService { if (tokens.userEmail) { localStorage.setItem(this.USER_EMAIL_KEY, tokens.userEmail); } + this.notifyListeners(); } static getTokens(): AuthTokens | null { @@ -43,6 +56,7 @@ export class AuthService { localStorage.removeItem(this.REFRESH_TOKEN_KEY); localStorage.removeItem(this.USER_ID_KEY); localStorage.removeItem(this.USER_EMAIL_KEY); + this.notifyListeners(); } static isAuthenticated(): boolean { diff --git a/apps/dashboard/src/services/transactions/userSigning.ts b/apps/dashboard/src/services/transactions/userSigning.ts index 1ed6addb1..b8ca1cde9 100644 --- a/apps/dashboard/src/services/transactions/userSigning.ts +++ b/apps/dashboard/src/services/transactions/userSigning.ts @@ -1,21 +1,17 @@ import { getNetworkId, isEvmTransactionData, type SignedTypedData, type UnsignedTx } from "@vortexfi/shared"; -import { getAccount, sendTransaction, signTypedData, switchChain, waitForTransactionReceipt } from "wagmi/actions"; -import { wagmiConfig } from "@/lib/wagmi"; +import { getAddress } from "viem"; +import { getActiveWalletSigningAdapter } from "@/wallets/signingAdapter"; /** * Signs multiple typed data objects with the connected wallet and returns signature * objects. Ported from the widget's userSigning service. */ export async function signMultipleTypedData(typedDataArray: SignedTypedData[]): Promise { + const adapter = getActiveWalletSigningAdapter(); const signedTypedDataArray: SignedTypedData[] = []; for (const typedData of typedDataArray) { - const rawSignature = await signTypedData(wagmiConfig, { - domain: typedData.domain, - message: typedData.message, - primaryType: typedData.primaryType, - types: typedData.types - }); + const rawSignature = await adapter.signTypedData(typedData); const v = parseInt(rawSignature.slice(130, 132), 16); const r = `0x${rawSignature.slice(2, 66)}` as `0x${string}`; @@ -51,37 +47,16 @@ export async function signAndSubmitEvmTransaction(unsignedTx: UnsignedTx): Promi throw new Error(`Invalid network: ${network}. Unable to determine chain ID.`); } - const account = getAccount(wagmiConfig); - const originalChainId = account.chainId; - if (!originalChainId) { - throw new Error("No wallet connected or unable to determine current chain ID."); - } - - const needsNetworkSwitch = originalChainId !== targetChainId; - if (needsNetworkSwitch) { - try { - await switchChain(wagmiConfig, { chainId: targetChainId }); - } catch (_error) { - throw new Error( - `Failed to switch to network ${network} (chainId: ${targetChainId}). Please switch manually and try again.` - ); - } - } - - try { - const gas = BigInt(txData.gas); - const hash = await sendTransaction(wagmiConfig, { - data: txData.data, - ...(gas > 0n ? { gas } : {}), - to: txData.to, - value: BigInt(txData.value) - }); - const receipt = await waitForTransactionReceipt(wagmiConfig, { chainId: targetChainId, hash }); - return receipt.transactionHash; - } finally { - if (needsNetworkSwitch) { - // Best effort — a failed switch-back must not mask the transaction outcome. - await switchChain(wagmiConfig, { chainId: originalChainId }).catch(() => undefined); - } + const adapter = getActiveWalletSigningAdapter(); + if (getAddress(adapter.address) !== getAddress(unsignedTx.signer)) { + throw new Error("The selected wallet does not match the server-issued transaction signer"); } + const hash = await adapter.sendTransaction({ + chainId: targetChainId, + data: txData.data, + gas: BigInt(txData.gas), + to: txData.to, + value: BigInt(txData.value) + }); + return adapter.waitForTransaction(hash, targetChainId); } diff --git a/apps/dashboard/src/wallets/PrivyWalletRuntime.tsx b/apps/dashboard/src/wallets/PrivyWalletRuntime.tsx new file mode 100644 index 000000000..18bf93b35 --- /dev/null +++ b/apps/dashboard/src/wallets/PrivyWalletRuntime.tsx @@ -0,0 +1,231 @@ +import { + type LinkedAccountWithMetadata, + PrivyProvider, + useCreateWallet, + useExportWallet, + usePrivy, + useSendTransaction, + useSignTypedData, + useSyncJwtBasedAuthState, + useWallets +} from "@privy-io/react-auth"; +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { hexToBytes } from "viem"; +import { waitForTransactionReceipt } from "wagmi/actions"; +import { wagmiConfig } from "@/lib/wagmi"; +import { type WalletMode, WalletsAPI, type WalletsResponse } from "@/services/api/wallets.api"; +import { AuthService } from "@/services/auth"; +import { useAuthStore } from "@/stores/auth.store"; +import { privyWalletConfig } from "./config"; +import { setActiveWalletSigningAdapter, type WalletSigningAdapter } from "./signingAdapter"; +import { type WalletExperience, WalletExperienceContext } from "./WalletExperienceContext"; + +interface PrivyWalletRuntimeProps { + autoCreate: boolean; + children: React.ReactNode; + connectExternalWallet: () => Promise; + onAutoCreateHandled: () => void; + onModeChange: (mode: WalletMode) => void; +} + +interface PrivyWalletProviderRuntimeProps extends PrivyWalletRuntimeProps { + appId: string; + clientId?: string; +} + +function isPrivyEmbeddedWallet(wallet: { type: string; walletClientType?: string }): boolean { + return wallet.type === "ethereum" && (wallet.walletClientType === "privy" || wallet.walletClientType === "privy-v2"); +} + +export function PrivyWalletRuntime({ + autoCreate, + children, + connectExternalWallet, + onAutoCreateHandled, + onModeChange +}: PrivyWalletRuntimeProps) { + const user = useAuthStore(state => state.user); + const queryClient = useQueryClient(); + const { createWallet } = useCreateWallet(); + const { exportWallet } = useExportWallet(); + const { user: privyUser } = usePrivy(); + const { sendTransaction } = useSendTransaction(); + const { signTypedData } = useSignTypedData(); + const { ready: walletsReady, wallets } = useWallets(); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(); + + const getExternalJwt = useCallback(async () => AuthService.getTokens()?.accessToken, []); + const subscribeToAuth = useCallback((onChange: () => void) => AuthService.subscribe(onChange), []); + const { state: authState } = useSyncJwtBasedAuthState({ + enabled: Boolean(user), + getExternalJwt, + onError: authError => setError(`Embedded wallet authentication failed: ${authError.message}`), + subscribe: subscribeToAuth + }); + + const embeddedWallet = wallets.find(isPrivyEmbeddedWallet); + const address = embeddedWallet?.address as `0x${string}` | undefined; + const linkedEmbeddedWallet = privyUser?.linkedAccounts.find( + (account): account is Extract => + account.type === "wallet" && + account.chainType === "ethereum" && + (account.walletClientType === "privy" || account.walletClientType === "privy-v2") && + account.address.toLowerCase() === address?.toLowerCase() + ); + + const signingAdapter = useMemo(() => { + if (!address) return null; + return { + address, + kind: "privy_embedded", + sendTransaction: async transaction => { + const result = await sendTransaction( + { + chainId: transaction.chainId, + data: transaction.data, + gasLimit: transaction.gas, + to: transaction.to, + value: transaction.value + }, + { address, sponsor: privyWalletConfig.gasPolicy === "sponsored" } + ); + return result.hash; + }, + signTypedData: async typedData => { + const domain = { + ...(typedData.domain.chainId !== undefined ? { chainId: Number(typedData.domain.chainId) } : {}), + ...(typedData.domain.name ? { name: typedData.domain.name } : {}), + ...(typedData.domain.salt ? { salt: Uint8Array.from(hexToBytes(typedData.domain.salt)).buffer } : {}), + ...(typedData.domain.verifyingContract ? { verifyingContract: typedData.domain.verifyingContract } : {}), + ...(typedData.domain.version ? { version: typedData.domain.version } : {}) + }; + const result = await signTypedData( + { + domain, + message: typedData.message, + primaryType: typedData.primaryType, + types: typedData.types + }, + { address } + ); + return result.signature as `0x${string}`; + }, + waitForTransaction: async (hash, chainId) => { + const receipt = await waitForTransactionReceipt(wagmiConfig, { chainId, hash }); + return receipt.transactionHash; + } + }; + }, [address, sendTransaction, signTypedData]); + + const register = useCallback( + async (wallet: { address: string; id?: string | null }) => { + if (!wallet.id) { + throw new Error("Privy did not return an embedded wallet ID"); + } + const registered = await WalletsAPI.registerPrivyWallet({ + address: wallet.address, + providerWalletId: wallet.id + }); + queryClient.setQueryData(["wallets", user?.userId], current => ({ + mode: registered.mode, + wallets: current?.wallets.some(item => item.id === registered.wallet.id) + ? (current.wallets ?? []) + : [...(current?.wallets ?? []), registered.wallet] + })); + onModeChange("privy_embedded"); + }, + [onModeChange, queryClient, user?.userId] + ); + + const createEmbeddedWallet = useCallback(async () => { + setCreating(true); + setError(undefined); + try { + if (embeddedWallet && linkedEmbeddedWallet) { + await register(linkedEmbeddedWallet); + } else { + const created = await createWallet(); + await register(created); + } + } catch (cause) { + setError(cause instanceof Error ? cause.message : "Could not create the embedded wallet"); + } finally { + setCreating(false); + } + }, [createWallet, embeddedWallet, linkedEmbeddedWallet, register]); + + const autoCreateStarted = useRef(false); + useEffect(() => { + if (!autoCreate) { + autoCreateStarted.current = false; + } + }, [autoCreate]); + + useEffect(() => { + if (!autoCreate || autoCreateStarted.current || !walletsReady || authState.status !== "done") { + return; + } + autoCreateStarted.current = true; + void createEmbeddedWallet().finally(onAutoCreateHandled); + }, [authState.status, autoCreate, createEmbeddedWallet, onAutoCreateHandled, walletsReady]); + + const switchToExternalWallet = useCallback(async () => { + await WalletsAPI.setMode("external"); + onModeChange("external"); + await connectExternalWallet(); + }, [connectExternalWallet, onModeChange]); + + const value = useMemo( + () => ({ + activateSigner: () => setActiveWalletSigningAdapter(signingAdapter), + address, + canSignOfframp: privyWalletConfig.offrampEnabled, + canUseAsOnrampDestination: privyWalletConfig.onrampEnabled, + canUseEmbeddedWallet: privyWalletConfig.provisioningEnabled, + connectExternalWallet, + connected: Boolean(address), + createEmbeddedWallet, + creatingEmbeddedWallet: creating, + error, + exportEmbeddedWallet: async () => { + if (!address) throw new Error("No embedded wallet is available to export"); + await exportWallet({ address }); + }, + mode: "privy_embedded", + ready: walletsReady, + switchToExternalWallet + }), + [ + address, + connectExternalWallet, + createEmbeddedWallet, + creating, + error, + exportWallet, + signingAdapter, + switchToExternalWallet, + walletsReady + ] + ); + + return {children}; +} + +export function PrivyWalletProviderRuntime({ appId, clientId, ...runtimeProps }: PrivyWalletProviderRuntimeProps) { + return ( + + + + ); +} diff --git a/apps/dashboard/src/wallets/WalletExperienceContext.ts b/apps/dashboard/src/wallets/WalletExperienceContext.ts new file mode 100644 index 000000000..f85c94704 --- /dev/null +++ b/apps/dashboard/src/wallets/WalletExperienceContext.ts @@ -0,0 +1,29 @@ +import { createContext, use } from "react"; +import type { WalletMode } from "@/services/api/wallets.api"; + +export interface WalletExperience { + activateSigner: () => void; + address?: `0x${string}`; + canSignOfframp: boolean; + canUseAsOnrampDestination: boolean; + canUseEmbeddedWallet: boolean; + connectExternalWallet: () => Promise; + connected: boolean; + createEmbeddedWallet: () => Promise; + creatingEmbeddedWallet: boolean; + error?: string; + exportEmbeddedWallet: () => Promise; + mode: WalletMode; + ready: boolean; + switchToExternalWallet: () => Promise; +} + +export const WalletExperienceContext = createContext(null); + +export function useWalletExperience(): WalletExperience { + const value = use(WalletExperienceContext); + if (!value) { + throw new Error("useWalletExperience must be used inside WalletExperienceProvider"); + } + return value; +} diff --git a/apps/dashboard/src/wallets/WalletExperienceProvider.tsx b/apps/dashboard/src/wallets/WalletExperienceProvider.tsx new file mode 100644 index 000000000..f6184c89e --- /dev/null +++ b/apps/dashboard/src/wallets/WalletExperienceProvider.tsx @@ -0,0 +1,106 @@ +import { useAppKit, useAppKitAccount } from "@reown/appkit/react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { lazy, Suspense, useCallback, useMemo, useState } from "react"; +import { useAccount } from "wagmi"; +import { type WalletMode, WalletsAPI, type WalletsResponse } from "@/services/api/wallets.api"; +import { useAuthStore } from "@/stores/auth.store"; +import { privyWalletConfig } from "./config"; +import { createExternalSigningAdapter } from "./externalSigningAdapter"; +import { setActiveWalletSigningAdapter } from "./signingAdapter"; +import { type WalletExperience, WalletExperienceContext } from "./WalletExperienceContext"; + +const LazyPrivyWalletRuntime = lazy(async () => { + const module = await import("./PrivyWalletRuntime"); + return { default: module.PrivyWalletProviderRuntime }; +}); + +export function WalletExperienceProvider({ children }: { children: React.ReactNode }) { + const user = useAuthStore(state => state.user); + const queryClient = useQueryClient(); + const { address } = useAccount(); + const { isConnected } = useAppKitAccount(); + const { open } = useAppKit(); + const [pendingMode, setPendingMode] = useState(); + const [autoCreateEmbedded, setAutoCreateEmbedded] = useState(false); + + const walletsQuery = useQuery({ + enabled: Boolean(user), + queryFn: ({ signal }) => WalletsAPI.getWallets(signal), + queryKey: ["wallets", user?.userId], + staleTime: 30_000 + }); + + const storedMode = pendingMode ?? walletsQuery.data?.mode ?? null; + const mode = storedMode === "privy_embedded" && !privyWalletConfig.enabled ? "external" : storedMode; + const embeddedActive = privyWalletConfig.enabled && storedMode === "privy_embedded"; + + const connectExternalWallet = useCallback(async () => { + if (storedMode === "privy_embedded") { + const response = await WalletsAPI.setMode("external"); + setPendingMode(response.mode); + queryClient.setQueryData(["wallets", user?.userId], current => ({ + mode: response.mode, + wallets: current?.wallets ?? [] + })); + } + await open({ view: "Connect" }); + }, [open, queryClient, storedMode, user?.userId]); + + const onModeChange = useCallback( + (nextMode: WalletMode) => { + setPendingMode(nextMode); + queryClient.setQueryData(["wallets", user?.userId], current => ({ + mode: nextMode, + wallets: current?.wallets ?? [] + })); + }, + [queryClient, user?.userId] + ); + + const externalAdapter = useMemo(() => (address ? createExternalSigningAdapter(address) : null), [address]); + const externalValue = useMemo( + () => ({ + activateSigner: () => setActiveWalletSigningAdapter(externalAdapter), + address, + canSignOfframp: true, + canUseAsOnrampDestination: true, + canUseEmbeddedWallet: privyWalletConfig.provisioningEnabled, + connectExternalWallet, + connected: isConnected && Boolean(address), + createEmbeddedWallet: async () => { + if (!privyWalletConfig.provisioningEnabled) { + throw new Error("Embedded wallets are not enabled in this environment"); + } + setAutoCreateEmbedded(true); + setPendingMode("privy_embedded"); + }, + creatingEmbeddedWallet: false, + exportEmbeddedWallet: async () => { + throw new Error("Select your embedded wallet before exporting it"); + }, + mode, + ready: !walletsQuery.isLoading, + switchToExternalWallet: connectExternalWallet + }), + [address, connectExternalWallet, externalAdapter, isConnected, mode, walletsQuery.isLoading] + ); + + if (embeddedActive) { + return ( + + setAutoCreateEmbedded(false)} + onModeChange={onModeChange} + > + {children} + + + ); + } + + return {children}; +} diff --git a/apps/dashboard/src/wallets/config.test.ts b/apps/dashboard/src/wallets/config.test.ts new file mode 100644 index 000000000..05fde3291 --- /dev/null +++ b/apps/dashboard/src/wallets/config.test.ts @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { readPrivyWalletConfig } from "./config"; + +function env(values: Record): ImportMetaEnv { + return values as ImportMetaEnv; +} + +describe("dashboard Privy configuration", () => { + it("is disabled by default and never enables without an app ID", () => { + assert.deepEqual(readPrivyWalletConfig(env({})), { + appId: "", + clientId: undefined, + enabled: false, + gasPolicy: "user_pays", + offrampEnabled: false, + onrampEnabled: false, + provisioningEnabled: false + }); + assert.equal(readPrivyWalletConfig(env({ VITE_PRIVY_ENABLED: "true" })).enabled, false); + }); + + it("requires an explicit true flag and defaults unknown gas policies to user-pays", () => { + const config = readPrivyWalletConfig( + env({ + VITE_PRIVY_APP_ID: " app-test ", + VITE_PRIVY_CLIENT_ID: " client-test ", + VITE_PRIVY_ENABLED: "TRUE", + VITE_PRIVY_GAS_POLICY: "anything", + VITE_PRIVY_OFFRAMP_ENABLED: "true", + VITE_PRIVY_ONRAMP_ENABLED: "true", + VITE_PRIVY_PROVISIONING_ENABLED: "true" + }) + ); + assert.deepEqual(config, { + appId: "app-test", + clientId: "client-test", + enabled: true, + gasPolicy: "user_pays", + offrampEnabled: true, + onrampEnabled: true, + provisioningEnabled: true + }); + }); +}); diff --git a/apps/dashboard/src/wallets/config.ts b/apps/dashboard/src/wallets/config.ts new file mode 100644 index 000000000..515bdaad0 --- /dev/null +++ b/apps/dashboard/src/wallets/config.ts @@ -0,0 +1,34 @@ +export type PrivyGasPolicy = "sponsored" | "user_pays"; + +export interface PrivyWalletConfig { + appId: string; + clientId?: string; + enabled: boolean; + gasPolicy: PrivyGasPolicy; + offrampEnabled: boolean; + onrampEnabled: boolean; + provisioningEnabled: boolean; +} + +function enabled(value: string | undefined): boolean { + return value?.trim().toLowerCase() === "true"; +} + +export function readPrivyWalletConfig(env: ImportMetaEnv = import.meta.env): PrivyWalletConfig { + const appId = env.VITE_PRIVY_APP_ID?.trim() ?? ""; + const clientId = env.VITE_PRIVY_CLIENT_ID?.trim() || undefined; + const gasPolicy = env.VITE_PRIVY_GAS_POLICY === "sponsored" ? "sponsored" : "user_pays"; + const isEnabled = enabled(env.VITE_PRIVY_ENABLED) && appId.length > 0; + + return { + appId, + clientId, + enabled: isEnabled, + gasPolicy, + offrampEnabled: isEnabled && enabled(env.VITE_PRIVY_OFFRAMP_ENABLED), + onrampEnabled: isEnabled && enabled(env.VITE_PRIVY_ONRAMP_ENABLED), + provisioningEnabled: isEnabled && enabled(env.VITE_PRIVY_PROVISIONING_ENABLED) + }; +} + +export const privyWalletConfig = readPrivyWalletConfig(); diff --git a/apps/dashboard/src/wallets/externalSigningAdapter.ts b/apps/dashboard/src/wallets/externalSigningAdapter.ts new file mode 100644 index 000000000..01353692f --- /dev/null +++ b/apps/dashboard/src/wallets/externalSigningAdapter.ts @@ -0,0 +1,61 @@ +import type { SignedTypedData } from "@vortexfi/shared"; +import { getAccount, sendTransaction, signTypedData, switchChain, waitForTransactionReceipt } from "wagmi/actions"; +import { wagmiConfig } from "@/lib/wagmi"; +import type { WalletSigningAdapter, WalletTransactionRequest } from "./signingAdapter"; + +export function createExternalSigningAdapter(address: `0x${string}`): WalletSigningAdapter { + const originalChainByHash = new Map<`0x${string}`, number>(); + return { + address, + kind: "external", + sendTransaction: async (transaction: WalletTransactionRequest) => { + const account = getAccount(wagmiConfig); + if (!account.chainId) { + throw new Error("No wallet connected or unable to determine current chain ID."); + } + const originalChainId = account.chainId; + const switched = originalChainId !== transaction.chainId; + if (switched) { + try { + await switchChain(wagmiConfig, { chainId: transaction.chainId }); + } catch { + throw new Error(`Failed to switch to chain ${transaction.chainId}. Please switch manually and try again.`); + } + } + try { + const hash = await sendTransaction(wagmiConfig, { + data: transaction.data, + ...(transaction.gas && transaction.gas > 0n ? { gas: transaction.gas } : {}), + to: transaction.to, + value: transaction.value + }); + if (switched) originalChainByHash.set(hash, originalChainId); + return hash; + } catch (error) { + if (switched) { + await switchChain(wagmiConfig, { chainId: originalChainId }).catch(() => undefined); + } + throw error; + } + }, + signTypedData: (typedData: SignedTypedData) => + signTypedData(wagmiConfig, { + domain: typedData.domain, + message: typedData.message, + primaryType: typedData.primaryType, + types: typedData.types + }), + waitForTransaction: async (hash, chainId) => { + try { + const receipt = await waitForTransactionReceipt(wagmiConfig, { chainId, hash }); + return receipt.transactionHash; + } finally { + const originalChainId = originalChainByHash.get(hash); + originalChainByHash.delete(hash); + if (originalChainId !== undefined) { + await switchChain(wagmiConfig, { chainId: originalChainId }).catch(() => undefined); + } + } + } + }; +} diff --git a/apps/dashboard/src/wallets/signingAdapter.ts b/apps/dashboard/src/wallets/signingAdapter.ts new file mode 100644 index 000000000..5cdc1dc4b --- /dev/null +++ b/apps/dashboard/src/wallets/signingAdapter.ts @@ -0,0 +1,31 @@ +import type { SignedTypedData } from "@vortexfi/shared"; +import type { Hex } from "viem"; + +export interface WalletTransactionRequest { + chainId: number; + data: Hex; + gas?: bigint; + to: Hex; + value: bigint; +} + +export interface WalletSigningAdapter { + address: Hex; + kind: "external" | "privy_embedded"; + sendTransaction: (transaction: WalletTransactionRequest) => Promise; + signTypedData: (typedData: SignedTypedData) => Promise; + waitForTransaction: (hash: Hex, chainId: number) => Promise; +} + +let activeAdapter: WalletSigningAdapter | null = null; + +export function setActiveWalletSigningAdapter(adapter: WalletSigningAdapter | null): void { + activeAdapter = adapter; +} + +export function getActiveWalletSigningAdapter(): WalletSigningAdapter { + if (!activeAdapter) { + throw new Error("The selected wallet is not ready to sign"); + } + return activeAdapter; +} diff --git a/apps/dashboard/src/wallets/walletSigning.contract.test.ts b/apps/dashboard/src/wallets/walletSigning.contract.test.ts new file mode 100644 index 000000000..aaf531c15 --- /dev/null +++ b/apps/dashboard/src/wallets/walletSigning.contract.test.ts @@ -0,0 +1,111 @@ +import { + Networks, + type SignedTypedData, + type UnsignedTx +} from "@vortexfi/shared"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + signAndSubmitEvmTransaction, + signMultipleTypedData +} from "@/services/transactions/userSigning"; +import { + setActiveWalletSigningAdapter, + type WalletSigningAdapter +} from "./signingAdapter"; + +const address = "0x1111111111111111111111111111111111111111"; +const txHash = `0x${"cd".repeat(32)}` as `0x${string}`; +const confirmedHash = `0x${"ef".repeat(32)}` as `0x${string}`; +const rawSignature = `0x${"11".repeat(64)}1b` as `0x${string}`; + +const typedData: SignedTypedData = { + domain: { + name: "Permit2", + verifyingContract: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + version: "1" + }, + message: { amount: "1", deadline: "123" }, + primaryType: "PermitTransferFrom", + types: { PermitTransferFrom: [{ name: "amount", type: "uint256" }] } +}; + +const unsignedTx = { + meta: {}, + network: Networks.Base, + nonce: 0, + phase: "squidRouterNoPermitTransfer", + signer: address, + txData: { + data: "0x1234", + gas: "21000", + nonce: 0, + to: "0x2222222222222222222222222222222222222222", + value: "7" + } +} as UnsignedTx; + +function fakeAdapter(kind: WalletSigningAdapter["kind"]) { + const calls: Array<{ name: string; value: unknown }> = []; + const adapter: WalletSigningAdapter = { + address, + kind, + sendTransaction: async transaction => { + calls.push({ name: "sendTransaction", value: transaction }); + return txHash; + }, + signTypedData: async data => { + calls.push({ name: "signTypedData", value: data }); + return rawSignature; + }, + waitForTransaction: async (hash, chainId) => { + calls.push({ name: "waitForTransaction", value: { chainId, hash } }); + return confirmedHash; + } + }; + return { adapter, calls }; +} + +describe("wallet signer contract", () => { + for (const kind of ["external", "privy_embedded"] as const) { + it(`${kind} produces the same permit and transaction result shapes`, async () => { + const fake = fakeAdapter(kind); + setActiveWalletSigningAdapter(fake.adapter); + + const [signed] = await signMultipleTypedData([typedData]); + const hash = await signAndSubmitEvmTransaction(unsignedTx); + + assert.ok(signed); + assert.deepEqual(signed.signature, { + deadline: 123, + r: `0x${"11".repeat(32)}`, + s: `0x${"11".repeat(32)}`, + v: 27 + }); + assert.equal(hash, confirmedHash); + assert.deepEqual(fake.calls.map(call => call.name), [ + "signTypedData", + "sendTransaction", + "waitForTransaction" + ]); + const sent = fake.calls[1]?.value as { chainId: number; gas: bigint; value: bigint }; + assert.equal(sent.chainId, 8453); + assert.equal(sent.gas, 21000n); + assert.equal(sent.value, 7n); + }); + } + + it("rejects a server-issued transaction for a different signer before broadcasting", async () => { + const fake = fakeAdapter("privy_embedded"); + setActiveWalletSigningAdapter(fake.adapter); + + await assert.rejects( + signAndSubmitEvmTransaction({ + ...unsignedTx, + signer: "0x2222222222222222222222222222222222222222" + }), + /does not match the server-issued transaction signer/ + ); + assert.equal(fake.calls.length, 0); + }); +}); From fbabce87b7397c5ff4d9f78a2e14faa730ebb425 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 27 Jul 2026 19:10:31 +0200 Subject: [PATCH 05/16] feat(frontend): add optional Privy embedded wallets --- apps/frontend/.env.example | 13 + apps/frontend/e2e/support/mockBackend.ts | 9 + apps/frontend/e2e/wallet-privy-choice.spec.ts | 14 ++ apps/frontend/playwright.privy.config.ts | 29 +++ .../components/QuoteSubmitButtons/index.tsx | 4 +- .../components/Ramp/Offramp/Offramp.test.tsx | 17 ++ .../components/Ramp/Onramp/Onramp.test.tsx | 17 ++ .../src/components/UserBalance/index.tsx | 4 +- .../buttons/EVMWalletButton/index.tsx | 53 ++-- .../buttons/SwapSubmitButton/index.tsx | 4 +- apps/frontend/src/contexts/network.tsx | 6 +- apps/frontend/src/hooks/useRampHistory.ts | 4 +- apps/frontend/src/hooks/useVortexAccount.ts | 44 ++-- .../src/machines/ramp.machine.test.ts | 18 ++ apps/frontend/src/machines/ramp.machine.ts | 30 +++ apps/frontend/src/machines/types.ts | 7 +- apps/frontend/src/main.tsx | 13 +- apps/frontend/src/pages/widget/index.tsx | 2 +- apps/frontend/src/services/api/api-client.ts | 4 +- .../src/services/api/wallets.service.ts | 26 ++ apps/frontend/src/services/auth.test.ts | 25 ++ apps/frontend/src/services/auth.ts | 14 ++ .../src/services/transactions/userSigning.ts | 87 ++----- .../src/stories/providers/MockProviders.tsx | 17 +- .../src/wallets/PrivyWidgetWalletRuntime.tsx | 230 ++++++++++++++++++ .../src/wallets/WidgetWalletContext.ts | 26 ++ .../src/wallets/WidgetWalletProvider.tsx | 128 ++++++++++ apps/frontend/src/wallets/config.test.ts | 50 ++++ apps/frontend/src/wallets/config.ts | 69 ++++++ .../src/wallets/externalSigningAdapter.ts | 61 +++++ apps/frontend/src/wallets/signingAdapter.ts | 31 +++ .../wallets/walletSigning.contract.test.ts | 99 ++++++++ 32 files changed, 1015 insertions(+), 140 deletions(-) create mode 100644 apps/frontend/e2e/wallet-privy-choice.spec.ts create mode 100644 apps/frontend/playwright.privy.config.ts create mode 100644 apps/frontend/src/services/api/wallets.service.ts create mode 100644 apps/frontend/src/services/auth.test.ts create mode 100644 apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx create mode 100644 apps/frontend/src/wallets/WidgetWalletContext.ts create mode 100644 apps/frontend/src/wallets/WidgetWalletProvider.tsx create mode 100644 apps/frontend/src/wallets/config.test.ts create mode 100644 apps/frontend/src/wallets/config.ts create mode 100644 apps/frontend/src/wallets/externalSigningAdapter.ts create mode 100644 apps/frontend/src/wallets/signingAdapter.ts create mode 100644 apps/frontend/src/wallets/walletSigning.contract.test.ts diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example index 498ce283d..5aaf7c4ca 100644 --- a/apps/frontend/.env.example +++ b/apps/frontend/.env.example @@ -4,3 +4,16 @@ VITE_SUPABASE_ANON_KEY=your-anon-key-here # Sentry (optional). If unset, Sentry is not initialized. VITE_SENTRY_DSN= + +# Optional Privy embedded-wallet path. This remains disabled unless all required +# public configuration is present. Existing Reown and Polkadot flows are unchanged. +VITE_PRIVY_ENABLED=false +VITE_PRIVY_APP_ID= +VITE_PRIVY_CLIENT_ID= +VITE_PRIVY_PROVISIONING_ENABLED=false +VITE_PRIVY_GAS_POLICY=user_pays + +# Comma-separated, exact origins allowed to frame the widget while Privy is active. +# Top-level Vortex pages are allowed automatically. An unknown/referrer-less iframe +# never initializes Privy. +VITE_PRIVY_WIDGET_PARENT_ORIGINS= diff --git a/apps/frontend/e2e/support/mockBackend.ts b/apps/frontend/e2e/support/mockBackend.ts index e241f442c..ac1fb2386 100644 --- a/apps/frontend/e2e/support/mockBackend.ts +++ b/apps/frontend/e2e/support/mockBackend.ts @@ -191,6 +191,15 @@ export async function mockBackend(page: Page, options: MockBackendOptions = {}) await fulfillJson({ access_token: "e2e-access-token", refresh_token: "e2e-refresh-token", success: true }); return; } + if (path === "/v1/wallets" && method === "GET") { + await fulfillJson({ mode: null, wallets: [] }); + return; + } + if (path === "/v1/wallets/mode" && method === "PATCH") { + const body = request.postDataJSON() as { mode: string | null }; + await fulfillJson({ mode: body.mode }); + return; + } // Avenia/BRLA KYC gate: an existing, KYC-confirmed user (BrlaGetUserResponse shape), // so validateKyc reports kycNeeded=false and the ramp can proceed to the summary. diff --git a/apps/frontend/e2e/wallet-privy-choice.spec.ts b/apps/frontend/e2e/wallet-privy-choice.spec.ts new file mode 100644 index 000000000..2ce4111fc --- /dev/null +++ b/apps/frontend/e2e/wallet-privy-choice.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from "@playwright/test"; +import { mockBackend } from "./support/mockBackend"; + +test("the widget offers both external and Vortex wallets when provisioning is enabled", async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== "privy-choice", "Runs with the opt-in Privy Playwright configuration"); + + await mockBackend(page); + await page.goto("/widget?rampType=SELL&fiat=BRL&inputAmount=100"); + + await expect(page.getByRole("button", { name: /Connect Wallet/ }).first()).toBeVisible({ + timeout: 20_000 + }); + await expect(page.getByRole("button", { name: "Use a Vortex wallet" }).first()).toBeVisible(); +}); diff --git a/apps/frontend/playwright.privy.config.ts b/apps/frontend/playwright.privy.config.ts new file mode 100644 index 000000000..27a39746e --- /dev/null +++ b/apps/frontend/playwright.privy.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + forbidOnly: !!process.env.CI, + projects: [{ name: "privy-choice", use: { ...devices["Desktop Chrome"] } }], + reporter: process.env.CI ? [["list"], ["github"]] : [["list"]], + retries: process.env.CI ? 2 : 0, + testDir: "./e2e", + testMatch: "wallet-privy-choice.spec.ts", + timeout: 60_000, + use: { + baseURL: "http://127.0.0.1:5176", + trace: "on-first-retry" + }, + webServer: { + command: "bun x --bun vite --port 5176 --strictPort --host 127.0.0.1", + env: { + VITE_ALCHEMY_API_KEY: "e2e-mock-key", + VITE_PRIVY_APP_ID: "e2e-public-app-id", + VITE_PRIVY_ENABLED: "true", + VITE_PRIVY_PROVISIONING_ENABLED: "true", + VITE_SUPABASE_ANON_KEY: "e2e-mock-anon-key", + VITE_SUPABASE_URL: "http://supabase.invalid" + }, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + url: "http://127.0.0.1:5176/" + } +}); diff --git a/apps/frontend/src/components/QuoteSubmitButtons/index.tsx b/apps/frontend/src/components/QuoteSubmitButtons/index.tsx index 938d39ef6..472310ba0 100644 --- a/apps/frontend/src/components/QuoteSubmitButtons/index.tsx +++ b/apps/frontend/src/components/QuoteSubmitButtons/index.tsx @@ -1,5 +1,4 @@ import { InformationCircleIcon } from "@heroicons/react/24/outline"; -import { useAppKitAccount } from "@reown/appkit/react"; import { useParams, useRouter } from "@tanstack/react-router"; import { FiatToken, isNetworkEVM, RampDirection } from "@vortexfi/shared"; import Big from "big.js"; @@ -10,6 +9,7 @@ import { usePolkadotWalletState } from "../../contexts/polkadotWallet"; import { useRampActor } from "../../contexts/rampState"; import { useRampValidation } from "../../hooks/ramp/useRampValidation"; import { useMaintenanceAwareButton } from "../../hooks/useMaintenanceAware"; +import { useVortexAccount } from "../../hooks/useVortexAccount"; import { useFiatToken, useInputAmount } from "../../stores/quote/useQuoteFormStore"; import { useQuoteStore } from "../../stores/quote/useQuoteStore"; import { useRampDirection } from "../../stores/rampDirectionStore"; @@ -27,7 +27,7 @@ export const WalletConnectedSubmitButton: FC = needsWalletConnection = false }) => { const { walletAccount } = usePolkadotWalletState(); - const { isConnected } = useAppKitAccount(); + const { isConnected } = useVortexAccount(); const { selectedNetwork } = useNetwork(); if (needsWalletConnection) { diff --git a/apps/frontend/src/components/Ramp/Offramp/Offramp.test.tsx b/apps/frontend/src/components/Ramp/Offramp/Offramp.test.tsx index 84016614a..feeab7037 100644 --- a/apps/frontend/src/components/Ramp/Offramp/Offramp.test.tsx +++ b/apps/frontend/src/components/Ramp/Offramp/Offramp.test.tsx @@ -32,6 +32,20 @@ const stubs = { signMessage: { signMessageAsync: vi.fn() }, switchChain: { switchChainAsync: vi.fn() } }; +const widgetWallet = { + activateSigner: vi.fn(), + address: undefined, + canUseEmbeddedWallet: false, + connectExternalWallet: vi.fn(), + connected: false, + createEmbeddedWallet: vi.fn(), + creatingEmbeddedWallet: false, + exportEmbeddedWallet: vi.fn(), + mode: null, + ready: true, + signMessage: vi.fn(), + switchToExternalWallet: vi.fn() +}; vi.mock("wagmi", () => ({ useAccount: () => stubs.account, @@ -70,6 +84,9 @@ vi.mock("../../../contexts/events", () => ({ vi.mock("../../../contexts/polkadotWallet", () => ({ usePolkadotWalletState: () => stubs.polkadotWallet })); +vi.mock("../../../wallets/WidgetWalletContext", () => ({ + useWidgetWallet: () => widgetWallet +})); import { Offramp } from "./index"; diff --git a/apps/frontend/src/components/Ramp/Onramp/Onramp.test.tsx b/apps/frontend/src/components/Ramp/Onramp/Onramp.test.tsx index 261183b9f..5cd385896 100644 --- a/apps/frontend/src/components/Ramp/Onramp/Onramp.test.tsx +++ b/apps/frontend/src/components/Ramp/Onramp/Onramp.test.tsx @@ -35,6 +35,20 @@ const stubs = { signMessage: { signMessageAsync: vi.fn() }, switchChain: { switchChainAsync: vi.fn() } }; +const widgetWallet = { + activateSigner: vi.fn(), + address: undefined, + canUseEmbeddedWallet: false, + connectExternalWallet: vi.fn(), + connected: false, + createEmbeddedWallet: vi.fn(), + creatingEmbeddedWallet: false, + exportEmbeddedWallet: vi.fn(), + mode: null, + ready: true, + signMessage: vi.fn(), + switchToExternalWallet: vi.fn() +}; vi.mock("wagmi", () => ({ useAccount: () => stubs.account, @@ -74,6 +88,9 @@ vi.mock("../../../contexts/events", () => ({ vi.mock("../../../contexts/polkadotWallet", () => ({ usePolkadotWalletState: () => stubs.polkadotWallet })); +vi.mock("../../../wallets/WidgetWalletContext", () => ({ + useWidgetWallet: () => widgetWallet +})); import { Onramp } from "./index"; diff --git a/apps/frontend/src/components/UserBalance/index.tsx b/apps/frontend/src/components/UserBalance/index.tsx index f66a3e4fb..bfae9de30 100644 --- a/apps/frontend/src/components/UserBalance/index.tsx +++ b/apps/frontend/src/components/UserBalance/index.tsx @@ -1,6 +1,5 @@ import { OnChainTokenDetails } from "@vortexfi/shared"; import Big from "big.js"; -import { useAccount } from "wagmi"; import wallet from "../../assets/wallet-bifold-outline.svg"; import { usePolkadotWalletState } from "../../contexts/polkadotWallet"; @@ -67,8 +66,7 @@ const FullBalance = ({ token, onClick }: { token: OnChainTokenDetails; onClick: }; export const UserBalance = ({ token, onClick, className }: UserBalanceProps) => { - const { isDisconnected } = useVortexAccount(); - const { address: evmAddress } = useAccount(); + const { evmAddress, isDisconnected } = useVortexAccount(); const { walletAccount: polkadotWalletAccount } = usePolkadotWalletState(); const hasNoWallets = !evmAddress && !polkadotWalletAccount; diff --git a/apps/frontend/src/components/buttons/EVMWalletButton/index.tsx b/apps/frontend/src/components/buttons/EVMWalletButton/index.tsx index 446255b9a..f9c8446d7 100644 --- a/apps/frontend/src/components/buttons/EVMWalletButton/index.tsx +++ b/apps/frontend/src/components/buttons/EVMWalletButton/index.tsx @@ -1,8 +1,9 @@ -import { useAppKit, useAppKitAccount, useAppKitNetwork } from "@reown/appkit/react"; +import { useAppKit, useAppKitNetwork } from "@reown/appkit/react"; import { isNetworkEVM, Networks } from "@vortexfi/shared"; import { useTranslation } from "react-i18next"; import { useVortexAccount } from "../../../hooks/useVortexAccount"; import { wagmiConfig } from "../../../wagmiConfig"; +import { useWidgetWallet } from "../../../wallets/WidgetWalletContext"; import { WalletButtonVariant } from "../ConnectWalletButton"; import { BaseWalletButton } from "../ConnectWalletButton/BaseWalletButton"; @@ -18,7 +19,7 @@ export function EVMWalletButton({ forceNetwork?: Networks; }) { const { address, chainId: walletChainId } = useVortexAccount(forceNetwork); - const { isConnected } = useAppKitAccount(); + const wallet = useWidgetWallet(); const { caipNetwork: appkitNetwork, switchNetwork } = useAppKitNetwork(); const { open } = useAppKit(); const { t } = useTranslation(); @@ -26,25 +27,41 @@ export function EVMWalletButton({ const isOnSupportedNetwork = (forceNetwork && isNetworkEVM(forceNetwork)) || wagmiConfig.chains.find(chain => chain.id === walletChainId) !== undefined; - if (!isConnected) { + if (!wallet.connected) { return ( - { - open({ view: "Connect" }); - }} - showPlayIcon - variant={variant} - > -

- {t("components.dialogs.connectWallet.connect")} Wallet -

-
+
+ { + void wallet.connectExternalWallet(); + }} + showPlayIcon + variant={variant} + > +

+ {t("components.dialogs.connectWallet.connect")} Wallet +

+
+ {wallet.canUseEmbeddedWallet && ( + + Use a Vortex wallet + + )} + {wallet.embeddedUnavailableReason && ( +

{wallet.embeddedUnavailableReason}

+ )} +
); } - if (!isOnSupportedNetwork) { + if (wallet.mode !== "privy_embedded" && !isOnSupportedNetwork) { return ( { - open({ view: "Account" }); + if (wallet.mode !== "privy_embedded") open({ view: "Account" }); }} variant={variant} /> diff --git a/apps/frontend/src/components/buttons/SwapSubmitButton/index.tsx b/apps/frontend/src/components/buttons/SwapSubmitButton/index.tsx index 754820934..858a6ea52 100644 --- a/apps/frontend/src/components/buttons/SwapSubmitButton/index.tsx +++ b/apps/frontend/src/components/buttons/SwapSubmitButton/index.tsx @@ -1,9 +1,9 @@ -import { useAppKitAccount } from "@reown/appkit/react"; import { isNetworkEVM } from "@vortexfi/shared"; import { FC } from "react"; import { useNetwork } from "../../../contexts/network"; import { usePolkadotWalletState } from "../../../contexts/polkadotWallet"; import { useMaintenanceAwareButton } from "../../../hooks/useMaintenanceAware"; +import { useVortexAccount } from "../../../hooks/useVortexAccount"; import { Spinner } from "../../Spinner"; import { ConnectWalletButton } from "../ConnectWalletButton"; @@ -17,7 +17,7 @@ export const SwapSubmitButton: FC = ({ text, disabled, pe const { buttonProps, isMaintenanceDisabled } = useMaintenanceAwareButton(disabled || pending); const { walletAccount } = usePolkadotWalletState(); - const { isConnected } = useAppKitAccount(); + const { isConnected } = useVortexAccount(); const { selectedNetwork } = useNetwork(); if (!isNetworkEVM(selectedNetwork) && !walletAccount) { diff --git a/apps/frontend/src/contexts/network.tsx b/apps/frontend/src/contexts/network.tsx index 0f7cef49a..f3b8847dc 100644 --- a/apps/frontend/src/contexts/network.tsx +++ b/apps/frontend/src/contexts/network.tsx @@ -5,6 +5,7 @@ import { getEnabledFrontendNetwork } from "../config/networkAvailability"; import { WALLETCONNECT_ASSETHUB_ID } from "../constants/constants"; import { LocalStorageKeys, useLocalStorage } from "../hooks/useLocalStorage"; import { useRampUrlParams } from "../hooks/useRampUrlParams"; +import { useWidgetWallet } from "../wallets/WidgetWalletContext"; import { useRampActor } from "./rampState"; interface NetworkContextType { @@ -45,6 +46,7 @@ export const NetworkProvider = ({ children }: NetworkProviderProps) => { const { switchChainAsync } = useSwitchChain(); const { chain: connectedEvmChain } = useAccount(); + const evmWallet = useWidgetWallet(); const setSelectedNetwork = useCallback( async (network: Networks, resetState = false) => { @@ -56,7 +58,7 @@ export const NetworkProvider = ({ children }: NetworkProviderProps) => { setSelectedNetworkLocalStorage(enabledNetwork); // Will only switch chain on the EVM connected wallet case. - if (isNetworkEVM(enabledNetwork)) { + if (isNetworkEVM(enabledNetwork) && evmWallet.mode !== "privy_embedded") { // Only switch chain if the network is different from the current one // see https://github.com/wevm/wagmi/issues/3417 if (!connectedEvmChain || connectedEvmChain.id !== getNetworkId(enabledNetwork)) { @@ -64,7 +66,7 @@ export const NetworkProvider = ({ children }: NetworkProviderProps) => { } } }, - [connectedEvmChain, switchChainAsync, setSelectedNetworkLocalStorage, rampActor] + [connectedEvmChain, switchChainAsync, setSelectedNetworkLocalStorage, rampActor, evmWallet.mode] ); return ( diff --git a/apps/frontend/src/hooks/useRampHistory.ts b/apps/frontend/src/hooks/useRampHistory.ts index 2c0255b9e..dd37e716c 100644 --- a/apps/frontend/src/hooks/useRampHistory.ts +++ b/apps/frontend/src/hooks/useRampHistory.ts @@ -1,10 +1,10 @@ import { useQuery } from "@tanstack/react-query"; import { GetRampHistoryTransaction } from "@vortexfi/shared"; -import { useAccount } from "wagmi"; import { Transaction } from "../components/menus/HistoryMenu/types"; import { usePolkadotWalletState } from "../contexts/polkadotWallet"; import { RampService } from "../services/api/ramp.service"; +import { useWidgetWallet } from "../wallets/WidgetWalletContext"; function formatTransaction(tx: GetRampHistoryTransaction): Transaction { return { @@ -23,7 +23,7 @@ function formatTransaction(tx: GetRampHistoryTransaction): Transaction { } export function useRampHistory(walletAddress?: string) { - const { address: evmAddress } = useAccount(); + const { address: evmAddress } = useWidgetWallet(); const { walletAccount: polkadotAccount } = usePolkadotWalletState(); const addresses = walletAddress ? [walletAddress] : ([evmAddress, polkadotAccount?.address].filter(Boolean) as string[]); diff --git a/apps/frontend/src/hooks/useVortexAccount.ts b/apps/frontend/src/hooks/useVortexAccount.ts index d1e306f66..05d66baad 100644 --- a/apps/frontend/src/hooks/useVortexAccount.ts +++ b/apps/frontend/src/hooks/useVortexAccount.ts @@ -1,10 +1,10 @@ -import * as Sentry from "@sentry/react"; -import { ASSETHUB_CHAIN_ID, isNetworkEVM, Networks } from "@vortexfi/shared"; +import { ASSETHUB_CHAIN_ID, getNetworkId, isNetworkEVM, Networks } from "@vortexfi/shared"; import { useCallback, useEffect, useMemo } from "react"; -import { useAccount, useSignMessage } from "wagmi"; +import { useAccount } from "wagmi"; import { useNetwork } from "../contexts/network"; import { usePolkadotWalletState } from "../contexts/polkadotWallet"; import { useRampActor } from "../contexts/rampState"; +import { useWidgetWallet } from "../wallets/WidgetWalletContext"; // A helper hook to provide an abstraction over the account used. // The account could be an EVM account or a Polkadot account. @@ -14,8 +14,9 @@ export const useVortexAccount = (forceNetwork?: Networks) => { const rampActor = useRampActor(); const { walletAccount: polkadotWalletAccount } = usePolkadotWalletState(); - const { chainId: evmChainId, address: evmAccountAddress } = useAccount(); - const { signMessageAsync } = useSignMessage(); + const { chainId: externalChainId } = useAccount(); + const evmWallet = useWidgetWallet(); + const evmAccountAddress = evmWallet.address; const address = useMemo(() => { if (!isNetworkEVM(selectedNetwork)) { @@ -25,17 +26,6 @@ export const useVortexAccount = (forceNetwork?: Networks) => { } }, [evmAccountAddress, polkadotWalletAccount, selectedNetwork]); - useEffect(() => { - const user = Sentry.getCurrentScope().getUser(); - // Set the wallet address in Sentry user context - if (address) { - Sentry.setUser({ - ...user, - wallet: address - }); - } - }, [address]); - const isDisconnected = useMemo(() => { if (isNetworkEVM(selectedNetwork)) { return !evmAccountAddress; @@ -52,9 +42,9 @@ export const useVortexAccount = (forceNetwork?: Networks) => { if (!isNetworkEVM(selectedNetwork)) { return ASSETHUB_CHAIN_ID; } else { - return evmChainId; + return evmWallet.mode === "privy_embedded" ? getNetworkId(selectedNetwork) : externalChainId; } - }, [selectedNetwork, evmChainId]); + }, [selectedNetwork, evmWallet.mode, externalChainId]); const type = useMemo(() => { if (!isNetworkEVM(selectedNetwork)) { @@ -67,7 +57,7 @@ export const useVortexAccount = (forceNetwork?: Networks) => { const getMessageSignature = useCallback( async (siweMessage: string) => { // For now, we only always need to sign with EVM accounts - const signature = await signMessageAsync({ message: siweMessage }); + const signature = await evmWallet.signMessage(siweMessage); // if (isNetworkEVM(selectedNetwork)) { // signature = await signMessageAsync({ message: siweMessage }); @@ -89,16 +79,18 @@ export const useVortexAccount = (forceNetwork?: Networks) => { return signature; }, - [signMessageAsync] + [evmWallet] ); // update the ramp actor with the current context useEffect(() => { - if (rampActor && address) { - rampActor.send({ - address, - type: "SET_ADDRESS" - }); + rampActor?.send({ + address, + type: "SET_ADDRESS" + }); + + if (isNetworkEVM(selectedNetwork)) { + evmWallet.activateSigner(); } if (rampActor && !isNetworkEVM(selectedNetwork) && polkadotWalletAccount) { @@ -112,7 +104,7 @@ export const useVortexAccount = (forceNetwork?: Networks) => { getMessageSignature, type: "SET_GET_MESSAGE_SIGNATURE" }); - }, [address, rampActor, getMessageSignature, polkadotWalletAccount, selectedNetwork]); + }, [address, rampActor, getMessageSignature, polkadotWalletAccount, selectedNetwork, evmWallet]); return { address, // currently selected address diff --git a/apps/frontend/src/machines/ramp.machine.test.ts b/apps/frontend/src/machines/ramp.machine.test.ts index 47412d8bb..f3b14360d 100644 --- a/apps/frontend/src/machines/ramp.machine.test.ts +++ b/apps/frontend/src/machines/ramp.machine.test.ts @@ -767,6 +767,24 @@ describe("rampMachine", () => { }); describe("global events", () => { + it("authenticates before entering the optional embedded-wallet setup and returns to Idle when ready", async () => { + const actor = createRampActor(); + actor.start(); + + actor.send({ type: "REQUEST_EMBEDDED_WALLET" }); + await waitFor(actor, state => state.matches("EmbeddedWallet")); + + actor.send({ + address: "0x4444444444444444444444444444444444444444", + type: "EMBEDDED_WALLET_READY" + }); + expect(actor.getSnapshot().value).toBe("Idle"); + expect(actor.getSnapshot().context.connectedWalletAddress).toBe( + "0x4444444444444444444444444444444444444444" + ); + expect(actor.getSnapshot().context.postAuthTarget).toBeUndefined(); + }); + it("updates the connected wallet address from anywhere", () => { const actor = createRampActor(); actor.start(); diff --git a/apps/frontend/src/machines/ramp.machine.ts b/apps/frontend/src/machines/ramp.machine.ts index f7e51381e..f339e3416 100644 --- a/apps/frontend/src/machines/ramp.machine.ts +++ b/apps/frontend/src/machines/ramp.machine.ts @@ -148,6 +148,12 @@ export const rampMachine = setup({ }), target: "#ramp.EnterEmail" }, + REQUEST_EMBEDDED_WALLET: { + actions: assign({ + postAuthTarget: () => "EmbeddedWallet" + }), + target: ".CheckAuth" + }, RESET_RAMP: { target: ".Resetting" }, @@ -306,6 +312,26 @@ export const rampMachine = setup({ ] } }, + EmbeddedWallet: { + on: { + EMBEDDED_WALLET_FAILED: { + actions: [{ type: "setErrorMessage" }], + target: "Error" + }, + EMBEDDED_WALLET_READY: { + actions: assign({ + connectedWalletAddress: ({ event }) => event.address, + errorMessage: undefined, + postAuthTarget: undefined + }), + target: "Idle" + }, + GO_BACK: { + actions: assign({ postAuthTarget: undefined }), + target: "Idle" + } + } + }, EnterEmail: { on: { ENTER_EMAIL: { @@ -550,6 +576,10 @@ export const rampMachine = setup({ guard: ({ context }) => context.kybLink?.invite !== undefined, target: "RedeemingInvite" }, + { + guard: ({ context }) => context.postAuthTarget === "EmbeddedWallet", + target: "EmbeddedWallet" + }, { guard: ({ context }) => context.postAuthTarget === "RegisterRamp", target: "RegisterRamp" diff --git a/apps/frontend/src/machines/types.ts b/apps/frontend/src/machines/types.ts index 3befa0dc0..aacd1e23a 100644 --- a/apps/frontend/src/machines/types.ts +++ b/apps/frontend/src/machines/types.ts @@ -46,7 +46,7 @@ export interface RampContext { isAuthenticated: boolean; isAuthLoading?: boolean; alfredpayCustomer?: unknown; - postAuthTarget?: "QuoteReady" | "RegisterRamp" | "SelectRegion"; + postAuthTarget?: "EmbeddedWallet" | "QuoteReady" | "RegisterRamp" | "SelectRegion"; // Present only in the quote-less KYB deep-link flow — its presence enables the mode. kybLink?: { customerType?: "individual" | "business"; @@ -95,7 +95,10 @@ export type RampMachineEvents = | { type: "GO_BACK" } | { type: "START_KYB_LINK"; invite?: string; region?: string; locked?: boolean } | { type: "RETRY_INVITE" } - | { type: "SELECT_REGION"; fiatToken: FiatToken }; + | { type: "SELECT_REGION"; fiatToken: FiatToken } + | { type: "REQUEST_EMBEDDED_WALLET" } + | { type: "EMBEDDED_WALLET_READY"; address: string } + | { type: "EMBEDDED_WALLET_FAILED"; error: Error }; export type RampMachineActor = ActorRef, RampMachineEvents>; export type RampMachineSnapshot = SnapshotFrom; diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index b1833fd99..ce13ec75d 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -20,6 +20,7 @@ import { SENTRY_DENY_URLS, SENTRY_IGNORE_ERRORS, sentryBeforeSend } from "./help import { AuthService } from "./services/auth"; import { initializeEvmTokens } from "./services/tokens"; import { wagmiConfig } from "./wagmiConfig"; +import { WidgetWalletProvider } from "./wallets/WidgetWalletProvider"; import "./helpers/googleTranslate"; import { PersistentRampStateProvider } from "./contexts/rampState"; import { routeTree } from "./routeTree.gen"; @@ -107,11 +108,13 @@ createRoot(root, { - - - - - + + + + + + + diff --git a/apps/frontend/src/pages/widget/index.tsx b/apps/frontend/src/pages/widget/index.tsx index af62ae9f1..16f7f0b1b 100644 --- a/apps/frontend/src/pages/widget/index.tsx +++ b/apps/frontend/src/pages/widget/index.tsx @@ -99,7 +99,7 @@ const WidgetContent = () => { isInitialQuoteFailed: state.matches("InitialFetchFailed"), isKybComplete: state.matches("KybLinkComplete"), isKybLinkMode: !!state.context.kybLink, - isLoadingAuthEmail: state.matches("CheckAuth") || state.matches("RedeemingInvite"), + isLoadingAuthEmail: state.matches("CheckAuth") || state.matches("RedeemingInvite") || state.matches("EmbeddedWallet"), isRedirectCallback: state.matches("RedirectCallback"), isSelectRegion: state.matches("SelectRegion"), kybCustomerType: state.context.kybLink?.customerType, diff --git a/apps/frontend/src/services/api/api-client.ts b/apps/frontend/src/services/api/api-client.ts index b8ee31e2f..57579a972 100644 --- a/apps/frontend/src/services/api/api-client.ts +++ b/apps/frontend/src/services/api/api-client.ts @@ -88,7 +88,8 @@ const DOMAIN_BY_SEGMENT: Record = { ramp: SentryDomain.Ramp, recipients: SentryDomain.Ramp, siwe: SentryDomain.Auth, - subsidize: SentryDomain.Ramp + subsidize: SentryDomain.Ramp, + wallets: SentryDomain.Wallet }; // Map an endpoint path to a business domain for Sentry tagging. Unmapped endpoints fall @@ -186,6 +187,7 @@ export const apiClient = { delete: (url: string, config?: { params?: Params }) => apiFetch("DELETE", url, { params: config?.params }), get: (url: string, config?: { params?: Params; signal?: AbortSignal }) => apiFetch("GET", url, { params: config?.params, signal: config?.signal }), + patch: (url: string, data?: unknown) => apiFetch("PATCH", url, { data }), post: (url: string, data?: unknown, config?: { headers?: Record; params?: Params }) => apiFetch("POST", url, { data, headers: config?.headers, params: config?.params }), put: (url: string, data?: unknown) => apiFetch("PUT", url, { data }) diff --git a/apps/frontend/src/services/api/wallets.service.ts b/apps/frontend/src/services/api/wallets.service.ts new file mode 100644 index 000000000..26ab534b8 --- /dev/null +++ b/apps/frontend/src/services/api/wallets.service.ts @@ -0,0 +1,26 @@ +import { apiClient } from "./api-client"; + +export type WalletMode = "external" | "privy_embedded" | null; + +export interface ProfileWallet { + address: `0x${string}`; + chainType: "ethereum"; + createdAt: string; + id: string; + lastUsedAt: string; + provider: "privy"; + providerWalletId: string; + status: "active"; +} + +export interface WalletsResponse { + mode: WalletMode; + wallets: ProfileWallet[]; +} + +export const WalletsService = { + getWallets: (signal?: AbortSignal) => apiClient.get("/wallets", { signal }), + registerPrivyWallet: (input: { address: string; providerWalletId: string }) => + apiClient.post<{ mode: "privy_embedded"; wallet: ProfileWallet }>("/wallets/privy", input), + setMode: (mode: WalletMode) => apiClient.patch<{ mode: WalletMode }>("/wallets/mode", { mode }) +}; diff --git a/apps/frontend/src/services/auth.test.ts b/apps/frontend/src/services/auth.test.ts new file mode 100644 index 000000000..e7f974fec --- /dev/null +++ b/apps/frontend/src/services/auth.test.ts @@ -0,0 +1,25 @@ +// @vitest-environment jsdom +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { AuthService } from "./auth"; + +describe("widget auth session bridge", () => { + beforeEach(() => localStorage.clear()); + + it("notifies Privy subscribers when access tokens change and on logout", () => { + const listener = vi.fn(); + const unsubscribe = AuthService.subscribe(listener); + const tokens = { + accessToken: "access-one", + refreshToken: "refresh-one", + userId: "user-one" + }; + + AuthService.storeTokens(tokens); + AuthService.storeTokens({ ...tokens, accessToken: "access-two" }); + AuthService.clearTokens(); + unsubscribe(); + AuthService.storeTokens(tokens); + + expect(listener).toHaveBeenCalledTimes(3); + }); +}); diff --git a/apps/frontend/src/services/auth.ts b/apps/frontend/src/services/auth.ts index f9f4ca8fb..add966e4d 100644 --- a/apps/frontend/src/services/auth.ts +++ b/apps/frontend/src/services/auth.ts @@ -14,6 +14,18 @@ export class AuthService { private static readonly REFRESH_TOKEN_KEY = "vortex_refresh_token"; private static readonly USER_ID_KEY = "vortex_user_id"; private static readonly USER_EMAIL_KEY = "vortex_user_email"; + private static readonly listeners = new Set<() => void>(); + + private static notifyListeners(): void { + for (const listener of this.listeners) { + listener(); + } + } + + static subscribe(listener: () => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } /** * Store tokens in localStorage @@ -33,6 +45,7 @@ export class AuthService { // Attach the pseudonymous Supabase user id for issue-impact counts. Deliberately no email/IP // so Sentry can count affected users without storing PII. Sentry.setUser({ id: tokens.userId }); + this.notifyListeners(); } /** @@ -60,6 +73,7 @@ export class AuthService { localStorage.removeItem(this.USER_ID_KEY); localStorage.removeItem(this.USER_EMAIL_KEY); Sentry.setUser(null); + this.notifyListeners(); } /** diff --git a/apps/frontend/src/services/transactions/userSigning.ts b/apps/frontend/src/services/transactions/userSigning.ts index 64e2f5e91..d6e4058b5 100644 --- a/apps/frontend/src/services/transactions/userSigning.ts +++ b/apps/frontend/src/services/transactions/userSigning.ts @@ -9,25 +9,20 @@ import { SignedTypedData, UnsignedTx } from "@vortexfi/shared"; -import { getAccount, sendTransaction, signTypedData, switchChain } from "@wagmi/core"; +import { getAddress } from "viem"; import { config } from "../../config"; -import { waitForTransactionConfirmation } from "../../helpers/safe-wallet/waitForTransactionConfirmation"; -import { wagmiConfig } from "../../wagmiConfig"; +import { getActiveEvmWalletSigningAdapter } from "../../wallets/signingAdapter"; import { PolkadotNodeName, polkadotApiService } from "../api/polkadot.service"; /** * Signs multiple typed data objects and returns signature objects */ export async function signMultipleTypedData(typedDataArray: SignedTypedData[]): Promise { + const adapter = getActiveEvmWalletSigningAdapter(); const signedTypedDataArray: SignedTypedData[] = []; for (const typedData of typedDataArray) { - const rawSignature = await signTypedData(wagmiConfig, { - domain: typedData.domain, - message: typedData.message, - primaryType: typedData.primaryType, - types: typedData.types - }); + const rawSignature = await adapter.signTypedData(typedData); const v = parseInt(rawSignature.slice(130, 132), 16); const r = `0x${rawSignature.slice(2, 66)}` as `0x${string}`; @@ -58,73 +53,27 @@ export async function signAndSubmitEvmTransaction(unsignedTx: UnsignedTx): Promi const targetChainId = getNetworkId(network); - const account = getAccount(wagmiConfig); - const originalChainId = account.chainId; - console.log("About to send transaction for phase", unsignedTx.phase); if (!targetChainId) { throw new Error(`Invalid network: ${network}. Unable to determine chain ID.`); } - if (!originalChainId) { - throw new Error("No wallet connected or unable to determine current chain ID."); - } - - const needsNetworkSwitch = originalChainId !== targetChainId; - - if (needsNetworkSwitch) { - console.log(`Switching from chain ${originalChainId} to chain ${targetChainId} for transaction`); - try { - await switchChain(wagmiConfig, { chainId: targetChainId }); - } catch (error) { - console.error("Failed to switch chain:", error); - throw new Error( - `Failed to switch to network ${network} (chainId: ${targetChainId}). Please switch manually and try again.` - ); - } - } - - try { - const gas = BigInt(txData.gas); - const hash = await sendTransaction(wagmiConfig, { - data: txData.data, - ...(gas > 0n ? { gas } : {}), - to: txData.to, - value: BigInt(txData.value) - }); - console.log("Transaction sent", hash); - - const confirmedHash = await waitForTransactionConfirmation(hash, targetChainId); - console.log("Transaction confirmed", confirmedHash); - - // Switch back to original chain if we switched - if (needsNetworkSwitch) { - console.log(`Switching back to original chain ${originalChainId}`); - try { - await switchChain(wagmiConfig, { chainId: originalChainId }); - } catch (error) { - console.warn("Failed to switch back to original chain:", error); - } - } - - return confirmedHash; - } catch (error) { - console.error("Transaction failed:", error); - - if (needsNetworkSwitch) { - console.log(`Switching back to original chain ${originalChainId} after failure`); - try { - await switchChain(wagmiConfig, { chainId: originalChainId }); - console.log(`Successfully switched back to chain ${originalChainId}`); - } catch (switchError) { - console.warn("Failed to switch back to original chain after transaction failure:", switchError); - // Preserve the original error - } - } - - throw error; + const adapter = getActiveEvmWalletSigningAdapter(); + if (getAddress(adapter.address) !== getAddress(unsignedTx.signer)) { + throw new Error("The selected wallet does not match the server-issued transaction signer"); } + const hash = await adapter.sendTransaction({ + chainId: targetChainId, + data: txData.data, + gas: BigInt(txData.gas), + to: txData.to, + value: BigInt(txData.value) + }); + console.log("Transaction sent", hash); + const confirmedHash = await adapter.waitForTransaction(hash, targetChainId); + console.log("Transaction confirmed", confirmedHash); + return confirmedHash; } /// Sign the transaction with the user's connected wallet. The api needs to be for the correct network. diff --git a/apps/frontend/src/stories/providers/MockProviders.tsx b/apps/frontend/src/stories/providers/MockProviders.tsx index 1c8367d4b..b66531b03 100644 --- a/apps/frontend/src/stories/providers/MockProviders.tsx +++ b/apps/frontend/src/stories/providers/MockProviders.tsx @@ -8,6 +8,7 @@ import { PolkadotNodeProvider } from "../../contexts/polkadotNode"; import { PolkadotWalletStateProvider } from "../../contexts/polkadotWallet"; import { PersistentRampStateProvider } from "../../contexts/rampState"; import { wagmiConfig } from "../../wagmiConfig"; +import { WidgetWalletProvider } from "../../wallets/WidgetWalletProvider"; // RouterProvider renders the route tree and doesn't accept children directly. // We pass inner content through a module-level context so the root route component can render it. @@ -54,13 +55,15 @@ export const MockProviders = ({ children }: PropsWithChildren) => { - - - - {children} - - - + + + + + {children} + + + + diff --git a/apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx b/apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx new file mode 100644 index 000000000..c67b98cef --- /dev/null +++ b/apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx @@ -0,0 +1,230 @@ +import { + type LinkedAccountWithMetadata, + PrivyProvider, + useCreateWallet, + useExportWallet, + usePrivy, + useSendTransaction, + useSignMessage, + useSignTypedData, + useSyncJwtBasedAuthState, + useWallets +} from "@privy-io/react-auth"; +import { useQueryClient } from "@tanstack/react-query"; +import { useSelector } from "@xstate/react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { hexToBytes } from "viem"; +import { useRampActor } from "../contexts/rampState"; +import { waitForTransactionConfirmation } from "../helpers/safe-wallet/waitForTransactionConfirmation"; +import { ProfileWallet, WalletMode, WalletsResponse, WalletsService } from "../services/api/wallets.service"; +import { AuthService } from "../services/auth"; +import { privyWidgetConfig } from "./config"; +import { EvmWalletSigningAdapter, setActiveEvmWalletSigningAdapter } from "./signingAdapter"; +import { WidgetEvmWallet, WidgetWalletContext } from "./WidgetWalletContext"; + +interface PrivyWidgetWalletRuntimeProps { + children: React.ReactNode; + connectExternalWallet: () => Promise; + mode: WalletMode; + onModeChange: (mode: WalletMode, wallet?: ProfileWallet) => void; +} + +interface PrivyWidgetWalletProviderRuntimeProps extends PrivyWidgetWalletRuntimeProps { + appId: string; + clientId?: string; +} + +function isPrivyEmbeddedWallet(wallet: { type: string; walletClientType?: string }): boolean { + return wallet.type === "ethereum" && (wallet.walletClientType === "privy" || wallet.walletClientType === "privy-v2"); +} + +export function PrivyWidgetWalletRuntime({ + children, + connectExternalWallet, + mode, + onModeChange +}: PrivyWidgetWalletRuntimeProps) { + const rampActor = useRampActor(); + const walletSetupRequested = useSelector(rampActor, state => state.matches("EmbeddedWallet")); + const queryClient = useQueryClient(); + const { createWallet } = useCreateWallet(); + const { exportWallet } = useExportWallet(); + const { user: privyUser } = usePrivy(); + const { sendTransaction } = useSendTransaction(); + const { signMessage } = useSignMessage(); + const { signTypedData } = useSignTypedData(); + const { ready: walletsReady, wallets } = useWallets(); + const [creating, setCreating] = useState(false); + + const getExternalJwt = useCallback(async () => AuthService.getTokens()?.accessToken, []); + const subscribeToAuth = useCallback((onChange: () => void) => AuthService.subscribe(onChange), []); + const { state: authState } = useSyncJwtBasedAuthState({ + enabled: AuthService.isAuthenticated(), + getExternalJwt, + subscribe: subscribeToAuth + }); + + const embeddedWallet = wallets.find(isPrivyEmbeddedWallet); + const address = embeddedWallet?.address as `0x${string}` | undefined; + const linkedEmbeddedWallet = privyUser?.linkedAccounts.find( + (account): account is Extract => + account.type === "wallet" && + account.chainType === "ethereum" && + (account.walletClientType === "privy" || account.walletClientType === "privy-v2") && + account.address.toLowerCase() === address?.toLowerCase() + ); + + const signingAdapter = useMemo(() => { + if (!address) return null; + return { + address, + kind: "privy_embedded", + sendTransaction: async transaction => { + const result = await sendTransaction( + { + chainId: transaction.chainId, + data: transaction.data, + gasLimit: transaction.gas, + to: transaction.to, + value: transaction.value + }, + { address, sponsor: privyWidgetConfig.gasPolicy === "sponsored" } + ); + return result.hash; + }, + signTypedData: async typedData => { + const domain = { + ...(typedData.domain.chainId !== undefined ? { chainId: Number(typedData.domain.chainId) } : {}), + ...(typedData.domain.name ? { name: typedData.domain.name } : {}), + ...(typedData.domain.salt ? { salt: Uint8Array.from(hexToBytes(typedData.domain.salt)).buffer } : {}), + ...(typedData.domain.verifyingContract ? { verifyingContract: typedData.domain.verifyingContract } : {}), + ...(typedData.domain.version ? { version: typedData.domain.version } : {}) + }; + const result = await signTypedData( + { + domain, + message: typedData.message, + primaryType: typedData.primaryType, + types: typedData.types + }, + { address } + ); + return result.signature as `0x${string}`; + }, + waitForTransaction: waitForTransactionConfirmation + }; + }, [address, sendTransaction, signTypedData]); + + const register = useCallback( + async (wallet: { address: string; id?: string | null }) => { + if (!wallet.id) throw new Error("Privy did not return an embedded wallet ID"); + const response = await WalletsService.registerPrivyWallet({ + address: wallet.address, + providerWalletId: wallet.id + }); + queryClient.setQueryData(["wallets", AuthService.getUserId()], current => ({ + mode: response.mode, + wallets: current?.wallets.some(item => item.id === response.wallet.id) + ? (current.wallets ?? []) + : [...(current?.wallets ?? []), response.wallet] + })); + onModeChange(response.mode, response.wallet); + rampActor.send({ address: response.wallet.address, type: "EMBEDDED_WALLET_READY" }); + }, + [onModeChange, queryClient, rampActor] + ); + + const createEmbeddedWallet = useCallback(async () => { + setCreating(true); + try { + if (embeddedWallet && linkedEmbeddedWallet) { + await register(linkedEmbeddedWallet); + } else { + await register(await createWallet()); + } + } catch (cause) { + rampActor.send({ + error: cause instanceof Error ? cause : new Error("Could not create the embedded wallet"), + type: "EMBEDDED_WALLET_FAILED" + }); + } finally { + setCreating(false); + } + }, [createWallet, embeddedWallet, linkedEmbeddedWallet, rampActor, register]); + + const autoCreateStarted = useRef(false); + useEffect(() => { + if (!walletSetupRequested) { + autoCreateStarted.current = false; + } + }, [walletSetupRequested]); + + useEffect(() => { + if (!walletSetupRequested || autoCreateStarted.current || !walletsReady || authState.status !== "done") { + return; + } + autoCreateStarted.current = true; + void createEmbeddedWallet(); + }, [authState.status, createEmbeddedWallet, walletSetupRequested, walletsReady]); + + const switchToExternalWallet = useCallback(async () => { + await WalletsService.setMode("external"); + onModeChange("external"); + await connectExternalWallet(); + }, [connectExternalWallet, onModeChange]); + + const value = useMemo( + () => ({ + activateSigner: () => setActiveEvmWalletSigningAdapter(signingAdapter), + address, + canUseEmbeddedWallet: true, + connectExternalWallet, + connected: Boolean(address), + createEmbeddedWallet: () => rampActor.send({ type: "REQUEST_EMBEDDED_WALLET" }), + creatingEmbeddedWallet: creating, + exportEmbeddedWallet: async () => { + if (!address) throw new Error("No embedded wallet is available to export"); + await exportWallet({ address }); + }, + mode, + ready: walletsReady, + signMessage: async message => { + if (!address) throw new Error("The embedded wallet is not ready"); + const result = await signMessage({ message }, { address }); + return result.signature as `0x${string}`; + }, + switchToExternalWallet + }), + [ + address, + connectExternalWallet, + creating, + exportWallet, + mode, + rampActor, + signMessage, + signingAdapter, + switchToExternalWallet, + walletsReady + ] + ); + + return {children}; +} + +export function PrivyWidgetWalletProviderRuntime({ appId, clientId, ...runtimeProps }: PrivyWidgetWalletProviderRuntimeProps) { + return ( + + + + ); +} diff --git a/apps/frontend/src/wallets/WidgetWalletContext.ts b/apps/frontend/src/wallets/WidgetWalletContext.ts new file mode 100644 index 000000000..e2d660de3 --- /dev/null +++ b/apps/frontend/src/wallets/WidgetWalletContext.ts @@ -0,0 +1,26 @@ +import { createContext, use } from "react"; +import { WalletMode } from "../services/api/wallets.service"; + +export interface WidgetEvmWallet { + activateSigner: () => void; + address?: `0x${string}`; + canUseEmbeddedWallet: boolean; + connectExternalWallet: () => Promise; + connected: boolean; + createEmbeddedWallet: () => void; + creatingEmbeddedWallet: boolean; + embeddedUnavailableReason?: string; + exportEmbeddedWallet: () => Promise; + mode: WalletMode; + ready: boolean; + signMessage: (message: string) => Promise<`0x${string}`>; + switchToExternalWallet: () => Promise; +} + +export const WidgetWalletContext = createContext(null); + +export function useWidgetWallet(): WidgetEvmWallet { + const value = use(WidgetWalletContext); + if (!value) throw new Error("useWidgetWallet must be used inside WidgetWalletProvider"); + return value; +} diff --git a/apps/frontend/src/wallets/WidgetWalletProvider.tsx b/apps/frontend/src/wallets/WidgetWalletProvider.tsx new file mode 100644 index 000000000..bdb549068 --- /dev/null +++ b/apps/frontend/src/wallets/WidgetWalletProvider.tsx @@ -0,0 +1,128 @@ +import { useAppKit, useAppKitAccount } from "@reown/appkit/react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useSelector } from "@xstate/react"; +import { lazy, Suspense, useCallback, useMemo, useState, useSyncExternalStore } from "react"; +import { useAccount, useSignMessage } from "wagmi"; +import { useRampActor } from "../contexts/rampState"; +import { WalletMode, WalletsResponse, WalletsService } from "../services/api/wallets.service"; +import { AuthService } from "../services/auth"; +import { isPrivyEnabledForCurrentFrame, isPrivyProvisioningEnabledForCurrentFrame, privyWidgetConfig } from "./config"; +import { createExternalSigningAdapter } from "./externalSigningAdapter"; +import { setActiveEvmWalletSigningAdapter } from "./signingAdapter"; +import { WidgetEvmWallet, WidgetWalletContext } from "./WidgetWalletContext"; + +const LazyPrivyWidgetWalletRuntime = lazy(async () => { + const module = await import("./PrivyWidgetWalletRuntime"); + return { default: module.PrivyWidgetWalletProviderRuntime }; +}); + +function accessTokenSnapshot(): string { + return AuthService.getTokens()?.accessToken ?? ""; +} + +function subscribeToAuth(listener: () => void): () => void { + return AuthService.subscribe(listener); +} + +export function WidgetWalletProvider({ children }: { children: React.ReactNode }) { + const rampActor = useRampActor(); + const walletSetupRequested = useSelector(rampActor, state => state.matches("EmbeddedWallet")); + const accessToken = useSyncExternalStore(subscribeToAuth, accessTokenSnapshot, () => ""); + const userId = AuthService.getUserId(); + const authenticated = accessToken.length > 0 && Boolean(userId); + const queryClient = useQueryClient(); + const { address } = useAccount(); + const evmAddress = address as `0x${string}` | undefined; + const { isConnected } = useAppKitAccount(); + const { signMessageAsync } = useSignMessage(); + const { open } = useAppKit(); + const [pendingMode, setPendingMode] = useState(); + + const walletsQuery = useQuery({ + enabled: authenticated, + queryFn: ({ signal }) => WalletsService.getWallets(signal), + queryKey: ["wallets", userId], + staleTime: 30_000 + }); + + const storedMode = pendingMode ?? walletsQuery.data?.mode ?? null; + const mode = storedMode === "privy_embedded" && !isPrivyEnabledForCurrentFrame ? "external" : storedMode; + const embeddedActive = + isPrivyEnabledForCurrentFrame && authenticated && (storedMode === "privy_embedded" || walletSetupRequested); + + const connectExternalWallet = useCallback(async () => { + if (storedMode === "privy_embedded") { + const response = await WalletsService.setMode("external"); + setPendingMode(response.mode); + queryClient.setQueryData(["wallets", userId], current => ({ + mode: response.mode, + wallets: current?.wallets ?? [] + })); + } + await open({ view: "Connect" }); + }, [open, queryClient, storedMode, userId]); + + const onModeChange = useCallback( + (nextMode: WalletMode) => { + setPendingMode(nextMode); + queryClient.setQueryData(["wallets", userId], current => ({ + mode: nextMode, + wallets: current?.wallets ?? [] + })); + }, + [queryClient, userId] + ); + + const externalAdapter = useMemo(() => (evmAddress ? createExternalSigningAdapter(evmAddress) : null), [evmAddress]); + const externalValue = useMemo( + () => ({ + activateSigner: () => setActiveEvmWalletSigningAdapter(externalAdapter), + address: evmAddress, + canUseEmbeddedWallet: isPrivyProvisioningEnabledForCurrentFrame, + connectExternalWallet, + connected: isConnected && Boolean(evmAddress), + createEmbeddedWallet: () => rampActor.send({ type: "REQUEST_EMBEDDED_WALLET" }), + creatingEmbeddedWallet: walletSetupRequested, + embeddedUnavailableReason: + privyWidgetConfig.enabled && !isPrivyEnabledForCurrentFrame + ? "Open this flow on Vortex to create or use an embedded wallet." + : undefined, + exportEmbeddedWallet: async () => { + throw new Error("Select your embedded wallet before exporting it"); + }, + mode, + ready: !walletsQuery.isLoading, + signMessage: message => signMessageAsync({ message }), + switchToExternalWallet: connectExternalWallet + }), + [ + evmAddress, + connectExternalWallet, + externalAdapter, + isConnected, + mode, + rampActor, + signMessageAsync, + walletSetupRequested, + walletsQuery.isLoading + ] + ); + + if (embeddedActive) { + return ( + + + {children} + + + ); + } + + return {children}; +} diff --git a/apps/frontend/src/wallets/config.test.ts b/apps/frontend/src/wallets/config.test.ts new file mode 100644 index 000000000..db52c8d28 --- /dev/null +++ b/apps/frontend/src/wallets/config.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { isPrivyOriginAllowed, readPrivyWidgetConfig } from "./config"; + +function env(values: Record): ImportMetaEnv { + return values as ImportMetaEnv; +} + +describe("widget Privy configuration", () => { + it("stays disabled without both the explicit flag and app ID", () => { + expect(readPrivyWidgetConfig(env({})).enabled).toBe(false); + expect(readPrivyWidgetConfig(env({ VITE_PRIVY_ENABLED: "true" })).enabled).toBe(false); + }); + + it("normalizes and deduplicates exact allowed parent origins", () => { + expect( + readPrivyWidgetConfig( + env({ + VITE_PRIVY_APP_ID: "app-test", + VITE_PRIVY_ENABLED: "true", + VITE_PRIVY_PROVISIONING_ENABLED: "true", + VITE_PRIVY_WIDGET_PARENT_ORIGINS: + "https://partner.example/path, https://partner.example,not-a-url,https://vortex.example" + }) + ) + ).toMatchObject({ + allowedParentOrigins: ["https://partner.example", "https://vortex.example"], + enabled: true, + gasPolicy: "user_pays", + provisioningEnabled: true + }); + }); + + it("allows top-level pages and known iframe parents but rejects unknown or referrer-less parents", () => { + const config = { allowedParentOrigins: ["https://partner.example"] }; + expect(isPrivyOriginAllowed(config, { isTopLevel: true, referrer: "" })).toBe(true); + expect( + isPrivyOriginAllowed(config, { + isTopLevel: false, + referrer: "https://partner.example/checkout" + }) + ).toBe(true); + expect( + isPrivyOriginAllowed(config, { + isTopLevel: false, + referrer: "https://unknown.example/checkout" + }) + ).toBe(false); + expect(isPrivyOriginAllowed(config, { isTopLevel: false, referrer: "" })).toBe(false); + }); +}); diff --git a/apps/frontend/src/wallets/config.ts b/apps/frontend/src/wallets/config.ts new file mode 100644 index 000000000..a2a956771 --- /dev/null +++ b/apps/frontend/src/wallets/config.ts @@ -0,0 +1,69 @@ +export type PrivyGasPolicy = "sponsored" | "user_pays"; + +export interface PrivyWidgetConfig { + allowedParentOrigins: string[]; + appId: string; + clientId?: string; + enabled: boolean; + gasPolicy: PrivyGasPolicy; + provisioningEnabled: boolean; +} + +function normalizeOrigin(value: string): string | undefined { + try { + return new URL(value.trim()).origin; + } catch { + return undefined; + } +} + +export function readPrivyWidgetConfig(env: ImportMetaEnv = import.meta.env): PrivyWidgetConfig { + const appId = env.VITE_PRIVY_APP_ID?.trim() ?? ""; + const rawParentOrigins: string = env.VITE_PRIVY_WIDGET_PARENT_ORIGINS ?? ""; + const allowedParentOrigins = rawParentOrigins + .split(",") + .map(normalizeOrigin) + .filter((origin: string | undefined): origin is string => Boolean(origin)); + + const isEnabled = env.VITE_PRIVY_ENABLED?.trim().toLowerCase() === "true" && appId.length > 0; + return { + allowedParentOrigins: [...new Set(allowedParentOrigins)], + appId, + clientId: env.VITE_PRIVY_CLIENT_ID?.trim() || undefined, + enabled: isEnabled, + gasPolicy: env.VITE_PRIVY_GAS_POLICY === "sponsored" ? "sponsored" : "user_pays", + provisioningEnabled: isEnabled && env.VITE_PRIVY_PROVISIONING_ENABLED?.trim().toLowerCase() === "true" + }; +} + +export function isPrivyOriginAllowed( + config: Pick, + frame: { isTopLevel: boolean; referrer: string } +): boolean { + if (frame.isTopLevel) return true; + if (!frame.referrer) return false; + try { + return config.allowedParentOrigins.includes(new URL(frame.referrer).origin); + } catch { + return false; + } +} + +function browserFrame(): { isTopLevel: boolean; referrer: string } { + if (typeof window === "undefined" || typeof document === "undefined") { + return { isTopLevel: true, referrer: "" }; + } + let isTopLevel = false; + try { + isTopLevel = window.self === window.top; + } catch { + isTopLevel = false; + } + return { isTopLevel, referrer: document.referrer }; +} + +export const privyWidgetConfig = readPrivyWidgetConfig(); +export const isPrivyEnabledForCurrentFrame = + privyWidgetConfig.enabled && isPrivyOriginAllowed(privyWidgetConfig, browserFrame()); +export const isPrivyProvisioningEnabledForCurrentFrame = + privyWidgetConfig.provisioningEnabled && isPrivyOriginAllowed(privyWidgetConfig, browserFrame()); diff --git a/apps/frontend/src/wallets/externalSigningAdapter.ts b/apps/frontend/src/wallets/externalSigningAdapter.ts new file mode 100644 index 000000000..b688914de --- /dev/null +++ b/apps/frontend/src/wallets/externalSigningAdapter.ts @@ -0,0 +1,61 @@ +import { SignedTypedData } from "@vortexfi/shared"; +import { getAccount, sendTransaction, signTypedData, switchChain } from "@wagmi/core"; +import { waitForTransactionConfirmation } from "../helpers/safe-wallet/waitForTransactionConfirmation"; +import { wagmiConfig } from "../wagmiConfig"; +import { EvmWalletSigningAdapter, WalletTransactionRequest } from "./signingAdapter"; + +export function createExternalSigningAdapter(address: `0x${string}`): EvmWalletSigningAdapter { + const originalChainByHash = new Map<`0x${string}`, number>(); + return { + address, + kind: "external", + sendTransaction: async (transaction: WalletTransactionRequest) => { + const account = getAccount(wagmiConfig); + if (!account.chainId) { + throw new Error("No wallet connected or unable to determine current chain ID."); + } + const originalChainId = account.chainId; + const switched = originalChainId !== transaction.chainId; + if (switched) { + try { + await switchChain(wagmiConfig, { chainId: transaction.chainId }); + } catch { + throw new Error(`Failed to switch to chain ${transaction.chainId}. Please switch manually and try again.`); + } + } + try { + const hash = await sendTransaction(wagmiConfig, { + data: transaction.data, + ...(transaction.gas && transaction.gas > 0n ? { gas: transaction.gas } : {}), + to: transaction.to, + value: transaction.value + }); + if (switched) originalChainByHash.set(hash, originalChainId); + return hash; + } catch (error) { + if (switched) { + await switchChain(wagmiConfig, { chainId: originalChainId }).catch(() => undefined); + } + throw error; + } + }, + signTypedData: (typedData: SignedTypedData) => + signTypedData(wagmiConfig, { + domain: typedData.domain, + message: typedData.message, + primaryType: typedData.primaryType, + types: typedData.types + }), + waitForTransaction: async (hash, chainId) => { + try { + return await waitForTransactionConfirmation(hash, chainId); + } finally { + const originalChainId = originalChainByHash.get(hash); + originalChainByHash.delete(hash); + if (originalChainId !== undefined) { + await switchChain(wagmiConfig, { chainId: originalChainId }).catch(() => undefined); + } + } + } + }; +} diff --git a/apps/frontend/src/wallets/signingAdapter.ts b/apps/frontend/src/wallets/signingAdapter.ts new file mode 100644 index 000000000..856e0eb80 --- /dev/null +++ b/apps/frontend/src/wallets/signingAdapter.ts @@ -0,0 +1,31 @@ +import { SignedTypedData } from "@vortexfi/shared"; +import type { Hex } from "viem"; + +export interface WalletTransactionRequest { + chainId: number; + data: Hex; + gas?: bigint; + to: Hex; + value: bigint; +} + +export interface EvmWalletSigningAdapter { + address: Hex; + kind: "external" | "privy_embedded"; + sendTransaction: (transaction: WalletTransactionRequest) => Promise; + signTypedData: (typedData: SignedTypedData) => Promise; + waitForTransaction: (hash: Hex, chainId: number) => Promise; +} + +let activeAdapter: EvmWalletSigningAdapter | null = null; + +export function setActiveEvmWalletSigningAdapter(adapter: EvmWalletSigningAdapter | null): void { + activeAdapter = adapter; +} + +export function getActiveEvmWalletSigningAdapter(): EvmWalletSigningAdapter { + if (!activeAdapter) { + throw new Error("The selected EVM wallet is not ready to sign"); + } + return activeAdapter; +} diff --git a/apps/frontend/src/wallets/walletSigning.contract.test.ts b/apps/frontend/src/wallets/walletSigning.contract.test.ts new file mode 100644 index 000000000..90f94fee4 --- /dev/null +++ b/apps/frontend/src/wallets/walletSigning.contract.test.ts @@ -0,0 +1,99 @@ +import { Networks, type SignedTypedData, type UnsignedTx } from "@vortexfi/shared"; +import { describe, expect, it } from "vitest"; +import { signAndSubmitEvmTransaction, signMultipleTypedData } from "../services/transactions/userSigning"; +import { type EvmWalletSigningAdapter, setActiveEvmWalletSigningAdapter } from "./signingAdapter"; + +const address = "0x1111111111111111111111111111111111111111"; +const txHash = `0x${"cd".repeat(32)}` as `0x${string}`; +const confirmedHash = `0x${"ef".repeat(32)}` as `0x${string}`; +const rawSignature = `0x${"11".repeat(64)}1b` as `0x${string}`; + +const typedData: SignedTypedData = { + domain: { + name: "Permit2", + verifyingContract: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + version: "1" + }, + message: { amount: "1", deadline: "123" }, + primaryType: "PermitTransferFrom", + types: { PermitTransferFrom: [{ name: "amount", type: "uint256" }] } +}; + +const unsignedTx = { + meta: {}, + network: Networks.Base, + nonce: 0, + phase: "squidRouterNoPermitTransfer", + signer: address, + txData: { + data: "0x1234", + gas: "21000", + nonce: 0, + to: "0x2222222222222222222222222222222222222222", + value: "7" + } +} as UnsignedTx; + +function fakeAdapter(kind: EvmWalletSigningAdapter["kind"]) { + const calls: Array<{ name: string; value: unknown }> = []; + const adapter: EvmWalletSigningAdapter = { + address, + kind, + sendTransaction: async transaction => { + calls.push({ name: "sendTransaction", value: transaction }); + return txHash; + }, + signTypedData: async data => { + calls.push({ name: "signTypedData", value: data }); + return rawSignature; + }, + waitForTransaction: async (hash, chainId) => { + calls.push({ name: "waitForTransaction", value: { chainId, hash } }); + return confirmedHash; + } + }; + return { adapter, calls }; +} + +describe("widget wallet signer contract", () => { + for (const kind of ["external", "privy_embedded"] as const) { + it(`${kind} produces the same permit and transaction result shapes`, async () => { + const fake = fakeAdapter(kind); + setActiveEvmWalletSigningAdapter(fake.adapter); + + const [signed] = await signMultipleTypedData([typedData]); + const hash = await signAndSubmitEvmTransaction(unsignedTx); + + expect(signed?.signature).toEqual({ + deadline: 123, + r: `0x${"11".repeat(32)}`, + s: `0x${"11".repeat(32)}`, + v: 27 + }); + expect(hash).toBe(confirmedHash); + expect(fake.calls.map(call => call.name)).toEqual([ + "signTypedData", + "sendTransaction", + "waitForTransaction" + ]); + expect(fake.calls[1]?.value).toMatchObject({ + chainId: 8453, + gas: 21000n, + value: 7n + }); + }); + } + + it("rejects a transaction for a different signer before broadcasting", async () => { + const fake = fakeAdapter("privy_embedded"); + setActiveEvmWalletSigningAdapter(fake.adapter); + + await expect( + signAndSubmitEvmTransaction({ + ...unsignedTx, + signer: "0x2222222222222222222222222222222222222222" + }) + ).rejects.toThrow("does not match the server-issued transaction signer"); + expect(fake.calls).toHaveLength(0); + }); +}); From 02556d41047b1310d85745b6ab3619d34acfbf3d Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 27 Jul 2026 19:10:54 +0200 Subject: [PATCH 06/16] docs(repo): document Privy wallet security and rollout --- docs/architecture/ramp-machine-widget-flow.md | 38 ++++- .../privy-embedded-wallet-rollout.md | 117 ++++++++++++++ .../01-auth/privy-embedded-wallets.md | 143 ++++++++++++++++++ docs/security-spec/01-auth/supabase-otp.md | 9 ++ 4 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 docs/operations/privy-embedded-wallet-rollout.md create mode 100644 docs/security-spec/01-auth/privy-embedded-wallets.md diff --git a/docs/architecture/ramp-machine-widget-flow.md b/docs/architecture/ramp-machine-widget-flow.md index b5d0cb53c..da8dec8b0 100644 --- a/docs/architecture/ramp-machine-widget-flow.md +++ b/docs/architecture/ramp-machine-widget-flow.md @@ -46,6 +46,7 @@ flowchart TD R -->|urlCleaner done| A AA -->|authenticated + postAuthTarget=RegisterRamp| G + AA -->|authenticated + postAuthTarget=EmbeddedWallet| EW[EmbeddedWallet] AA -->|authenticated + postAuthTarget=QuoteReady| C AA -->|authenticated| C AA -->|not authenticated| AB[EnterEmail] @@ -54,9 +55,14 @@ flowchart TD AD --> AE[EnterOTP] AE --> AF[VerifyingOTP] AF -->|success + postAuthTarget=RegisterRamp| G + AF -->|success + postAuthTarget=EmbeddedWallet| EW AF -->|success otherwise| C AF -->|error| AE + A -->|REQUEST_EMBEDDED_WALLET| AA + EW -->|EMBEDDED_WALLET_READY| A + EW -->|EMBEDDED_WALLET_FAILED| Z + G -->|error| Z[Error] H -->|error| Z I -->|error| Z @@ -67,16 +73,17 @@ flowchart TD 1. `ErrorStep` if machine matches `Error` 2. `RampFollowUpRedirectStep` if machine matches `RedirectCallback` -3. `AuthEmailStep` for `CheckAuth | EnterEmail | CheckingEmail | RequestingOTP` -4. `AuthOTPStep` for `EnterOTP | VerifyingOTP` -5. `MoneriumRedirectStep` if Monerium child actor exists and child state is `Redirect` -6. `SummaryStep` for `KycComplete | RegisterRamp | UpdateRamp | StartRamp` -7. Avenia branch if Avenia child actor exists: +3. Loading card for `EmbeddedWallet` +4. `AuthEmailStep` for `CheckAuth | EnterEmail | CheckingEmail | RequestingOTP` +5. `AuthOTPStep` for `EnterOTP | VerifyingOTP` +6. `MoneriumRedirectStep` if Monerium child actor exists and child state is `Redirect` +7. `SummaryStep` for `KycComplete | RegisterRamp | UpdateRamp | StartRamp` +8. Avenia branch if Avenia child actor exists: - `AveniaKYBFlow` when CNPJ + `kybUrls` present - else `AveniaKYBForm` (CNPJ) - else `AveniaKYCForm` (CPF) -8. `InitialQuoteFailedStep` for `InitialFetchFailed` -9. fallback: `DetailsStep` +9. `InitialQuoteFailedStep` for `InitialFetchFailed` +10. fallback: `DetailsStep` ## KYC subflow and cards ```mermaid @@ -108,6 +115,7 @@ flowchart TD |---|---| | `Error` | `ErrorStep` | | `RedirectCallback` | `RampFollowUpRedirectStep` | +| `EmbeddedWallet` | Loading card while the authenticated Privy wallet is created and server-verified | | `CheckAuth`, `EnterEmail`, `CheckingEmail`, `RequestingOTP` | `AuthEmailStep` | | `EnterOTP`, `VerifyingOTP` | `AuthOTPStep` | | Monerium child actor state `Redirect` | `MoneriumRedirectStep` | @@ -128,6 +136,9 @@ flowchart TD - if quote expired -> `RESET_RAMP` - `AuthEmailStep` -> `ENTER_EMAIL` - `AuthOTPStep` -> `VERIFY_OTP` +- `EVMWalletButton` embedded option -> `REQUEST_EMBEDDED_WALLET` +- Privy wallet registration success -> `EMBEDDED_WALLET_READY` +- Privy authentication, creation, or registration failure -> `EMBEDDED_WALLET_FAILED` - Error/initial-failure/retry actions -> `RESET_RAMP` - Back button (`StepBackButton`) primarily sends `GO_BACK` (with Avenia-specific child events in document/liveness/KYB sub-steps) @@ -145,10 +156,23 @@ This is why many sessions start in `LoadingQuote`/`QuoteReady` rather than plain - For `/widget` entry coming from Quote form (`enteredViaForm`), auth can happen directly after `LoadingQuote` and before `QuoteReady`. - Auth is also deferred to `KycComplete -> PROCEED_TO_REGISTRATION` when needed. - `postAuthTarget` tracks whether post-auth continuation should be `QuoteReady` or `RegisterRamp`. +- `postAuthTarget=EmbeddedWallet` preserves the explicit embedded-wallet request across OTP and continues to the + provisioning state only after authentication. - `GO_BACK` behavior in auth states: - `CheckAuth`, `EnterEmail`, `CheckingEmail`, `RequestingOTP`: back to `KycComplete` when `postAuthTarget=RegisterRamp`, otherwise reset to `Idle` (Quote form path). - `EnterOTP`, `VerifyingOTP`: back to `EnterEmail`. +## Optional wallet selection + +- External Reown/Wagmi remains available and does not initialize Privy. +- The embedded option is shown only when both the base and provisioning flags are enabled. +- `EmbeddedWallet` is entered only after the user explicitly chooses that option. +- Successful creation is not enough: the API verifies ownership and persists the wallet before + `EMBEDDED_WALLET_READY`. +- Privy supports only the EVM branch. AssetHub and other Polkadot paths keep their existing connection/signing logic. +- In an iframe, the embedded option is available only for an exact allowed parent origin. Unknown ancestry fails + closed and leaves the existing-wallet flow available. + ## Practical reading model When debugging what card should show, check in this order: 1. Top-level ramp state (`rampActor.getSnapshot().value`) diff --git a/docs/operations/privy-embedded-wallet-rollout.md b/docs/operations/privy-embedded-wallet-rollout.md new file mode 100644 index 000000000..510d461c3 --- /dev/null +++ b/docs/operations/privy-embedded-wallet-rollout.md @@ -0,0 +1,117 @@ +# Privy Embedded Wallet Rollout + +This runbook enables the optional Privy path without changing the existing external-wallet experience. Apply it +independently in local, staging, and production; use separate Privy clients or apps and secrets for each environment. + +## Prerequisites + +1. Create or select a Privy application. +2. In Privy, enable client-side JWT-based authentication for the Supabase project used by the environment. +3. Register the Supabase JWKS endpoint and use `sub` as the user ID claim: + + ```text + https://.supabase.co/auth/v1/.well-known/jwks.json + ``` + + Confirm the Supabase project is using an asymmetric signing key exposed by that endpoint before rollout. +4. Add exact Vortex client origins to Privy's allowed origins. Do not use broad hosting-provider wildcards for + production previews. +5. Decide which exact third-party origins may embed the widget. Each must be present both in Privy's allowed origins + and in `VITE_PRIVY_WIDGET_PARENT_ORIGINS`. +6. Put the Privy app secret only in the API secret manager. Never place it in either Vite environment. + +## Configuration + +API: + +```text +PRIVY_WALLET_REGISTRATION_ENABLED=false +PRIVY_APP_ID= +PRIVY_APP_SECRET= +``` + +Dashboard: + +```text +VITE_PRIVY_ENABLED=false +VITE_PRIVY_APP_ID= +VITE_PRIVY_CLIENT_ID= +VITE_PRIVY_PROVISIONING_ENABLED=false +VITE_PRIVY_ONRAMP_ENABLED=false +VITE_PRIVY_OFFRAMP_ENABLED=false +VITE_PRIVY_GAS_POLICY=user_pays +``` + +Widget: + +```text +VITE_PRIVY_ENABLED=false +VITE_PRIVY_APP_ID= +VITE_PRIVY_CLIENT_ID= +VITE_PRIVY_PROVISIONING_ENABLED=false +VITE_PRIVY_GAS_POLICY=user_pays +VITE_PRIVY_WIDGET_PARENT_ORIGINS=https://trusted-parent.example +``` + +The client requires `VITE_PRIVY_ENABLED=true` and a nonempty app ID before mounting Privy. Provisioning additionally +requires `VITE_PRIVY_PROVISIONING_ENABLED=true`. Existing embedded-wallet users can still restore their wallet while +new provisioning is disabled, provided the base feature remains enabled. + +## Staged Enablement + +1. Deploy database migrations and API code with every flag off. +2. Set API credentials, enable `PRIVY_WALLET_REGISTRATION_ENABLED`, and verify API startup. +3. Enable base Privy support for an internal staging origin, but keep provisioning off. Confirm external wallets and + all Polkadot/AssetHub flows are unchanged. +4. Enable provisioning for internal accounts. Test create, refresh, logout/login restore, export, and switch back to + an external wallet. +5. Enable the dashboard onramp destination flag and run supported EVM onramp scenarios. +6. Enable the dashboard offramp flag and run all EVM signing phases, including rejection, insufficient balance, and + network switching. +7. Enable approved widget parents one at a time. Confirm an unlisted or referrer-less iframe shows the safe fallback + and never initializes Privy. +8. Roll out to a small production cohort before general availability. +9. Keep `VITE_PRIVY_GAS_POLICY=user_pays` until a separate gas-sponsorship review is complete. + +## Credentialed Smoke Test + +Use a non-production test user and low-value testnet funds: + +- authenticate with Vortex OTP and choose the embedded option; +- verify exactly one wallet is created and registered; +- refresh and sign out/in, then verify the same address returns; +- export the selected wallet and verify the address after importing it into an independent client; +- submit each supported EVM signing phase and compare the request/transaction shape with an external wallet; +- attempt to register a mismatched wallet ID/address and confirm it is rejected; +- start a ramp, attempt a mode change, and confirm it is rejected; +- load the widget top-level, in an allowed parent, in a disallowed parent, and in a referrer-less sandbox; +- verify no Privy UI is offered for AssetHub or other Polkadot paths; +- inspect Sentry, API logs, and analytics for wallet identifiers or sensitive signing material. + +## Monitoring + +Track counts and rates without addresses: + +- embedded wallet registration successes and failures by error code; +- Privy authentication and provider-unavailable errors; +- mode-switch conflicts caused by active ramps; +- signing rejection and transaction-confirmation failure rates by wallet kind and chain; +- external versus embedded completion rates; +- gas sponsorship spend if sponsorship is ever enabled. + +Alerts must not include wallet addresses, Privy wallet IDs, JWTs, signatures, or raw transaction bodies. + +## Rollback + +The switches are intentionally granular: + +1. Set onramp/offramp flags to `false` to stop embedded-wallet use in new ramp flows while preserving account access + and export. +2. Set provisioning to `false` to stop creating new wallets while allowing existing users to restore/export. +3. Remove affected widget parent origins to disable only a compromised embed. +4. Set API registration to `false` to reject new wallet bindings. +5. Set base client support to `false` only for a severe incident; tell existing embedded-wallet users how to export or + recover before doing so when possible. + +Do not delete `profile_wallets` records during rollback. They are required to restore the same user-wallet association +after re-enablement and are not signing authority. diff --git a/docs/security-spec/01-auth/privy-embedded-wallets.md b/docs/security-spec/01-auth/privy-embedded-wallets.md new file mode 100644 index 000000000..2faec0877 --- /dev/null +++ b/docs/security-spec/01-auth/privy-embedded-wallets.md @@ -0,0 +1,143 @@ +# Optional Privy Embedded Wallets + +## Scope + +Vortex offers a Privy-created EVM wallet as an optional alternative for users who do not have, or do not want to +connect, a browser wallet. Existing Reown/Wagmi wallet connections remain the default-compatible path and do not +create a Privy user or wallet. + +This integration does not replace Vortex authentication: + +- Supabase email OTP remains the canonical Vortex login and API identity. +- Privy consumes the existing Supabase JWT through JWT-based custom authentication. +- Vortex never accepts a Privy access token as authorization for Vortex API routes. +- Privy is used only to provision and operate an EVM wallet after an explicit user choice. +- Polkadot and AssetHub wallet flows are unchanged and never use Privy. + +## Trust Boundaries + +```mermaid +sequenceDiagram + participant U as User + participant C as Vortex client + participant S as Supabase + participant P as Privy + participant A as Vortex API + + U->>C: Choose "Create embedded wallet" + C->>S: Complete existing email OTP login + S-->>C: Supabase access token + C->>P: Authenticate with Supabase JWT + P-->>C: Privy user session + C->>P: Create client-side EVM wallet + P-->>C: Wallet ID and address + C->>A: POST /v1/wallets/privy with Vortex bearer token + A->>P: Look up user by Supabase subject + P-->>A: Linked accounts + A->>A: Match wallet ID, address, chain, provider, and profile + A-->>C: Registered wallet metadata +``` + +The browser asks Privy to sign. The Vortex API stores only wallet metadata and never receives the private key, +recovery material, or authority to sign. No delegated server signer is configured by this implementation. + +## Security Invariants + +1. **Embedded wallet creation MUST be opt-in.** `createOnLogin` is `off`. Loading Vortex, authenticating, or connecting + an external wallet must not create a Privy wallet. +2. **Supabase remains authoritative.** All `/v1/wallets` routes use `requireAuth`; `req.userId` comes only from verified + Supabase authentication. +3. **The Privy app secret MUST remain server-side.** Only the public app ID and optional client ID may use a `VITE_` + prefix. +4. **Registration MUST verify provider ownership.** Before persisting metadata, the API looks up the Privy user whose + custom auth ID equals the authenticated Supabase profile ID and matches the wallet ID and checksummed address. +5. **Wallet metadata MUST NOT authorize movement of funds.** A `profile_wallets` record may choose UX defaults, but + ramp ownership checks and every existing server-issued signer/address binding remain mandatory. +6. **Private-key operations MUST stay in the client.** Vortex must not log, transmit, persist, or request a user's + exported private key. +7. **There is at most one active Privy EVM wallet per profile.** The database also prevents a Privy wallet ID or EVM + address from being registered to multiple profiles. +8. **Mode changes MUST be blocked during a nonterminal ramp.** This prevents the wallet selected for an in-flight ramp + from silently changing. +9. **Unknown iframe ancestry MUST fail closed.** The widget initializes Privy only at top level or when + `document.referrer` has an exact origin match in `VITE_PRIVY_WIDGET_PARENT_ORIGINS`. A missing referrer in an iframe + is not trusted. +10. **Gas is user-paid by default.** Sponsorship is enabled only when `VITE_PRIVY_GAS_POLICY=sponsored` and the + corresponding Privy policy and Vortex transaction allowlist have been reviewed. +11. **Signing behavior MUST be wallet-neutral.** External and embedded adapters feed the same existing ramp signing + functions; this integration must not weaken transaction contents, chain checks, signature checks, or phase + ordering. +12. **Observability MUST exclude wallet identity.** Wallet addresses, provider wallet IDs, JWTs, signatures, and + transaction payloads are not attached to Sentry user context or error messages. + +## Persistence and API Contract + +`profiles.wallet_mode` is nullable and accepts `external` or `privy_embedded`. `null` preserves the legacy behavior. + +`profile_wallets` stores: + +- Vortex profile ID; +- provider (`privy`); +- opaque Privy wallet ID; +- checksummed EVM address; +- chain type (`ethereum`); +- status and timestamps. + +Authenticated routes: + +- `GET /v1/wallets` returns the selected mode and active wallet metadata. +- `PATCH /v1/wallets/mode` changes the preference unless a ramp is active. +- `POST /v1/wallets/privy` verifies Privy ownership, registers idempotently, and selects the embedded mode atomically. + +The server verification request is bounded by a timeout. Disabled or unavailable provider verification returns a +service-unavailable response; missing or mismatched ownership fails registration. + +## User Recovery and Export + +The dashboard exposes Privy's client-side export flow for the selected address. Export is a sensitive operation: the +user must be authenticated and Privy's protected UI performs the disclosure. Vortex must not render or intercept the +key itself. + +Before enabling the feature in production: + +- verify export on every supported production browser; +- provide user-facing guidance for importing the exported EVM key; +- document that losing access to the Vortex/Supabase account can affect access until the key has been exported; +- decide whether MFA is required for wallet actions; +- establish a support escalation for account recovery and provider outages. + +## Threats and Mitigations + +| Threat | Mitigation | +|---|---| +| An attacker submits another user's wallet metadata | Server-side Privy lookup by authenticated custom auth ID, plus wallet ID/address matching | +| A wallet ID or address is rebound to another profile | Unique database constraints and conflict responses | +| A malicious embed tries to initialize wallet controls | Exact parent-origin allowlist and fail-closed behavior for unknown iframe ancestry | +| A client flag accidentally creates wallets for everyone | `createOnLogin: "off"` plus a separate provisioning feature flag | +| A provider outage changes wallet ownership | Registration fails closed; existing external-wallet flow remains available | +| A user changes wallets during a ramp | API rejects mode changes while a nonterminal ramp exists | +| Sponsored gas becomes an unbounded cost or transaction bypass | User-paid default; sponsorship needs an explicit flag and separately reviewed provider policies | +| Sensitive wallet data reaches monitoring | Wallet Sentry domain uses profile-level auth context only; no address/provider ID fields | +| Embedded signing diverges from external signing | Shared adapter contract tests exercise the existing phase calls and request shapes | + +## Audit Checklist + +- [ ] Production and staging use separate Privy app/client configuration. +- [ ] Supabase JWT verification is configured in Privy using the correct JWKS and `sub` claim. +- [ ] Only the required dashboard, widget, and exact trusted parent origins are allowlisted. +- [ ] `PRIVY_APP_SECRET` is available only to the API secret store and is absent from built client assets. +- [ ] Provisioning, onramp, offramp, and sponsorship flags were enabled independently and in that order. +- [ ] Wallet creation is absent from external-wallet login and connection tests. +- [ ] Ownership mismatch, duplicate binding, active-ramp switching, and provider outage tests pass. +- [ ] Client-side export and account recovery have been tested and documented for support. +- [ ] AssetHub and Polkadot flows do not render or initialize Privy. +- [ ] Sentry events contain no wallet address, provider wallet ID, token, signature, or transaction payload. + +## References + +- [Privy: using an existing JWT authentication provider](https://docs.privy.io/authentication/user-authentication/jwt-based-auth/usage) +- [Privy: configure JWT-based authentication](https://docs.privy.io/authentication/user-authentication/jwt-based-auth/setup) +- [Privy: automatic wallet creation](https://docs.privy.io/basics/react/advanced/automatic-wallet-creation) +- [Privy: configure allowed origins](https://docs.privy.io/recipes/dashboard/allowed-domains) +- [Privy: export a wallet](https://docs.privy.io/wallets/wallets/export) +- [Privy: configure gas sponsorship](https://docs.privy.io/wallets/gas-and-asset-management/gas/setup) diff --git a/docs/security-spec/01-auth/supabase-otp.md b/docs/security-spec/01-auth/supabase-otp.md index 62888870a..270185315 100644 --- a/docs/security-spec/01-auth/supabase-otp.md +++ b/docs/security-spec/01-auth/supabase-otp.md @@ -12,6 +12,11 @@ The flow: 5. API middleware (`supabaseAuth.ts`) verifies the JWT via `SupabaseAuthService.verifyToken()` and attaches `userId` to the request 6. Access tokens are short-lived. The frontend refreshes them via `POST /v1/auth/refresh` (`SupabaseAuthService.refreshToken()` → Supabase `refreshSession`), scheduled just before expiry and also triggered on a `401` (single-flight refresh + one retry). The frontend never calls Supabase `refreshSession` directly with the anon key. The endpoint returns `401` **only** when the refresh token is confirmed invalid/revoked; transient upstream failures (Supabase unreachable / 5xx) return `503` so the frontend keeps the session and retries. +For users who explicitly choose an embedded EVM wallet, the client also supplies this same Supabase access token to +Privy's JWT-based authentication integration. That creates a Privy session keyed by the token's `sub` claim; it does +not replace the Supabase session, add a second Vortex login, or make Privy tokens valid on Vortex API routes. See +[`privy-embedded-wallets.md`](./privy-embedded-wallets.md). + Two middleware variants exist: - **`requireAuth`** — Returns 401 if token is missing or invalid. Used on protected endpoints. - **`optionalAuth`** — Attaches `userId` if token is present and valid, but continues without auth if absent. Used on endpoints that behave differently for authenticated users. @@ -27,6 +32,9 @@ Two middleware variants exist: 7. **Supabase configuration MUST be present** — If `SUPABASE_URL`, `SUPABASE_ANON_KEY`, or `SUPABASE_SERVICE_KEY` are empty/missing, the auth system is non-functional. The service should fail to start rather than silently accept all tokens. 8. **JWT expiry MUST be enforced** — Supabase tokens have a configurable expiry. The verification MUST reject expired tokens, not just validate the signature. 9. **Session teardown MUST happen only on confirmed-invalid refresh** — The frontend clears the stored session (and forces re-login) only when `/v1/auth/refresh` returns `401` (refresh token invalid/revoked). Transient failures (network errors, 5xx, timeouts) MUST NOT clear the session; they are retried while the existing session is preserved. The backend enforces this contract: `/v1/auth/refresh` returns `401` only for a definite invalid-token error from Supabase and returns `503` for transient/transport failures (and any unexpected error), so a Supabase outage cannot masquerade as an invalid token and log users out. +10. **Privy synchronization MUST preserve Supabase authority** — Privy may consume the current Supabase JWT only after + the user chooses or restores an embedded wallet. Vortex API authorization continues to verify the Supabase bearer + token, and logout/token refresh events must be propagated to the Privy synchronization hook. ## Threat Vectors & Mitigations @@ -38,6 +46,7 @@ Two middleware variants exist: | **Email enumeration** | Attacker probes OTP endpoint to discover registered emails | OTP flow handled by Supabase — Vortex API never sees OTP requests; Supabase rate limits apply | | **Token reuse after logout** | User "logs out" in frontend but JWT is still valid server-side | Supabase token invalidation on signout; short expiry window limits exposure | | **userId injection** | Attacker sends crafted request with `userId` in body/headers to bypass auth | `req.userId` is set exclusively by middleware; controllers read from `req.userId` not from request body | +| **Stale secondary wallet session** | Supabase refresh/logout is not reflected in Privy | Auth service subscription notifies the Privy JWT synchronization hook on login, refresh, and logout | ## Audit Checklist From 0bf3f3ea7c7078b89009de16cc95fa2b362006fb Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 28 Jul 2026 09:50:35 +0200 Subject: [PATCH 07/16] fix(repo): harden wallet identity handling --- .../src/wallets/PrivyWalletRuntime.tsx | 33 ++++++++++++------- .../src/wallets/WalletExperienceProvider.tsx | 30 ++++++++++++----- .../src/wallets/externalSigningAdapter.ts | 13 ++++++-- .../src/wallets/privyWalletSelection.ts | 26 +++++++++++++++ apps/dashboard/src/wallets/walletAccount.ts | 10 ++++++ .../src/wallets/walletIdentity.test.ts | 31 +++++++++++++++++ .../src/wallets/PrivyWidgetWalletRuntime.tsx | 28 +++++++++++----- .../src/wallets/WidgetWalletProvider.tsx | 30 +++++++++++++++-- .../src/wallets/externalSigningAdapter.ts | 13 ++++++-- .../src/wallets/privyWalletSelection.ts | 26 +++++++++++++++ apps/frontend/src/wallets/walletAccount.ts | 10 ++++++ .../src/wallets/walletIdentity.test.ts | 29 ++++++++++++++++ 12 files changed, 242 insertions(+), 37 deletions(-) create mode 100644 apps/dashboard/src/wallets/privyWalletSelection.ts create mode 100644 apps/dashboard/src/wallets/walletAccount.ts create mode 100644 apps/dashboard/src/wallets/walletIdentity.test.ts create mode 100644 apps/frontend/src/wallets/privyWalletSelection.ts create mode 100644 apps/frontend/src/wallets/walletAccount.ts create mode 100644 apps/frontend/src/wallets/walletIdentity.test.ts diff --git a/apps/dashboard/src/wallets/PrivyWalletRuntime.tsx b/apps/dashboard/src/wallets/PrivyWalletRuntime.tsx index 18bf93b35..e7d484054 100644 --- a/apps/dashboard/src/wallets/PrivyWalletRuntime.tsx +++ b/apps/dashboard/src/wallets/PrivyWalletRuntime.tsx @@ -14,10 +14,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { hexToBytes } from "viem"; import { waitForTransactionReceipt } from "wagmi/actions"; import { wagmiConfig } from "@/lib/wagmi"; -import { type WalletMode, WalletsAPI, type WalletsResponse } from "@/services/api/wallets.api"; +import { type ProfileWallet, type WalletMode, WalletsAPI, type WalletsResponse } from "@/services/api/wallets.api"; import { AuthService } from "@/services/auth"; import { useAuthStore } from "@/stores/auth.store"; import { privyWalletConfig } from "./config"; +import { selectPrivyEmbeddedWallet } from "./privyWalletSelection"; import { setActiveWalletSigningAdapter, type WalletSigningAdapter } from "./signingAdapter"; import { type WalletExperience, WalletExperienceContext } from "./WalletExperienceContext"; @@ -27,6 +28,7 @@ interface PrivyWalletRuntimeProps { connectExternalWallet: () => Promise; onAutoCreateHandled: () => void; onModeChange: (mode: WalletMode) => void; + registeredWallet?: Pick; } interface PrivyWalletProviderRuntimeProps extends PrivyWalletRuntimeProps { @@ -34,16 +36,13 @@ interface PrivyWalletProviderRuntimeProps extends PrivyWalletRuntimeProps { clientId?: string; } -function isPrivyEmbeddedWallet(wallet: { type: string; walletClientType?: string }): boolean { - return wallet.type === "ethereum" && (wallet.walletClientType === "privy" || wallet.walletClientType === "privy-v2"); -} - export function PrivyWalletRuntime({ autoCreate, children, connectExternalWallet, onAutoCreateHandled, - onModeChange + onModeChange, + registeredWallet }: PrivyWalletRuntimeProps) { const user = useAuthStore(state => state.user); const queryClient = useQueryClient(); @@ -65,15 +64,19 @@ export function PrivyWalletRuntime({ subscribe: subscribeToAuth }); - const embeddedWallet = wallets.find(isPrivyEmbeddedWallet); - const address = embeddedWallet?.address as `0x${string}` | undefined; + const embeddedWallet = selectPrivyEmbeddedWallet(wallets, registeredWallet); + const walletAddress = embeddedWallet?.address as `0x${string}` | undefined; const linkedEmbeddedWallet = privyUser?.linkedAccounts.find( (account): account is Extract => account.type === "wallet" && account.chainType === "ethereum" && (account.walletClientType === "privy" || account.walletClientType === "privy-v2") && - account.address.toLowerCase() === address?.toLowerCase() + account.address.toLowerCase() === walletAddress?.toLowerCase() && + (!registeredWallet || account.id === registeredWallet.providerWalletId) ); + const registeredWalletUnavailable = + walletsReady && authState.status === "done" && Boolean(registeredWallet) && (!embeddedWallet || !linkedEmbeddedWallet); + const address = registeredWalletUnavailable ? undefined : walletAddress; const signingAdapter = useMemo(() => { if (!address) return null; @@ -143,6 +146,9 @@ export function PrivyWalletRuntime({ setCreating(true); setError(undefined); try { + if (registeredWalletUnavailable) { + throw new Error("The registered embedded wallet is not available in the current Privy session"); + } if (embeddedWallet && linkedEmbeddedWallet) { await register(linkedEmbeddedWallet); } else { @@ -154,7 +160,7 @@ export function PrivyWalletRuntime({ } finally { setCreating(false); } - }, [createWallet, embeddedWallet, linkedEmbeddedWallet, register]); + }, [createWallet, embeddedWallet, linkedEmbeddedWallet, register, registeredWalletUnavailable]); const autoCreateStarted = useRef(false); useEffect(() => { @@ -188,7 +194,11 @@ export function PrivyWalletRuntime({ connected: Boolean(address), createEmbeddedWallet, creatingEmbeddedWallet: creating, - error, + error: + error ?? + (registeredWalletUnavailable + ? "The registered embedded wallet is not available in the current Privy session" + : undefined), exportEmbeddedWallet: async () => { if (!address) throw new Error("No embedded wallet is available to export"); await exportWallet({ address }); @@ -204,6 +214,7 @@ export function PrivyWalletRuntime({ creating, error, exportWallet, + registeredWalletUnavailable, signingAdapter, switchToExternalWallet, walletsReady diff --git a/apps/dashboard/src/wallets/WalletExperienceProvider.tsx b/apps/dashboard/src/wallets/WalletExperienceProvider.tsx index f6184c89e..3bfb1220b 100644 --- a/apps/dashboard/src/wallets/WalletExperienceProvider.tsx +++ b/apps/dashboard/src/wallets/WalletExperienceProvider.tsx @@ -1,6 +1,6 @@ import { useAppKit, useAppKitAccount } from "@reown/appkit/react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { lazy, Suspense, useCallback, useMemo, useState } from "react"; +import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react"; import { useAccount } from "wagmi"; import { type WalletMode, WalletsAPI, type WalletsResponse } from "@/services/api/wallets.api"; import { useAuthStore } from "@/stores/auth.store"; @@ -15,7 +15,15 @@ const LazyPrivyWalletRuntime = lazy(async () => { }); export function WalletExperienceProvider({ children }: { children: React.ReactNode }) { - const user = useAuthStore(state => state.user); + const userId = useAuthStore(state => state.user?.userId); + return ( + + {children} + + ); +} + +function WalletExperienceSession({ children, userId }: { children: React.ReactNode; userId?: string }) { const queryClient = useQueryClient(); const { address } = useAccount(); const { isConnected } = useAppKitAccount(); @@ -24,37 +32,42 @@ export function WalletExperienceProvider({ children }: { children: React.ReactNo const [autoCreateEmbedded, setAutoCreateEmbedded] = useState(false); const walletsQuery = useQuery({ - enabled: Boolean(user), + enabled: Boolean(userId), queryFn: ({ signal }) => WalletsAPI.getWallets(signal), - queryKey: ["wallets", user?.userId], + queryKey: ["wallets", userId], staleTime: 30_000 }); + useEffect(() => () => setActiveWalletSigningAdapter(null), []); + const storedMode = pendingMode ?? walletsQuery.data?.mode ?? null; const mode = storedMode === "privy_embedded" && !privyWalletConfig.enabled ? "external" : storedMode; const embeddedActive = privyWalletConfig.enabled && storedMode === "privy_embedded"; + const registeredWallet = walletsQuery.data?.wallets.find( + wallet => wallet.provider === "privy" && wallet.chainType === "ethereum" && wallet.status === "active" + ); const connectExternalWallet = useCallback(async () => { if (storedMode === "privy_embedded") { const response = await WalletsAPI.setMode("external"); setPendingMode(response.mode); - queryClient.setQueryData(["wallets", user?.userId], current => ({ + queryClient.setQueryData(["wallets", userId], current => ({ mode: response.mode, wallets: current?.wallets ?? [] })); } await open({ view: "Connect" }); - }, [open, queryClient, storedMode, user?.userId]); + }, [open, queryClient, storedMode, userId]); const onModeChange = useCallback( (nextMode: WalletMode) => { setPendingMode(nextMode); - queryClient.setQueryData(["wallets", user?.userId], current => ({ + queryClient.setQueryData(["wallets", userId], current => ({ mode: nextMode, wallets: current?.wallets ?? [] })); }, - [queryClient, user?.userId] + [queryClient, userId] ); const externalAdapter = useMemo(() => (address ? createExternalSigningAdapter(address) : null), [address]); @@ -95,6 +108,7 @@ export function WalletExperienceProvider({ children }: { children: React.ReactNo connectExternalWallet={connectExternalWallet} onAutoCreateHandled={() => setAutoCreateEmbedded(false)} onModeChange={onModeChange} + registeredWallet={registeredWallet} > {children} diff --git a/apps/dashboard/src/wallets/externalSigningAdapter.ts b/apps/dashboard/src/wallets/externalSigningAdapter.ts index 01353692f..c9acedf73 100644 --- a/apps/dashboard/src/wallets/externalSigningAdapter.ts +++ b/apps/dashboard/src/wallets/externalSigningAdapter.ts @@ -2,6 +2,7 @@ import type { SignedTypedData } from "@vortexfi/shared"; import { getAccount, sendTransaction, signTypedData, switchChain, waitForTransactionReceipt } from "wagmi/actions"; import { wagmiConfig } from "@/lib/wagmi"; import type { WalletSigningAdapter, WalletTransactionRequest } from "./signingAdapter"; +import { assertExpectedWalletAccount } from "./walletAccount"; export function createExternalSigningAdapter(address: `0x${string}`): WalletSigningAdapter { const originalChainByHash = new Map<`0x${string}`, number>(); @@ -10,6 +11,7 @@ export function createExternalSigningAdapter(address: `0x${string}`): WalletSign kind: "external", sendTransaction: async (transaction: WalletTransactionRequest) => { const account = getAccount(wagmiConfig); + assertExpectedWalletAccount(address, account.address); if (!account.chainId) { throw new Error("No wallet connected or unable to determine current chain ID."); } @@ -23,7 +25,9 @@ export function createExternalSigningAdapter(address: `0x${string}`): WalletSign } } try { + assertExpectedWalletAccount(address, getAccount(wagmiConfig).address); const hash = await sendTransaction(wagmiConfig, { + account: address, data: transaction.data, ...(transaction.gas && transaction.gas > 0n ? { gas: transaction.gas } : {}), to: transaction.to, @@ -38,13 +42,16 @@ export function createExternalSigningAdapter(address: `0x${string}`): WalletSign throw error; } }, - signTypedData: (typedData: SignedTypedData) => - signTypedData(wagmiConfig, { + signTypedData: (typedData: SignedTypedData) => { + assertExpectedWalletAccount(address, getAccount(wagmiConfig).address); + return signTypedData(wagmiConfig, { + account: address, domain: typedData.domain, message: typedData.message, primaryType: typedData.primaryType, types: typedData.types - }), + }); + }, waitForTransaction: async (hash, chainId) => { try { const receipt = await waitForTransactionReceipt(wagmiConfig, { chainId, hash }); diff --git a/apps/dashboard/src/wallets/privyWalletSelection.ts b/apps/dashboard/src/wallets/privyWalletSelection.ts new file mode 100644 index 000000000..a13466882 --- /dev/null +++ b/apps/dashboard/src/wallets/privyWalletSelection.ts @@ -0,0 +1,26 @@ +export interface PrivyWalletCandidate { + address: string; + type: string; + walletClientType?: string; +} + +export interface RegisteredPrivyWallet { + address: string; + providerWalletId: string; +} + +function isPrivyEmbeddedWallet(wallet: PrivyWalletCandidate): boolean { + return wallet.type === "ethereum" && (wallet.walletClientType === "privy" || wallet.walletClientType === "privy-v2"); +} + +export function selectPrivyEmbeddedWallet( + wallets: T[], + registeredWallet?: RegisteredPrivyWallet +): T | undefined { + if (!registeredWallet) { + return wallets.find(isPrivyEmbeddedWallet); + } + + const registeredAddress = registeredWallet.address.toLowerCase(); + return wallets.find(wallet => isPrivyEmbeddedWallet(wallet) && wallet.address.toLowerCase() === registeredAddress); +} diff --git a/apps/dashboard/src/wallets/walletAccount.ts b/apps/dashboard/src/wallets/walletAccount.ts new file mode 100644 index 000000000..62c7a1eba --- /dev/null +++ b/apps/dashboard/src/wallets/walletAccount.ts @@ -0,0 +1,10 @@ +import { type Address, isAddressEqual } from "viem"; + +export function assertExpectedWalletAccount(expectedAddress: Address, currentAddress?: Address): asserts currentAddress { + if (!currentAddress) { + throw new Error("No wallet account is connected."); + } + if (!isAddressEqual(expectedAddress, currentAddress)) { + throw new Error("The connected wallet account changed. Reconnect the registered wallet and try again."); + } +} diff --git a/apps/dashboard/src/wallets/walletIdentity.test.ts b/apps/dashboard/src/wallets/walletIdentity.test.ts new file mode 100644 index 000000000..93d53d1c5 --- /dev/null +++ b/apps/dashboard/src/wallets/walletIdentity.test.ts @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { selectPrivyEmbeddedWallet } from "./privyWalletSelection"; +import { assertExpectedWalletAccount } from "./walletAccount"; + +const REGISTERED_ADDRESS = "0x1111111111111111111111111111111111111111"; +const OTHER_ADDRESS = "0x2222222222222222222222222222222222222222"; + +describe("wallet identity", () => { + it("restores the registered Privy wallet instead of relying on SDK order", () => { + const wallets = [ + { address: OTHER_ADDRESS, type: "ethereum", walletClientType: "privy" }, + { address: REGISTERED_ADDRESS, type: "ethereum", walletClientType: "privy-v2" } + ]; + + const selected = selectPrivyEmbeddedWallet(wallets, { + address: REGISTERED_ADDRESS, + providerWalletId: "registered-wallet" + }); + + assert.equal(selected?.address, REGISTERED_ADDRESS); + }); + + it("rejects a connector account that differs from the adapter account", () => { + assert.doesNotThrow(() => assertExpectedWalletAccount(REGISTERED_ADDRESS, REGISTERED_ADDRESS)); + assert.throws( + () => assertExpectedWalletAccount(REGISTERED_ADDRESS, OTHER_ADDRESS), + /connected wallet account changed/ + ); + }); +}); diff --git a/apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx b/apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx index c67b98cef..6fd112d6f 100644 --- a/apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx +++ b/apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx @@ -19,6 +19,7 @@ import { waitForTransactionConfirmation } from "../helpers/safe-wallet/waitForTr import { ProfileWallet, WalletMode, WalletsResponse, WalletsService } from "../services/api/wallets.service"; import { AuthService } from "../services/auth"; import { privyWidgetConfig } from "./config"; +import { selectPrivyEmbeddedWallet } from "./privyWalletSelection"; import { EvmWalletSigningAdapter, setActiveEvmWalletSigningAdapter } from "./signingAdapter"; import { WidgetEvmWallet, WidgetWalletContext } from "./WidgetWalletContext"; @@ -27,6 +28,7 @@ interface PrivyWidgetWalletRuntimeProps { connectExternalWallet: () => Promise; mode: WalletMode; onModeChange: (mode: WalletMode, wallet?: ProfileWallet) => void; + registeredWallet?: Pick; } interface PrivyWidgetWalletProviderRuntimeProps extends PrivyWidgetWalletRuntimeProps { @@ -34,15 +36,12 @@ interface PrivyWidgetWalletProviderRuntimeProps extends PrivyWidgetWalletRuntime clientId?: string; } -function isPrivyEmbeddedWallet(wallet: { type: string; walletClientType?: string }): boolean { - return wallet.type === "ethereum" && (wallet.walletClientType === "privy" || wallet.walletClientType === "privy-v2"); -} - export function PrivyWidgetWalletRuntime({ children, connectExternalWallet, mode, - onModeChange + onModeChange, + registeredWallet }: PrivyWidgetWalletRuntimeProps) { const rampActor = useRampActor(); const walletSetupRequested = useSelector(rampActor, state => state.matches("EmbeddedWallet")); @@ -64,15 +63,19 @@ export function PrivyWidgetWalletRuntime({ subscribe: subscribeToAuth }); - const embeddedWallet = wallets.find(isPrivyEmbeddedWallet); - const address = embeddedWallet?.address as `0x${string}` | undefined; + const embeddedWallet = selectPrivyEmbeddedWallet(wallets, registeredWallet); + const walletAddress = embeddedWallet?.address as `0x${string}` | undefined; const linkedEmbeddedWallet = privyUser?.linkedAccounts.find( (account): account is Extract => account.type === "wallet" && account.chainType === "ethereum" && (account.walletClientType === "privy" || account.walletClientType === "privy-v2") && - account.address.toLowerCase() === address?.toLowerCase() + account.address.toLowerCase() === walletAddress?.toLowerCase() && + (!registeredWallet || account.id === registeredWallet.providerWalletId) ); + const registeredWalletUnavailable = + walletsReady && authState.status === "done" && Boolean(registeredWallet) && (!embeddedWallet || !linkedEmbeddedWallet); + const address = registeredWalletUnavailable ? undefined : walletAddress; const signingAdapter = useMemo(() => { if (!address) return null; @@ -137,6 +140,9 @@ export function PrivyWidgetWalletRuntime({ const createEmbeddedWallet = useCallback(async () => { setCreating(true); try { + if (registeredWalletUnavailable) { + throw new Error("The registered embedded wallet is not available in the current Privy session"); + } if (embeddedWallet && linkedEmbeddedWallet) { await register(linkedEmbeddedWallet); } else { @@ -150,7 +156,7 @@ export function PrivyWidgetWalletRuntime({ } finally { setCreating(false); } - }, [createWallet, embeddedWallet, linkedEmbeddedWallet, rampActor, register]); + }, [createWallet, embeddedWallet, linkedEmbeddedWallet, rampActor, register, registeredWalletUnavailable]); const autoCreateStarted = useRef(false); useEffect(() => { @@ -182,6 +188,9 @@ export function PrivyWidgetWalletRuntime({ connected: Boolean(address), createEmbeddedWallet: () => rampActor.send({ type: "REQUEST_EMBEDDED_WALLET" }), creatingEmbeddedWallet: creating, + embeddedUnavailableReason: registeredWalletUnavailable + ? "The registered embedded wallet is not available in the current Privy session." + : undefined, exportEmbeddedWallet: async () => { if (!address) throw new Error("No embedded wallet is available to export"); await exportWallet({ address }); @@ -202,6 +211,7 @@ export function PrivyWidgetWalletRuntime({ exportWallet, mode, rampActor, + registeredWalletUnavailable, signMessage, signingAdapter, switchToExternalWallet, diff --git a/apps/frontend/src/wallets/WidgetWalletProvider.tsx b/apps/frontend/src/wallets/WidgetWalletProvider.tsx index bdb549068..0a4ffce38 100644 --- a/apps/frontend/src/wallets/WidgetWalletProvider.tsx +++ b/apps/frontend/src/wallets/WidgetWalletProvider.tsx @@ -1,7 +1,7 @@ import { useAppKit, useAppKitAccount } from "@reown/appkit/react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useSelector } from "@xstate/react"; -import { lazy, Suspense, useCallback, useMemo, useState, useSyncExternalStore } from "react"; +import { lazy, Suspense, useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { useAccount, useSignMessage } from "wagmi"; import { useRampActor } from "../contexts/rampState"; import { WalletMode, WalletsResponse, WalletsService } from "../services/api/wallets.service"; @@ -25,11 +25,29 @@ function subscribeToAuth(listener: () => void): () => void { } export function WidgetWalletProvider({ children }: { children: React.ReactNode }) { - const rampActor = useRampActor(); - const walletSetupRequested = useSelector(rampActor, state => state.matches("EmbeddedWallet")); const accessToken = useSyncExternalStore(subscribeToAuth, accessTokenSnapshot, () => ""); const userId = AuthService.getUserId(); const authenticated = accessToken.length > 0 && Boolean(userId); + const sessionKey = authenticated && userId ? userId : "anonymous"; + + return ( + + {children} + + ); +} + +function WidgetWalletSession({ + authenticated, + children, + userId +}: { + authenticated: boolean; + children: React.ReactNode; + userId: string | null; +}) { + const rampActor = useRampActor(); + const walletSetupRequested = useSelector(rampActor, state => state.matches("EmbeddedWallet")); const queryClient = useQueryClient(); const { address } = useAccount(); const evmAddress = address as `0x${string}` | undefined; @@ -45,10 +63,15 @@ export function WidgetWalletProvider({ children }: { children: React.ReactNode } staleTime: 30_000 }); + useEffect(() => () => setActiveEvmWalletSigningAdapter(null), []); + const storedMode = pendingMode ?? walletsQuery.data?.mode ?? null; const mode = storedMode === "privy_embedded" && !isPrivyEnabledForCurrentFrame ? "external" : storedMode; const embeddedActive = isPrivyEnabledForCurrentFrame && authenticated && (storedMode === "privy_embedded" || walletSetupRequested); + const registeredWallet = walletsQuery.data?.wallets.find( + wallet => wallet.provider === "privy" && wallet.chainType === "ethereum" && wallet.status === "active" + ); const connectExternalWallet = useCallback(async () => { if (storedMode === "privy_embedded") { @@ -117,6 +140,7 @@ export function WidgetWalletProvider({ children }: { children: React.ReactNode } connectExternalWallet={connectExternalWallet} mode="privy_embedded" onModeChange={onModeChange} + registeredWallet={registeredWallet} > {children} diff --git a/apps/frontend/src/wallets/externalSigningAdapter.ts b/apps/frontend/src/wallets/externalSigningAdapter.ts index b688914de..96522b916 100644 --- a/apps/frontend/src/wallets/externalSigningAdapter.ts +++ b/apps/frontend/src/wallets/externalSigningAdapter.ts @@ -3,6 +3,7 @@ import { getAccount, sendTransaction, signTypedData, switchChain } from "@wagmi/ import { waitForTransactionConfirmation } from "../helpers/safe-wallet/waitForTransactionConfirmation"; import { wagmiConfig } from "../wagmiConfig"; import { EvmWalletSigningAdapter, WalletTransactionRequest } from "./signingAdapter"; +import { assertExpectedWalletAccount } from "./walletAccount"; export function createExternalSigningAdapter(address: `0x${string}`): EvmWalletSigningAdapter { const originalChainByHash = new Map<`0x${string}`, number>(); @@ -11,6 +12,7 @@ export function createExternalSigningAdapter(address: `0x${string}`): EvmWalletS kind: "external", sendTransaction: async (transaction: WalletTransactionRequest) => { const account = getAccount(wagmiConfig); + assertExpectedWalletAccount(address, account.address); if (!account.chainId) { throw new Error("No wallet connected or unable to determine current chain ID."); } @@ -24,7 +26,9 @@ export function createExternalSigningAdapter(address: `0x${string}`): EvmWalletS } } try { + assertExpectedWalletAccount(address, getAccount(wagmiConfig).address); const hash = await sendTransaction(wagmiConfig, { + account: address, data: transaction.data, ...(transaction.gas && transaction.gas > 0n ? { gas: transaction.gas } : {}), to: transaction.to, @@ -39,13 +43,16 @@ export function createExternalSigningAdapter(address: `0x${string}`): EvmWalletS throw error; } }, - signTypedData: (typedData: SignedTypedData) => - signTypedData(wagmiConfig, { + signTypedData: (typedData: SignedTypedData) => { + assertExpectedWalletAccount(address, getAccount(wagmiConfig).address); + return signTypedData(wagmiConfig, { + account: address, domain: typedData.domain, message: typedData.message, primaryType: typedData.primaryType, types: typedData.types - }), + }); + }, waitForTransaction: async (hash, chainId) => { try { return await waitForTransactionConfirmation(hash, chainId); diff --git a/apps/frontend/src/wallets/privyWalletSelection.ts b/apps/frontend/src/wallets/privyWalletSelection.ts new file mode 100644 index 000000000..a13466882 --- /dev/null +++ b/apps/frontend/src/wallets/privyWalletSelection.ts @@ -0,0 +1,26 @@ +export interface PrivyWalletCandidate { + address: string; + type: string; + walletClientType?: string; +} + +export interface RegisteredPrivyWallet { + address: string; + providerWalletId: string; +} + +function isPrivyEmbeddedWallet(wallet: PrivyWalletCandidate): boolean { + return wallet.type === "ethereum" && (wallet.walletClientType === "privy" || wallet.walletClientType === "privy-v2"); +} + +export function selectPrivyEmbeddedWallet( + wallets: T[], + registeredWallet?: RegisteredPrivyWallet +): T | undefined { + if (!registeredWallet) { + return wallets.find(isPrivyEmbeddedWallet); + } + + const registeredAddress = registeredWallet.address.toLowerCase(); + return wallets.find(wallet => isPrivyEmbeddedWallet(wallet) && wallet.address.toLowerCase() === registeredAddress); +} diff --git a/apps/frontend/src/wallets/walletAccount.ts b/apps/frontend/src/wallets/walletAccount.ts new file mode 100644 index 000000000..62c7a1eba --- /dev/null +++ b/apps/frontend/src/wallets/walletAccount.ts @@ -0,0 +1,10 @@ +import { type Address, isAddressEqual } from "viem"; + +export function assertExpectedWalletAccount(expectedAddress: Address, currentAddress?: Address): asserts currentAddress { + if (!currentAddress) { + throw new Error("No wallet account is connected."); + } + if (!isAddressEqual(expectedAddress, currentAddress)) { + throw new Error("The connected wallet account changed. Reconnect the registered wallet and try again."); + } +} diff --git a/apps/frontend/src/wallets/walletIdentity.test.ts b/apps/frontend/src/wallets/walletIdentity.test.ts new file mode 100644 index 000000000..a5023d373 --- /dev/null +++ b/apps/frontend/src/wallets/walletIdentity.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { selectPrivyEmbeddedWallet } from "./privyWalletSelection"; +import { assertExpectedWalletAccount } from "./walletAccount"; + +const REGISTERED_ADDRESS = "0x1111111111111111111111111111111111111111"; +const OTHER_ADDRESS = "0x2222222222222222222222222222222222222222"; + +describe("wallet identity", () => { + it("restores the registered Privy wallet instead of relying on SDK order", () => { + const wallets = [ + { address: OTHER_ADDRESS, type: "ethereum", walletClientType: "privy" }, + { address: REGISTERED_ADDRESS, type: "ethereum", walletClientType: "privy-v2" } + ]; + + const selected = selectPrivyEmbeddedWallet(wallets, { + address: REGISTERED_ADDRESS, + providerWalletId: "registered-wallet" + }); + + expect(selected?.address).toBe(REGISTERED_ADDRESS); + }); + + it("rejects a connector account that differs from the adapter account", () => { + expect(() => assertExpectedWalletAccount(REGISTERED_ADDRESS, REGISTERED_ADDRESS)).not.toThrow(); + expect(() => assertExpectedWalletAccount(REGISTERED_ADDRESS, OTHER_ADDRESS)).toThrow( + "connected wallet account changed" + ); + }); +}); From 9acfefb76ef2df4b88fd2163fbdf30c4dd7fcec4 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 28 Jul 2026 09:50:59 +0200 Subject: [PATCH 08/16] fix(api): serialize wallet mode changes --- .../src/api/controllers/wallets.controller.ts | 7 +- .../services/wallets/profileWallet.service.ts | 161 ++++++++++-------- apps/api/src/test-utils/factories.ts | 45 ++--- .../api/src/tests/wallets.integration.test.ts | 40 ++++- 4 files changed, 158 insertions(+), 95 deletions(-) diff --git a/apps/api/src/api/controllers/wallets.controller.ts b/apps/api/src/api/controllers/wallets.controller.ts index 1112d0177..c830158de 100644 --- a/apps/api/src/api/controllers/wallets.controller.ts +++ b/apps/api/src/api/controllers/wallets.controller.ts @@ -2,7 +2,6 @@ import { Request, Response } from "express"; import httpStatus from "http-status"; import { UniqueConstraintError } from "sequelize"; import logger from "../../config/logger"; -import { sequelize } from "../../models"; import { PrivyWalletVerificationError } from "../services/wallets/privyWallet.service"; import { listProfileWallets, @@ -92,11 +91,7 @@ export async function createPrivyWallet(req: Request, res: Response): Promise { - const registered = await registerPrivyWallet(profileId, { address, providerWalletId }, transaction); - await setWalletMode(profileId, "privy_embedded", transaction); - return registered; - }); + const wallet = await registerPrivyWallet(profileId, { address, providerWalletId }); res.status(httpStatus.OK).json({ mode: "privy_embedded", wallet: serializeWallet(wallet) }); } catch (error) { if (error instanceof WalletModeConflictError) { diff --git a/apps/api/src/api/services/wallets/profileWallet.service.ts b/apps/api/src/api/services/wallets/profileWallet.service.ts index 03e4360d0..60a6ccddb 100644 --- a/apps/api/src/api/services/wallets/profileWallet.service.ts +++ b/apps/api/src/api/services/wallets/profileWallet.service.ts @@ -1,5 +1,6 @@ import { Op, Transaction } from "sequelize"; import { getAddress, isAddress } from "viem"; +import { sequelize } from "../../../models"; import ProfileWallet from "../../../models/profileWallet.model"; import RampState from "../../../models/rampState.model"; import User from "../../../models/user.model"; @@ -20,6 +21,31 @@ export class WalletModeConflictError extends Error { } export class WalletRegistrationConflictError extends Error {} +async function getLockedProfile(profileId: string, transaction: Transaction): Promise { + const profile = await User.findByPk(profileId, { + lock: Transaction.LOCK.UPDATE, + transaction + }); + if (!profile) { + throw new Error("Profile not found"); + } + return profile; +} + +async function assertNoActiveRamp(profileId: string, transaction: Transaction): Promise { + const activeRamp = await RampState.findOne({ + attributes: ["id"], + transaction, + where: { + currentPhase: { [Op.notIn]: TERMINAL_RAMP_PHASES }, + userId: profileId + } + }); + if (activeRamp) { + throw new WalletModeConflictError("Wallet mode cannot change while a ramp is active", "active_ramp"); + } +} + export async function listProfileWallets(profileId: string): Promise<{ mode: WalletMode; wallets: ProfileWallet[]; @@ -34,47 +60,35 @@ export async function listProfileWallets(profileId: string): Promise<{ return { mode: profile?.walletMode ?? null, wallets }; } -export async function setWalletMode(profileId: string, mode: WalletMode, transaction?: Transaction): Promise { - const activeRamp = await RampState.findOne({ - attributes: ["id"], - transaction, - where: { - currentPhase: { [Op.notIn]: TERMINAL_RAMP_PHASES }, - userId: profileId - } - }); - if (activeRamp) { - throw new WalletModeConflictError("Wallet mode cannot change while a ramp is active", "active_ramp"); - } +export async function setWalletMode(profileId: string, mode: WalletMode): Promise { + return sequelize.transaction(async transaction => { + const profile = await getLockedProfile(profileId, transaction); + await assertNoActiveRamp(profileId, transaction); - if (mode === "privy_embedded") { - const embeddedWallet = await ProfileWallet.findOne({ - attributes: ["id"], - transaction, - where: { - chainType: "ethereum", - profileId, - provider: "privy", - status: "active" + if (mode === "privy_embedded") { + const embeddedWallet = await ProfileWallet.findOne({ + attributes: ["id"], + transaction, + where: { + chainType: "ethereum", + profileId, + provider: "privy", + status: "active" + } + }); + if (!embeddedWallet) { + throw new WalletModeConflictError("An active verified Privy wallet is required for embedded mode", "missing_wallet"); } - }); - if (!embeddedWallet) { - throw new WalletModeConflictError("An active verified Privy wallet is required for embedded mode", "missing_wallet"); } - } - const profile = await User.findByPk(profileId, { transaction }); - if (!profile) { - throw new Error("Profile not found"); - } - await profile.update({ walletMode: mode }, { transaction }); - return profile.walletMode; + await profile.update({ walletMode: mode }, { transaction }); + return profile.walletMode; + }); } export async function registerPrivyWallet( profileId: string, - input: { providerWalletId: string; address: string }, - transaction?: Transaction + input: { providerWalletId: string; address: string } ): Promise { if (!input.providerWalletId.trim() || !isAddress(input.address)) { throw new WalletRegistrationConflictError("A valid Privy wallet ID and EVM address are required"); @@ -86,44 +100,53 @@ export async function registerPrivyWallet( providerWalletId: input.providerWalletId }); - const conflictingWallet = await ProfileWallet.findOne({ - transaction, - where: { - [Op.or]: [ - { provider: "privy", providerWalletId: input.providerWalletId }, - { address: getAddress(verified.address), chainType: "ethereum" } - ] + return sequelize.transaction(async transaction => { + const profile = await getLockedProfile(profileId, transaction); + await assertNoActiveRamp(profileId, transaction); + + const conflictingWallet = await ProfileWallet.findOne({ + transaction, + where: { + [Op.or]: [ + { provider: "privy", providerWalletId: input.providerWalletId }, + { address: getAddress(verified.address), chainType: "ethereum" } + ] + } + }); + if (conflictingWallet && conflictingWallet.profileId !== profileId) { + throw new WalletRegistrationConflictError("This embedded wallet is already registered to another profile"); } - }); - if (conflictingWallet && conflictingWallet.profileId !== profileId) { - throw new WalletRegistrationConflictError("This embedded wallet is already registered to another profile"); - } - const existingForProfile = await ProfileWallet.findOne({ - transaction, - where: { chainType: "ethereum", profileId, provider: "privy", status: "active" } - }); - if (existingForProfile) { - if ( - existingForProfile.providerWalletId !== input.providerWalletId || - getAddress(existingForProfile.address) !== verified.address - ) { - throw new WalletRegistrationConflictError("This profile already has a different active Privy wallet"); + const existingForProfile = await ProfileWallet.findOne({ + transaction, + where: { chainType: "ethereum", profileId, provider: "privy", status: "active" } + }); + let wallet: ProfileWallet; + if (existingForProfile) { + if ( + existingForProfile.providerWalletId !== input.providerWalletId || + getAddress(existingForProfile.address) !== verified.address + ) { + throw new WalletRegistrationConflictError("This profile already has a different active Privy wallet"); + } + await existingForProfile.update({ lastUsedAt: new Date() }, { transaction }); + wallet = existingForProfile; + } else { + wallet = await ProfileWallet.create( + { + address: verified.address, + chainType: "ethereum", + lastUsedAt: new Date(), + profileId, + provider: "privy", + providerWalletId: input.providerWalletId, + status: "active" + }, + { transaction } + ); } - await existingForProfile.update({ lastUsedAt: new Date() }, { transaction }); - return existingForProfile; - } - return ProfileWallet.create( - { - address: verified.address, - chainType: "ethereum", - lastUsedAt: new Date(), - profileId, - provider: "privy", - providerWalletId: input.providerWalletId, - status: "active" - }, - { transaction } - ); + await profile.update({ walletMode: "privy_embedded" }, { transaction }); + return wallet; + }); } diff --git a/apps/api/src/test-utils/factories.ts b/apps/api/src/test-utils/factories.ts index 5b3d16780..9bec5e276 100644 --- a/apps/api/src/test-utils/factories.ts +++ b/apps/api/src/test-utils/factories.ts @@ -11,6 +11,7 @@ import { RampDirection, type UnsignedTx } from "@vortexfi/shared"; +import type { Transaction } from "sequelize"; import { generateApiKey, getKeyPrefix, hashApiKey } from "../api/middlewares/apiKeyAuth.helpers"; import { hashTaxReference } from "../api/services/avenia/avenia-customer.service"; import { getOrCreateCustomerEntityForProfile } from "../api/services/customer-entity.service"; @@ -228,24 +229,30 @@ const DEFAULT_UNSIGNED_TX: UnsignedTx = { /** * A ramp state in its initial phase, linked to a fresh quote unless quoteId is given. */ -export async function createTestRampState(overrides: Partial = {}): Promise { +export async function createTestRampState( + overrides: Partial = {}, + transaction?: Transaction +): Promise { const quoteId = overrides.quoteId ?? (await createTestQuote()).id; - return RampState.create({ - currentPhase: "initial", - errorLogs: [], - flowVariant: config.flowVariant, - from: EPaymentMethod.SEPA as DestinationType, - paymentMethod: EPaymentMethod.SEPA, - phaseHistory: [], - postCompleteState: { cleanup: { cleanupAt: null, cleanupCompleted: false, errors: null } }, - presignedTxs: null, - processingLock: { locked: false, lockedAt: null }, - state: (overrides.state ?? {}) as StateMetadata, - to: Networks.Base, - type: RampDirection.BUY, - unsignedTxs: [DEFAULT_UNSIGNED_TX], - userId: null, - ...overrides, - quoteId - }); + return RampState.create( + { + currentPhase: "initial", + errorLogs: [], + flowVariant: config.flowVariant, + from: EPaymentMethod.SEPA as DestinationType, + paymentMethod: EPaymentMethod.SEPA, + phaseHistory: [], + postCompleteState: { cleanup: { cleanupAt: null, cleanupCompleted: false, errors: null } }, + presignedTxs: null, + processingLock: { locked: false, lockedAt: null }, + state: (overrides.state ?? {}) as StateMetadata, + to: Networks.Base, + type: RampDirection.BUY, + unsignedTxs: [DEFAULT_UNSIGNED_TX], + userId: null, + ...overrides, + quoteId + }, + { transaction } + ); } diff --git a/apps/api/src/tests/wallets.integration.test.ts b/apps/api/src/tests/wallets.integration.test.ts index 05b4b3443..488257331 100644 --- a/apps/api/src/tests/wallets.integration.test.ts +++ b/apps/api/src/tests/wallets.integration.test.ts @@ -1,8 +1,11 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import { Transaction } from "sequelize"; import { config } from "../config/vars"; +import { sequelize } from "../models"; import ProfileWallet from "../models/profileWallet.model"; +import User from "../models/user.model"; import { resetTestDatabase, setupTestDatabase } from "../test-utils/db"; -import { createTestRampState, createTestUser } from "../test-utils/factories"; +import { createTestQuote, createTestRampState, createTestUser } from "../test-utils/factories"; import { type FakeSupabaseAuth, installFakeSupabaseAuth, testUserToken } from "../test-utils/fake-world/fake-auth"; import { startTestApp, type TestApp } from "../test-utils/test-app"; @@ -122,6 +125,41 @@ describe("wallet API", () => { expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe("ACTIVE_RAMP"); }); + it("rechecks active ramps after waiting for a concurrent ramp registration", async () => { + const user = await createTestUser({ email: "wallet-mode-race@example.com" }); + const quote = await createTestQuote({ userId: user.id }); + const rampTransaction = await sequelize.transaction(); + let transactionFinished = false; + + try { + await User.findByPk(user.id, { + lock: Transaction.LOCK.UPDATE, + transaction: rampTransaction + }); + await createTestRampState({ quoteId: quote.id, userId: user.id }, rampTransaction); + + const modeRequest = api.request("/v1/wallets/mode", { + body: JSON.stringify({ mode: "external" }), + headers: headers(testUserToken(user.id, user.email)), + method: "PATCH" + }); + + await new Promise(resolve => setTimeout(resolve, 100)); + await rampTransaction.commit(); + transactionFinished = true; + + const response = await modeRequest; + expect(response.status).toBe(409); + expect(((await response.json()) as { error: { code: string } }).error.code).toBe("ACTIVE_RAMP"); + await user.reload(); + expect(user.walletMode).toBeNull(); + } finally { + if (!transactionFinished) { + await rampTransaction.rollback(); + } + } + }); + it("verifies and idempotently registers a Privy wallet", async () => { const user = await createTestUser({ email: "wallet-register@example.com" }); const token = testUserToken(user.id, user.email); From 4abed4a2e5f736739001bc5b00c75de3bf735dc7 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 28 Jul 2026 10:01:57 +0200 Subject: [PATCH 09/16] chore(repo): recognize CLAUDE.md as Codex guidance --- .codex/config.toml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .codex/config.toml diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 000000000..64cfaf071 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1 @@ +project_doc_fallback_filenames = ["CLAUDE.md"] From cf05c214ddebbf63cd7f2abd51752ef7faef3b8a Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 28 Jul 2026 11:00:12 +0200 Subject: [PATCH 10/16] fix(frontend): preserve ramp address during wallet reconnect --- .../src/hooks/useVortexAccount.test.tsx | 59 +++++++++++++++++++ apps/frontend/src/hooks/useVortexAccount.ts | 10 ++-- .../src/machines/ramp.machine.test.ts | 10 ++++ apps/frontend/src/machines/ramp.machine.ts | 2 +- 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 apps/frontend/src/hooks/useVortexAccount.test.tsx diff --git a/apps/frontend/src/hooks/useVortexAccount.test.tsx b/apps/frontend/src/hooks/useVortexAccount.test.tsx new file mode 100644 index 000000000..4ca5a23e2 --- /dev/null +++ b/apps/frontend/src/hooks/useVortexAccount.test.tsx @@ -0,0 +1,59 @@ +// @vitest-environment jsdom +import { renderHook } from "@testing-library/react"; +import { Networks } from "@vortexfi/shared"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + activateSigner: vi.fn(), + address: undefined as `0x${string}` | undefined, + rampSend: vi.fn() +})); + +vi.mock("wagmi", () => ({ + useAccount: () => ({ chainId: 8453 }) +})); + +vi.mock("../contexts/network", () => ({ + useNetwork: () => ({ selectedNetwork: Networks.Base }) +})); + +vi.mock("../contexts/polkadotWallet", () => ({ + usePolkadotWalletState: () => ({ walletAccount: undefined }) +})); + +vi.mock("../contexts/rampState", () => ({ + useRampActor: () => ({ send: mocks.rampSend }) +})); + +vi.mock("../wallets/WidgetWalletContext", () => ({ + useWidgetWallet: () => ({ + activateSigner: mocks.activateSigner, + address: mocks.address, + mode: "external", + signMessage: vi.fn() + }) +})); + +import { useVortexAccount } from "./useVortexAccount"; + +describe("useVortexAccount", () => { + beforeEach(() => { + mocks.activateSigner.mockClear(); + mocks.rampSend.mockClear(); + mocks.address = "0x1111111111111111111111111111111111111111"; + }); + + it("does not clear the ramp address when the wallet address is transiently unavailable", () => { + const { rerender } = renderHook(() => useVortexAccount()); + + expect(mocks.rampSend).toHaveBeenCalledWith({ + address: mocks.address, + type: "SET_ADDRESS" + }); + + mocks.address = undefined; + rerender(); + + expect(mocks.rampSend.mock.calls.filter(([event]) => event.type === "SET_ADDRESS")).toHaveLength(1); + }); +}); diff --git a/apps/frontend/src/hooks/useVortexAccount.ts b/apps/frontend/src/hooks/useVortexAccount.ts index 05d66baad..2d094a23a 100644 --- a/apps/frontend/src/hooks/useVortexAccount.ts +++ b/apps/frontend/src/hooks/useVortexAccount.ts @@ -84,10 +84,12 @@ export const useVortexAccount = (forceNetwork?: Networks) => { // update the ramp actor with the current context useEffect(() => { - rampActor?.send({ - address, - type: "SET_ADDRESS" - }); + if (rampActor && address) { + rampActor.send({ + address, + type: "SET_ADDRESS" + }); + } if (isNetworkEVM(selectedNetwork)) { evmWallet.activateSigner(); diff --git a/apps/frontend/src/machines/ramp.machine.test.ts b/apps/frontend/src/machines/ramp.machine.test.ts index f3b14360d..25d906aa0 100644 --- a/apps/frontend/src/machines/ramp.machine.test.ts +++ b/apps/frontend/src/machines/ramp.machine.test.ts @@ -792,6 +792,16 @@ describe("rampMachine", () => { expect(actor.getSnapshot().context.connectedWalletAddress).toBe("0x3333333333333333333333333333333333333333"); }); + it("preserves the connected wallet address when an update is transiently undefined", () => { + const actor = createRampActor(); + actor.start(); + actor.send({ address: "0x3333333333333333333333333333333333333333", type: "SET_ADDRESS" }); + + actor.send({ address: undefined, type: "SET_ADDRESS" }); + + expect(actor.getSnapshot().context.connectedWalletAddress).toBe("0x3333333333333333333333333333333333333333"); + }); + it("EXPIRE_QUOTE marks the quote expired unless the quote is locked", async () => { const actor = createRampActor(); actor.start(); diff --git a/apps/frontend/src/machines/ramp.machine.ts b/apps/frontend/src/machines/ramp.machine.ts index f339e3416..d7b70b6fc 100644 --- a/apps/frontend/src/machines/ramp.machine.ts +++ b/apps/frontend/src/machines/ramp.machine.ts @@ -162,7 +162,7 @@ export const rampMachine = setup({ }, SET_ADDRESS: { actions: assign({ - connectedWalletAddress: ({ event }) => event.address + connectedWalletAddress: ({ context, event }) => event.address ?? context.connectedWalletAddress }) }, SET_EXTERNAL_ID: { From e8bbc553d5ace0ab07f8ec3f3d30e7a1a516075a Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 28 Jul 2026 11:00:24 +0200 Subject: [PATCH 11/16] fix(repo): bind typed-data signatures to requested signer --- apps/dashboard/src/machines/transfer.actors.ts | 4 ++-- .../src/services/transactions/userSigning.ts | 8 +++++++- .../src/wallets/walletSigning.contract.test.ts | 13 ++++++++++++- .../frontend/src/machines/actors/sign.actor.test.ts | 4 ++-- apps/frontend/src/machines/actors/sign.actor.ts | 4 ++-- .../src/services/transactions/userSigning.ts | 8 +++++++- .../src/wallets/walletSigning.contract.test.ts | 12 +++++++++++- 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/dashboard/src/machines/transfer.actors.ts b/apps/dashboard/src/machines/transfer.actors.ts index dc8f7a324..a85be142a 100644 --- a/apps/dashboard/src/machines/transfer.actors.ts +++ b/apps/dashboard/src/machines/transfer.actors.ts @@ -196,10 +196,10 @@ export async function signUserTransactions(input: SignUserTransactionsInput): Pr try { for (const tx of sortedTxs) { if (isSignedTypedData(tx.txData)) { - const signedArray = await signMultipleTypedData([tx.txData]); + const signedArray = await signMultipleTypedData([tx.txData], tx.signer); signedTxs.push({ ...tx, txData: signedArray[0] } as PresignedTx); } else if (isSignedTypedDataArray(tx.txData)) { - signedTxs.push({ ...tx, txData: await signMultipleTypedData(tx.txData) } as PresignedTx); + signedTxs.push({ ...tx, txData: await signMultipleTypedData(tx.txData, tx.signer) } as PresignedTx); } else if (tx.phase === "squidRouterApprove") { squidRouterApproveHash = await signAndSubmitEvmTransaction(tx); } else if (tx.phase === "squidRouterSwap") { diff --git a/apps/dashboard/src/services/transactions/userSigning.ts b/apps/dashboard/src/services/transactions/userSigning.ts index b8ca1cde9..b0e7f2715 100644 --- a/apps/dashboard/src/services/transactions/userSigning.ts +++ b/apps/dashboard/src/services/transactions/userSigning.ts @@ -6,8 +6,14 @@ import { getActiveWalletSigningAdapter } from "@/wallets/signingAdapter"; * Signs multiple typed data objects with the connected wallet and returns signature * objects. Ported from the widget's userSigning service. */ -export async function signMultipleTypedData(typedDataArray: SignedTypedData[]): Promise { +export async function signMultipleTypedData( + typedDataArray: SignedTypedData[], + expectedSigner: string +): Promise { const adapter = getActiveWalletSigningAdapter(); + if (getAddress(adapter.address) !== getAddress(expectedSigner)) { + throw new Error("The selected wallet does not match the server-issued typed-data signer"); + } const signedTypedDataArray: SignedTypedData[] = []; for (const typedData of typedDataArray) { diff --git a/apps/dashboard/src/wallets/walletSigning.contract.test.ts b/apps/dashboard/src/wallets/walletSigning.contract.test.ts index aaf531c15..079cd2407 100644 --- a/apps/dashboard/src/wallets/walletSigning.contract.test.ts +++ b/apps/dashboard/src/wallets/walletSigning.contract.test.ts @@ -72,7 +72,7 @@ describe("wallet signer contract", () => { const fake = fakeAdapter(kind); setActiveWalletSigningAdapter(fake.adapter); - const [signed] = await signMultipleTypedData([typedData]); + const [signed] = await signMultipleTypedData([typedData], address); const hash = await signAndSubmitEvmTransaction(unsignedTx); assert.ok(signed); @@ -108,4 +108,15 @@ describe("wallet signer contract", () => { ); assert.equal(fake.calls.length, 0); }); + + it("rejects typed data for a different signer before requesting a signature", async () => { + const fake = fakeAdapter("privy_embedded"); + setActiveWalletSigningAdapter(fake.adapter); + + await assert.rejects( + signMultipleTypedData([typedData], "0x2222222222222222222222222222222222222222"), + /does not match the server-issued typed-data signer/ + ); + assert.equal(fake.calls.length, 0); + }); }); diff --git a/apps/frontend/src/machines/actors/sign.actor.test.ts b/apps/frontend/src/machines/actors/sign.actor.test.ts index e59cec424..23c9ae68b 100644 --- a/apps/frontend/src/machines/actors/sign.actor.test.ts +++ b/apps/frontend/src/machines/actors/sign.actor.test.ts @@ -231,7 +231,7 @@ describe("signTransactionsActor", () => { const { events } = await runActor(context); - expect(signMultipleTypedData).toHaveBeenCalledWith([typedData]); + expect(signMultipleTypedData).toHaveBeenCalledWith([typedData], USER_ADDRESS); expect(events.map(event => event.phase)).toEqual(["started", "signed"]); expect(updateCalls[0].presignedTxs).toHaveLength(1); expect(updateCalls[0].presignedTxs[0].txData).toEqual(signedTypedData); @@ -248,7 +248,7 @@ describe("signTransactionsActor", () => { await runActor(context); - expect(signMultipleTypedData).toHaveBeenCalledWith(typedDataArray); + expect(signMultipleTypedData).toHaveBeenCalledWith(typedDataArray, USER_ADDRESS); expect(updateCalls[0].presignedTxs[0].txData).toEqual(signedArray); }); }); diff --git a/apps/frontend/src/machines/actors/sign.actor.ts b/apps/frontend/src/machines/actors/sign.actor.ts index cf03a2882..048678985 100644 --- a/apps/frontend/src/machines/actors/sign.actor.ts +++ b/apps/frontend/src/machines/actors/sign.actor.ts @@ -91,10 +91,10 @@ export const signTransactionsActor = async ({ if (isSignedTypedData(tx.txData) || isSignedTypedDataArray(tx.txData)) { input.parent.send({ current, max: total, phase: "started", type: "SIGNING_UPDATE" }); if (isSignedTypedData(tx.txData)) { - const signedArray = await signMultipleTypedData([tx.txData]); + const signedArray = await signMultipleTypedData([tx.txData], tx.signer); tx.txData = signedArray[0]; } else { - tx.txData = await signMultipleTypedData(tx.txData); + tx.txData = await signMultipleTypedData(tx.txData, tx.signer); } signedTxs.push(tx); diff --git a/apps/frontend/src/services/transactions/userSigning.ts b/apps/frontend/src/services/transactions/userSigning.ts index d6e4058b5..214802505 100644 --- a/apps/frontend/src/services/transactions/userSigning.ts +++ b/apps/frontend/src/services/transactions/userSigning.ts @@ -17,8 +17,14 @@ import { PolkadotNodeName, polkadotApiService } from "../api/polkadot.service"; /** * Signs multiple typed data objects and returns signature objects */ -export async function signMultipleTypedData(typedDataArray: SignedTypedData[]): Promise { +export async function signMultipleTypedData( + typedDataArray: SignedTypedData[], + expectedSigner: string +): Promise { const adapter = getActiveEvmWalletSigningAdapter(); + if (getAddress(adapter.address) !== getAddress(expectedSigner)) { + throw new Error("The selected wallet does not match the server-issued typed-data signer"); + } const signedTypedDataArray: SignedTypedData[] = []; for (const typedData of typedDataArray) { diff --git a/apps/frontend/src/wallets/walletSigning.contract.test.ts b/apps/frontend/src/wallets/walletSigning.contract.test.ts index 90f94fee4..2b89e1de5 100644 --- a/apps/frontend/src/wallets/walletSigning.contract.test.ts +++ b/apps/frontend/src/wallets/walletSigning.contract.test.ts @@ -61,7 +61,7 @@ describe("widget wallet signer contract", () => { const fake = fakeAdapter(kind); setActiveEvmWalletSigningAdapter(fake.adapter); - const [signed] = await signMultipleTypedData([typedData]); + const [signed] = await signMultipleTypedData([typedData], address); const hash = await signAndSubmitEvmTransaction(unsignedTx); expect(signed?.signature).toEqual({ @@ -96,4 +96,14 @@ describe("widget wallet signer contract", () => { ).rejects.toThrow("does not match the server-issued transaction signer"); expect(fake.calls).toHaveLength(0); }); + + it("rejects typed data for a different signer before requesting a signature", async () => { + const fake = fakeAdapter("privy_embedded"); + setActiveEvmWalletSigningAdapter(fake.adapter); + + await expect( + signMultipleTypedData([typedData], "0x2222222222222222222222222222222222222222") + ).rejects.toThrow("does not match the server-issued typed-data signer"); + expect(fake.calls).toHaveLength(0); + }); }); From 6c140b05d3574be794b44a70a02494da9568f2fa Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 28 Jul 2026 11:00:40 +0200 Subject: [PATCH 12/16] docs(api): sync wallet routes in security specification --- docs/security-spec/07-operations/api-surface.md | 6 +++--- docs/security-spec/README.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/security-spec/07-operations/api-surface.md b/docs/security-spec/07-operations/api-surface.md index 4b43861d3..8b9db4425 100644 --- a/docs/security-spec/07-operations/api-surface.md +++ b/docs/security-spec/07-operations/api-surface.md @@ -33,7 +33,7 @@ This spec covers the external-facing attack surface of the Vortex API (`apps/api - During an active window, mutable quote/ramp operations return HTTP `503 Service Unavailable` before controller/service work starts. - Rejections include `Retry-After`, `Cache-Control: no-store`, and downtime metadata (`maintenance_start`, `maintenance_end`, affected operations) in the error payload so direct API clients can pause and retry after the window. -**Route structure:** 27 TypeScript route files under `api/routes/v1/` including `index.ts`, each mounting controllers with appropriate auth middleware. +**Route structure:** 36 TypeScript route files under `api/routes/v1/` including `index.ts`, each mounting controllers with appropriate auth middleware. ## Security Invariants @@ -82,14 +82,14 @@ This spec covers the external-facing attack surface of the Vortex API (`apps/api - [N/A] Verify `NODE_ENV` is set to `"production"` in production — stack traces are only stripped when not in development mode. **N/A** — requires deployment configuration inspection. - [x] Verify error responses do not include internal error types, database error codes, or SQL fragments. **PASS** — error handler wraps errors in generic `APIError` format. - [x] Verify the `errors` array in `APIError` contains only user-facing messages, not internal field names or database column names. **PASS** — error messages are user-facing validation messages. -- [x] Map all 34 TypeScript route files and verify each has appropriate auth middleware (Supabase, API key, admin, metrics dashboard, or public). **PASS** — F-013 resolved (legacy `/pendulum/fundEphemeral`, `/moonbeam/execute-xcm`, `/subsidize/*` endpoints removed); `/v1/ramp/*` and `/v1/ramp/quotes(/best)` use `requirePartnerOrUserAuth()` with ownership guards; `/v1/brla/*` uses `requireAuth`; `/v1/mykobo/profiles` (GET + POST) use `requireAuth` (F-068 resolved); `/v1/maintenance/*`, `/v1/admin/partners/:partnerName/api-keys`, `/v1/admin/profile-partner-assignments`, `/v1/admin/partner-pricing-configs`, and `/v1/admin/profile-roles` use `adminAuth`; `/v1/admin/api-client-events` uses `metricsDashboardAuth`; `/v1/webhook/*` uses `apiKeyAuth`. +- [x] Map all 36 TypeScript route files and verify each has appropriate auth middleware (Supabase, API key, admin, metrics dashboard, or public). **PASS** — F-013 resolved (legacy `/pendulum/fundEphemeral`, `/moonbeam/execute-xcm`, `/subsidize/*` endpoints removed); `/v1/ramp/*` and `/v1/ramp/quotes(/best)` use `requirePartnerOrUserAuth()` with ownership guards; `/v1/brla/*`, `/v1/mykobo/profiles` (GET + POST), and `/v1/wallets/*` use `requireAuth` (`/v1/wallets/*` also scopes every lookup and mutation to the authenticated profile); F-068 is resolved; `/v1/maintenance/*`, `/v1/admin/partners/:partnerName/api-keys`, `/v1/admin/profile-partner-assignments`, `/v1/admin/partner-pricing-configs`, and `/v1/admin/profile-roles` use `adminAuth`; `/v1/admin/api-client-events` uses `metricsDashboardAuth`; `/v1/webhook/*` uses `apiKeyAuth`. - [x] Active customer-entity selection is Supabase-authenticated, serialized on the profile row, owner-scoped, idempotent for an identical retry, and rejects mutation or ambiguity. - [x] Verify no route accidentally uses `publicKeyAuth` (public key only, no secret key) for operations that should require `apiKeyAuth` (secret key). **PASS** — auth middleware usage reviewed per route. - [N/A] Verify controllers do not pass raw `req.body` to database operations — check for Sequelize `.create(req.body)` or `.update(req.body)` patterns. **N/A** — deferred; requires comprehensive Sequelize usage audit. - [x] Verify no endpoint returns `process.env`, server config, or internal paths in responses. **PASS** — no endpoint exposes internal configuration. - [PARTIAL] Check whether Supabase auth cookies use `SameSite=Strict` or `SameSite=Lax` — and whether CSRF tokens are required for state-changing operations. **PARTIAL** — cookie parser enabled but cookie attributes not explicitly configured for `SameSite`. - [x] Verify the 404 handler does not reveal Express version or framework information. **PASS** — custom 404 handler returns generic JSON error. -- [x] Check all 27 route files for endpoints that accept file uploads — verify file size limits and type validation if present. **PASS** — no file upload endpoints found. +- [x] Check all 36 route files for endpoints that accept file uploads — verify file size limits and type validation if present. **PASS** — no file upload endpoints found. - [ ] Verify request ID middleware runs before routes and returns `X-Request-ID` without using request IDs for authorization. - [ ] Verify partner-facing API observability writes are best-effort and cannot alter response status, response body, or quote/ramp state. - [x] Verify active maintenance windows are enforced by the backend on quote creation and ramp register/update/start, not only by frontend UI state. diff --git a/docs/security-spec/README.md b/docs/security-spec/README.md index 62c24b176..bcc69071b 100644 --- a/docs/security-spec/README.md +++ b/docs/security-spec/README.md @@ -20,6 +20,7 @@ This directory contains the security specification for the Vortex cross-border p |---|---|---| | System Overview | `00-system-overview/architecture.md` | Trust boundaries, component map, data flows | | Supabase OTP Auth | `01-auth/supabase-otp.md` | Email OTP, session lifecycle, token handling | +| Privy Embedded Wallets | `01-auth/privy-embedded-wallets.md` | Profile-scoped wallet ownership, mode selection, signer binding | | API Key Auth | `01-auth/api-keys.md` | Dual-key system (pk\_/sk\_), validation, partner matching | | Admin Auth | `01-auth/admin-auth.md` | Admin bearer token, endpoint protection | | Ephemeral Accounts | `02-signing-keys/ephemeral-accounts.md` | Client-side key generation, multi-chain, storage | From 378fd9d997fd8dd27eed282531c73e60de327a8a Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 28 Jul 2026 11:12:28 +0200 Subject: [PATCH 13/16] fix(dashboard): raise Netlify build heap limit --- apps/dashboard/netlify.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/dashboard/netlify.toml b/apps/dashboard/netlify.toml index 5d198bc05..cb10472e6 100644 --- a/apps/dashboard/netlify.toml +++ b/apps/dashboard/netlify.toml @@ -23,6 +23,9 @@ command = "cd ../.. && bun install --frozen-lockfile && bun run build:shared && bun run build:dashboard" publish = "dist" +[build.environment] + NODE_OPTIONS = "--max-old-space-size=4096" + [[redirects]] from = "/*" to = "/index.html" From aa8c2d0343dc3264f37998239075374c615398c6 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 29 Jul 2026 15:45:18 +0200 Subject: [PATCH 14/16] feat(api): replace Privy wallet registration with CDP --- apps/api/.env.example | 7 +- .../src/api/controllers/wallets.controller.ts | 29 +++--- apps/api/src/api/routes/v1/index.ts | 2 +- apps/api/src/api/routes/v1/wallets.route.ts | 4 +- .../api/services/wallets/cdpWallet.service.ts | 83 ++++++++++++++++ .../services/wallets/privyWallet.service.ts | 85 ----------------- .../services/wallets/profileWallet.service.ts | 39 ++++---- apps/api/src/config/vars.test.ts | 13 ++- apps/api/src/config/vars.ts | 18 ++-- .../055-add-wallet-mode-to-profiles.ts | 2 +- .../migrations/056-create-profile-wallets.ts | 4 +- apps/api/src/models/profileWallet.model.ts | 4 +- apps/api/src/models/user.model.ts | 4 +- .../api/src/tests/wallets.integration.test.ts | 94 ++++++++++--------- 14 files changed, 197 insertions(+), 191 deletions(-) create mode 100644 apps/api/src/api/services/wallets/cdpWallet.service.ts delete mode 100644 apps/api/src/api/services/wallets/privyWallet.service.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 2cfc632e5..58d7c233b 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -21,10 +21,9 @@ SUPABASE_URL=https://your-project-id.supabase.co SUPABASE_ANON_KEY=your-anon-key-here SUPABASE_SERVICE_KEY=your-service-role-key-here -# Optional Privy embedded-wallet metadata verification. Keep the secret server-side. -PRIVY_WALLET_REGISTRATION_ENABLED=false -PRIVY_APP_ID= -PRIVY_APP_SECRET= +# Optional Coinbase CDP embedded-wallet ownership verification. +CDP_WALLET_REGISTRATION_ENABLED=false +CDP_PROJECT_ID= # Database DB_HOST=localhost diff --git a/apps/api/src/api/controllers/wallets.controller.ts b/apps/api/src/api/controllers/wallets.controller.ts index c830158de..240afa3db 100644 --- a/apps/api/src/api/controllers/wallets.controller.ts +++ b/apps/api/src/api/controllers/wallets.controller.ts @@ -2,10 +2,10 @@ import { Request, Response } from "express"; import httpStatus from "http-status"; import { UniqueConstraintError } from "sequelize"; import logger from "../../config/logger"; -import { PrivyWalletVerificationError } from "../services/wallets/privyWallet.service"; +import { CdpWalletVerificationError } from "../services/wallets/cdpWallet.service"; import { listProfileWallets, - registerPrivyWallet, + registerCdpWallet, setWalletMode, type WalletMode, WalletModeConflictError, @@ -28,7 +28,7 @@ function sendWalletModeConflict(res: Response, error: WalletModeConflictError): sendError(res, httpStatus.CONFLICT, error.kind === "active_ramp" ? "ACTIVE_RAMP" : "WALLET_NOT_REGISTERED", error.message); } -function serializeWallet(wallet: Awaited>) { +function serializeWallet(wallet: Awaited>) { return { address: wallet.address, chainType: wallet.chainType, @@ -62,8 +62,8 @@ export async function updateWalletMode(req: Request, res: Response): Promise { +export async function createCdpWallet(req: Request, res: Response): Promise { const profileId = requireUserId(req, res); if (!profileId) return; - const { address, providerWalletId } = (req.body ?? {}) as { address?: unknown; providerWalletId?: unknown }; - if (typeof address !== "string" || typeof providerWalletId !== "string") { - sendError(res, httpStatus.BAD_REQUEST, "INVALID_WALLET", "address and providerWalletId are required"); + const accessToken = req.headers.authorization?.slice("Bearer ".length); + const { address, cdpUserId } = (req.body ?? {}) as { address?: unknown; cdpUserId?: unknown }; + if (!accessToken || typeof address !== "string" || typeof cdpUserId !== "string") { + sendError(res, httpStatus.BAD_REQUEST, "INVALID_WALLET", "address and cdpUserId are required"); return; } try { - const wallet = await registerPrivyWallet(profileId, { address, providerWalletId }); - res.status(httpStatus.OK).json({ mode: "privy_embedded", wallet: serializeWallet(wallet) }); + const wallet = await registerCdpWallet(profileId, { accessToken, address, cdpUserId }); + res.status(httpStatus.OK).json({ mode: "cdp_embedded", wallet: serializeWallet(wallet) }); } catch (error) { if (error instanceof WalletModeConflictError) { sendWalletModeConflict(res, error); @@ -102,13 +103,13 @@ export async function createPrivyWallet(req: Request, res: Response): Promise void); router.patch("/mode", updateWalletMode as unknown as (req: Request, res: Response) => void); -router.post("/privy", createPrivyWallet as unknown as (req: Request, res: Response) => void); +router.post("/cdp", createCdpWallet as unknown as (req: Request, res: Response) => void); export default router; diff --git a/apps/api/src/api/services/wallets/cdpWallet.service.ts b/apps/api/src/api/services/wallets/cdpWallet.service.ts new file mode 100644 index 000000000..b86fd2dbc --- /dev/null +++ b/apps/api/src/api/services/wallets/cdpWallet.service.ts @@ -0,0 +1,83 @@ +import { getAddress, isAddress } from "viem"; +import { config } from "../../../config/vars"; + +interface CdpAuthenticationMethod { + sub?: string; + type: string; +} + +interface CdpEvmAccount { + address?: string; +} + +interface CdpEndUserResponse { + authenticationMethods: CdpAuthenticationMethod[]; + evmAccountObjects: CdpEvmAccount[]; + userId: string; +} + +export class CdpWalletVerificationError extends Error { + constructor( + message: string, + readonly kind: "disabled" | "not_found" | "ownership_mismatch" | "unavailable" + ) { + super(message); + this.name = "CdpWalletVerificationError"; + } +} + +function isEvmAccount(account: CdpEvmAccount): account is { address: string } { + return typeof account.address === "string" && isAddress(account.address); +} + +export async function verifyCdpWalletOwnership(input: { + accessToken: string; + cdpUserId: string; + profileId: string; + address: string; + signal?: AbortSignal; +}): Promise<{ address: string; cdpUserId: string }> { + if (!config.cdp.walletRegistrationEnabled) { + throw new CdpWalletVerificationError("CDP wallet registration is disabled", "disabled"); + } + + let response: Response; + try { + const userId = encodeURIComponent(input.cdpUserId); + const projectId = encodeURIComponent(config.cdp.projectId); + response = await fetch( + `https://api.cdp.coinbase.com/platform/v2/embedded-wallet-api/end-users/${userId}?projectID=${projectId}`, + { + headers: { + Authorization: `Bearer ${input.accessToken}` + }, + signal: input.signal ?? AbortSignal.timeout(10000) + } + ); + } catch (error) { + throw new CdpWalletVerificationError( + `CDP ownership verification failed: ${error instanceof Error ? error.message : String(error)}`, + "unavailable" + ); + } + + if (response.status === 404) { + throw new CdpWalletVerificationError("CDP user was not found", "not_found"); + } + if (!response.ok) { + throw new CdpWalletVerificationError(`CDP ownership verification returned ${response.status}`, "unavailable"); + } + + const user = (await response.json()) as CdpEndUserResponse; + const requestedAddress = getAddress(input.address); + const jwtIdentity = user.authenticationMethods.find(method => method.type === "jwt"); + const ownsAddress = user.evmAccountObjects + .filter(isEvmAccount) + .some(account => getAddress(account.address) === requestedAddress); + + if (user.userId !== input.cdpUserId || jwtIdentity?.sub !== input.profileId || !ownsAddress) { + throw new CdpWalletVerificationError("The CDP wallet does not belong to this Vortex profile", "ownership_mismatch"); + } + + return { address: requestedAddress, cdpUserId: user.userId }; +} diff --git a/apps/api/src/api/services/wallets/privyWallet.service.ts b/apps/api/src/api/services/wallets/privyWallet.service.ts deleted file mode 100644 index 36bdd09d9..000000000 --- a/apps/api/src/api/services/wallets/privyWallet.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { getAddress, isAddress } from "viem"; -import { config } from "../../../config/vars"; - -interface PrivyLinkedAccount { - id?: string; - address?: string; - type?: string; - chain_type?: string; - wallet_client_type?: string; -} - -interface PrivyUserResponse { - id: string; - linked_accounts: PrivyLinkedAccount[]; -} - -export class PrivyWalletVerificationError extends Error { - constructor( - message: string, - readonly kind: "disabled" | "not_found" | "ownership_mismatch" | "unavailable" - ) { - super(message); - this.name = "PrivyWalletVerificationError"; - } -} - -function isEmbeddedEvmWallet(account: PrivyLinkedAccount): boolean { - return ( - account.type === "wallet" && - account.chain_type === "ethereum" && - (account.wallet_client_type === "privy" || account.wallet_client_type === "privy-v2") && - typeof account.address === "string" && - isAddress(account.address) - ); -} - -export async function verifyPrivyWalletOwnership(input: { - profileId: string; - providerWalletId: string; - address: string; - signal?: AbortSignal; -}): Promise<{ address: string; privyUserId: string }> { - if (!config.privy.walletRegistrationEnabled) { - throw new PrivyWalletVerificationError("Privy wallet registration is disabled", "disabled"); - } - - const authorization = Buffer.from(`${config.privy.appId}:${config.privy.appSecret}`).toString("base64"); - let response: Response; - try { - response = await fetch("https://api.privy.io/v1/users/custom_auth/id", { - body: JSON.stringify({ custom_user_id: input.profileId }), - headers: { - Authorization: `Basic ${authorization}`, - "Content-Type": "application/json", - "privy-app-id": config.privy.appId - }, - method: "POST", - signal: input.signal ?? AbortSignal.timeout(10000) - }); - } catch (error) { - throw new PrivyWalletVerificationError( - `Privy ownership verification failed: ${error instanceof Error ? error.message : String(error)}`, - "unavailable" - ); - } - - if (response.status === 404) { - throw new PrivyWalletVerificationError("Privy user was not found for this Vortex profile", "not_found"); - } - if (!response.ok) { - throw new PrivyWalletVerificationError(`Privy ownership verification returned ${response.status}`, "unavailable"); - } - - const user = (await response.json()) as PrivyUserResponse; - const requestedAddress = getAddress(input.address); - const wallet = user.linked_accounts - .filter(isEmbeddedEvmWallet) - .find(account => account.id === input.providerWalletId && getAddress(account.address as string) === requestedAddress); - - if (!wallet) { - throw new PrivyWalletVerificationError("The Privy wallet does not belong to this Vortex profile", "ownership_mismatch"); - } - - return { address: requestedAddress, privyUserId: user.id }; -} diff --git a/apps/api/src/api/services/wallets/profileWallet.service.ts b/apps/api/src/api/services/wallets/profileWallet.service.ts index 60a6ccddb..6ed25a661 100644 --- a/apps/api/src/api/services/wallets/profileWallet.service.ts +++ b/apps/api/src/api/services/wallets/profileWallet.service.ts @@ -4,9 +4,9 @@ import { sequelize } from "../../../models"; import ProfileWallet from "../../../models/profileWallet.model"; import RampState from "../../../models/rampState.model"; import User from "../../../models/user.model"; -import { verifyPrivyWalletOwnership } from "./privyWallet.service"; +import { verifyCdpWalletOwnership } from "./cdpWallet.service"; -export type WalletMode = "external" | "privy_embedded" | null; +export type WalletMode = "external" | "cdp_embedded" | null; const TERMINAL_RAMP_PHASES = ["complete", "failed", "timedOut"]; @@ -65,19 +65,19 @@ export async function setWalletMode(profileId: string, mode: WalletMode): Promis const profile = await getLockedProfile(profileId, transaction); await assertNoActiveRamp(profileId, transaction); - if (mode === "privy_embedded") { + if (mode === "cdp_embedded") { const embeddedWallet = await ProfileWallet.findOne({ attributes: ["id"], transaction, where: { chainType: "ethereum", profileId, - provider: "privy", + provider: "cdp", status: "active" } }); if (!embeddedWallet) { - throw new WalletModeConflictError("An active verified Privy wallet is required for embedded mode", "missing_wallet"); + throw new WalletModeConflictError("An active verified CDP wallet is required for embedded mode", "missing_wallet"); } } @@ -86,18 +86,19 @@ export async function setWalletMode(profileId: string, mode: WalletMode): Promis }); } -export async function registerPrivyWallet( +export async function registerCdpWallet( profileId: string, - input: { providerWalletId: string; address: string } + input: { accessToken: string; cdpUserId: string; address: string } ): Promise { - if (!input.providerWalletId.trim() || !isAddress(input.address)) { - throw new WalletRegistrationConflictError("A valid Privy wallet ID and EVM address are required"); + if (!input.cdpUserId.trim() || !isAddress(input.address)) { + throw new WalletRegistrationConflictError("A valid CDP user ID and EVM address are required"); } - const verified = await verifyPrivyWalletOwnership({ + const verified = await verifyCdpWalletOwnership({ + accessToken: input.accessToken, address: input.address, - profileId, - providerWalletId: input.providerWalletId + cdpUserId: input.cdpUserId, + profileId }); return sequelize.transaction(async transaction => { @@ -108,7 +109,7 @@ export async function registerPrivyWallet( transaction, where: { [Op.or]: [ - { provider: "privy", providerWalletId: input.providerWalletId }, + { provider: "cdp", providerWalletId: input.cdpUserId }, { address: getAddress(verified.address), chainType: "ethereum" } ] } @@ -119,15 +120,15 @@ export async function registerPrivyWallet( const existingForProfile = await ProfileWallet.findOne({ transaction, - where: { chainType: "ethereum", profileId, provider: "privy", status: "active" } + where: { chainType: "ethereum", profileId, provider: "cdp", status: "active" } }); let wallet: ProfileWallet; if (existingForProfile) { if ( - existingForProfile.providerWalletId !== input.providerWalletId || + existingForProfile.providerWalletId !== input.cdpUserId || getAddress(existingForProfile.address) !== verified.address ) { - throw new WalletRegistrationConflictError("This profile already has a different active Privy wallet"); + throw new WalletRegistrationConflictError("This profile already has a different active CDP wallet"); } await existingForProfile.update({ lastUsedAt: new Date() }, { transaction }); wallet = existingForProfile; @@ -138,15 +139,15 @@ export async function registerPrivyWallet( chainType: "ethereum", lastUsedAt: new Date(), profileId, - provider: "privy", - providerWalletId: input.providerWalletId, + provider: "cdp", + providerWalletId: verified.cdpUserId, status: "active" }, { transaction } ); } - await profile.update({ walletMode: "privy_embedded" }, { transaction }); + await profile.update({ walletMode: "cdp_embedded" }, { transaction }); return wallet; }); } diff --git a/apps/api/src/config/vars.test.ts b/apps/api/src/config/vars.test.ts index 97801af07..fe0f7afea 100644 --- a/apps/api/src/config/vars.test.ts +++ b/apps/api/src/config/vars.test.ts @@ -109,22 +109,21 @@ describe("vars deployment environment validation", () => { expect(result.stderr).toContain("MONERIUM_CLIENT_ID"); }); - it("requires both Privy server credentials when wallet registration is enabled", async () => { + it("requires the CDP project ID when wallet registration is enabled", async () => { const result = await importVarsWithEnv({ + CDP_WALLET_REGISTRATION_ENABLED: "true", NODE_ENV: "test", - PRIVY_WALLET_REGISTRATION_ENABLED: "true" }); expect(result.exitCode).toBe(1); - expect(result.stderr).toContain("PRIVY_APP_ID and PRIVY_APP_SECRET"); + expect(result.stderr).toContain("CDP_PROJECT_ID"); }); - it("allows Privy wallet registration when both server credentials are present", async () => { + it("allows CDP wallet registration when the project ID is present", async () => { const result = await importVarsWithEnv({ + CDP_PROJECT_ID: "test-cdp-project", + CDP_WALLET_REGISTRATION_ENABLED: "true", NODE_ENV: "test", - PRIVY_APP_ID: "test-privy-app", - PRIVY_APP_SECRET: "test-privy-secret", - PRIVY_WALLET_REGISTRATION_ENABLED: "true" }); expect(result).toEqual({ exitCode: 0, stderr: "", stdout: "ok\n" }); diff --git a/apps/api/src/config/vars.ts b/apps/api/src/config/vars.ts index 1b53d1358..f3094c318 100644 --- a/apps/api/src/config/vars.ts +++ b/apps/api/src/config/vars.ts @@ -192,9 +192,8 @@ interface Config { defaults: { vortexEvmPayoutAddress: string | undefined; }; - privy: { - appId: string; - appSecret: string; + cdp: { + projectId: string; walletRegistrationEnabled: boolean; }; } @@ -203,6 +202,10 @@ export const config: Config = { adminSecret: process.env.ADMIN_SECRET || "", amplitudeWss: process.env.AMPLITUDE_WSS || "wss://rpc-amplitude.pendulumchain.tech", backendTestStarterAccount: process.env.BACKEND_TEST_STARTER_ACCOUNT, + cdp: { + projectId: process.env.CDP_PROJECT_ID || "", + walletRegistrationEnabled: process.env.CDP_WALLET_REGISTRATION_ENABLED === "true" + }, database: { database: process.env.DB_NAME || "vortex", dialect: "postgres", @@ -270,11 +273,6 @@ export const config: Config = { partnerApiKey: process.env.TRANSAK_API_KEY } }, - privy: { - appId: process.env.PRIVY_APP_ID || "", - appSecret: process.env.PRIVY_APP_SECRET || "", - walletRegistrationEnabled: process.env.PRIVY_WALLET_REGISTRATION_ENABLED === "true" - }, quote: { deltaDBasisPoints: parseFloat(process.env.DELTA_D_BASIS_POINTS || "0.3"), discountStateTimeoutMinutes: parseInt(process.env.DISCOUNT_STATE_TIMEOUT_MINUTES || "10", 10) @@ -328,8 +326,8 @@ if (config.deploymentEnv === "sandbox" && !config.sandboxEnabled) { throw new Error("DEPLOYMENT_ENV=sandbox requires SANDBOX_ENABLED=true"); } -if (config.privy.walletRegistrationEnabled && (!config.privy.appId || !config.privy.appSecret)) { - throw new Error("PRIVY_APP_ID and PRIVY_APP_SECRET are required when PRIVY_WALLET_REGISTRATION_ENABLED=true"); +if (config.cdp.walletRegistrationEnabled && !config.cdp.projectId) { + throw new Error("CDP_PROJECT_ID is required when CDP_WALLET_REGISTRATION_ENABLED=true"); } if (config.env === "production") { diff --git a/apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts b/apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts index e976469e5..21a9ce3ce 100644 --- a/apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts +++ b/apps/api/src/database/migrations/055-add-wallet-mode-to-profiles.ts @@ -10,7 +10,7 @@ export async function up(queryInterface: QueryInterface): Promise { name: "profiles_wallet_mode_check", type: "check", where: { - wallet_mode: { [Op.in]: ["external", "privy_embedded"] } + wallet_mode: { [Op.in]: ["external", "cdp_embedded"] } } }); } diff --git a/apps/api/src/database/migrations/056-create-profile-wallets.ts b/apps/api/src/database/migrations/056-create-profile-wallets.ts index ca346c976..e27bd36dc 100644 --- a/apps/api/src/database/migrations/056-create-profile-wallets.ts +++ b/apps/api/src/database/migrations/056-create-profile-wallets.ts @@ -39,7 +39,7 @@ export async function up(queryInterface: QueryInterface): Promise { }, provider: { allowNull: false, - defaultValue: "privy", + defaultValue: "cdp", type: DataTypes.STRING(32) }, provider_wallet_id: { @@ -62,7 +62,7 @@ export async function up(queryInterface: QueryInterface): Promise { fields: ["provider"], name: "profile_wallets_provider_check", type: "check", - where: { provider: { [Op.in]: ["privy"] } } + where: { provider: { [Op.in]: ["cdp"] } } }); await queryInterface.addConstraint("profile_wallets", { fields: ["chain_type"], diff --git a/apps/api/src/models/profileWallet.model.ts b/apps/api/src/models/profileWallet.model.ts index 0a195509d..bc1692176 100644 --- a/apps/api/src/models/profileWallet.model.ts +++ b/apps/api/src/models/profileWallet.model.ts @@ -1,7 +1,7 @@ import { DataTypes, Model, Optional } from "sequelize"; import sequelize from "../config/database"; -export type ProfileWalletProvider = "privy"; +export type ProfileWalletProvider = "cdp"; export type ProfileWalletChainType = "ethereum"; export type ProfileWalletStatus = "active" | "archived"; @@ -79,7 +79,7 @@ ProfileWallet.init( }, provider: { allowNull: false, - defaultValue: "privy", + defaultValue: "cdp", type: DataTypes.STRING(32) }, providerWalletId: { diff --git a/apps/api/src/models/user.model.ts b/apps/api/src/models/user.model.ts index 19cd04363..cac87d54f 100644 --- a/apps/api/src/models/user.model.ts +++ b/apps/api/src/models/user.model.ts @@ -5,7 +5,7 @@ export interface UserAttributes { id: string; // UUID from Supabase Auth email: string; activeCustomerEntityId: string | null; - walletMode: "external" | "privy_embedded" | null; + walletMode: "external" | "cdp_embedded" | null; createdAt: Date; updatedAt: Date; } @@ -16,7 +16,7 @@ class User extends Model implements User declare id: string; declare email: string; declare activeCustomerEntityId: string | null; - declare walletMode: "external" | "privy_embedded" | null; + declare walletMode: "external" | "cdp_embedded" | null; declare createdAt: Date; declare updatedAt: Date; } diff --git a/apps/api/src/tests/wallets.integration.test.ts b/apps/api/src/tests/wallets.integration.test.ts index 488257331..7f6ce7e7b 100644 --- a/apps/api/src/tests/wallets.integration.test.ts +++ b/apps/api/src/tests/wallets.integration.test.ts @@ -10,33 +10,30 @@ import { type FakeSupabaseAuth, installFakeSupabaseAuth, testUserToken } from ". import { startTestApp, type TestApp } from "../test-utils/test-app"; const WALLET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; -const WALLET_ID = "wallet_privy_test_1"; +const CDP_USER_ID = "cdp-user-test-1"; let api: TestApp; let fakeAuth: FakeSupabaseAuth; const guardedFetch = globalThis.fetch; -const originalPrivyConfig = { ...config.privy }; +const originalCdpConfig = { ...config.cdp }; +const cdpRequests: Array<{ authorization: string | null; url: string }> = []; function headers(token: string): Record { return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; } -function installPrivyResponse(address = WALLET_ADDRESS, walletId = WALLET_ID): void { +function installCdpResponse(profileId: string, address = WALLET_ADDRESS, cdpUserId = CDP_USER_ID): void { globalThis.fetch = (async (input, init) => { const url = input instanceof Request ? input.url : String(input); - if (url === "https://api.privy.io/v1/users/custom_auth/id") { - const body = JSON.parse(String(init?.body)) as { custom_user_id: string }; + if (url.startsWith("https://api.cdp.coinbase.com/platform/v2/embedded-wallet-api/end-users/")) { + cdpRequests.push({ + authorization: new Headers(init?.headers).get("Authorization"), + url + }); return Response.json({ - id: `privy-user-${body.custom_user_id}`, - linked_accounts: [ - { - address, - chain_type: "ethereum", - id: walletId, - type: "wallet", - wallet_client_type: "privy" - } - ] + authenticationMethods: [{ sub: profileId, type: "jwt" }], + evmAccountObjects: [{ address }], + userId: cdpUserId }); } return guardedFetch(input, init); @@ -51,19 +48,18 @@ beforeAll(async () => { afterAll(async () => { globalThis.fetch = guardedFetch; - Object.assign(config.privy, originalPrivyConfig); + Object.assign(config.cdp, originalCdpConfig); if (api) await api.close(); if (fakeAuth) fakeAuth.restore(); }); beforeEach(async () => { await resetTestDatabase(); - Object.assign(config.privy, { - appId: "test-privy-app", - appSecret: "test-privy-secret", + cdpRequests.length = 0; + Object.assign(config.cdp, { + projectId: "test-cdp-project", walletRegistrationEnabled: true }); - installPrivyResponse(); }); describe("wallet API", () => { @@ -78,12 +74,12 @@ describe("wallet API", () => { await ProfileWallet.create({ address: WALLET_ADDRESS, profileId: first.id, - providerWalletId: WALLET_ID + providerWalletId: CDP_USER_ID }); await ProfileWallet.create({ address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", profileId: second.id, - providerWalletId: "wallet_privy_test_2" + providerWalletId: "cdp-user-test-2" }); const response = await api.request("/v1/wallets", { @@ -92,7 +88,7 @@ describe("wallet API", () => { expect(response.status).toBe(200); const body = (await response.json()) as { mode: null; wallets: Array<{ providerWalletId: string }> }; expect(body.mode).toBeNull(); - expect(body.wallets.map(wallet => wallet.providerWalletId)).toEqual([WALLET_ID]); + expect(body.wallets.map(wallet => wallet.providerWalletId)).toEqual([CDP_USER_ID]); }); it("rejects invalid modes and mode changes during a nonterminal ramp", async () => { @@ -106,7 +102,7 @@ describe("wallet API", () => { expect(invalid.status).toBe(400); const unverifiedEmbedded = await api.request("/v1/wallets/mode", { - body: JSON.stringify({ mode: "privy_embedded" }), + body: JSON.stringify({ mode: "cdp_embedded" }), headers: headers(token), method: "PATCH" }); @@ -160,12 +156,13 @@ describe("wallet API", () => { } }); - it("verifies and idempotently registers a Privy wallet", async () => { + it("verifies and idempotently registers a CDP wallet", async () => { const user = await createTestUser({ email: "wallet-register@example.com" }); const token = testUserToken(user.id, user.email); + installCdpResponse(user.id); const request = () => - api.request("/v1/wallets/privy", { - body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + api.request("/v1/wallets/cdp", { + body: JSON.stringify({ address: WALLET_ADDRESS, cdpUserId: CDP_USER_ID }), headers: headers(token), method: "POST" }); @@ -174,20 +171,32 @@ describe("wallet API", () => { const second = await request(); expect(first.status).toBe(200); expect(second.status).toBe(200); + expect(cdpRequests).toEqual([ + { + authorization: `Bearer ${token}`, + url: `https://api.cdp.coinbase.com/platform/v2/embedded-wallet-api/end-users/${CDP_USER_ID}?projectID=test-cdp-project` + }, + { + authorization: `Bearer ${token}`, + url: `https://api.cdp.coinbase.com/platform/v2/embedded-wallet-api/end-users/${CDP_USER_ID}?projectID=test-cdp-project` + } + ]); expect(await ProfileWallet.count({ where: { profileId: user.id } })).toBe(1); await user.reload(); - expect(user.walletMode).toBe("privy_embedded"); + expect(user.walletMode).toBe("cdp_embedded"); }); it("rejects a wallet already registered to another profile", async () => { const first = await createTestUser({ email: "wallet-owner@example.com" }); const second = await createTestUser({ email: "wallet-stranger@example.com" }); - const register = (user: typeof first) => - api.request("/v1/wallets/privy", { - body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + const register = (user: typeof first) => { + installCdpResponse(user.id); + return api.request("/v1/wallets/cdp", { + body: JSON.stringify({ address: WALLET_ADDRESS, cdpUserId: CDP_USER_ID }), headers: headers(testUserToken(user.id, user.email)), method: "POST" }); + }; expect((await register(first)).status).toBe(200); const conflict = await register(second); @@ -195,19 +204,19 @@ describe("wallet API", () => { expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe("WALLET_CONFLICT"); }); - it("rejects mismatched Privy ownership without persisting metadata", async () => { + it("rejects mismatched CDP ownership without persisting metadata", async () => { const user = await createTestUser({ email: "wallet-mismatch@example.com" }); - installPrivyResponse("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "a-different-wallet"); + installCdpResponse(user.id, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - const response = await api.request("/v1/wallets/privy", { - body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + const response = await api.request("/v1/wallets/cdp", { + body: JSON.stringify({ address: WALLET_ADDRESS, cdpUserId: CDP_USER_ID }), headers: headers(testUserToken(user.id, user.email)), method: "POST" }); expect(response.status).toBe(403); expect(((await response.json()) as { error: { code: string } }).error.code).toBe( - "PRIVY_WALLET_NOT_VERIFIED" + "CDP_WALLET_NOT_VERIFIED" ); expect(await ProfileWallet.count()).toBe(0); }); @@ -215,9 +224,10 @@ describe("wallet API", () => { it("atomically rolls back registration when a ramp is active", async () => { const user = await createTestUser({ email: "wallet-active-ramp@example.com" }); await createTestRampState({ userId: user.id }); + installCdpResponse(user.id); - const response = await api.request("/v1/wallets/privy", { - body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + const response = await api.request("/v1/wallets/cdp", { + body: JSON.stringify({ address: WALLET_ADDRESS, cdpUserId: CDP_USER_ID }), headers: headers(testUserToken(user.id, user.email)), method: "POST" }); @@ -229,11 +239,11 @@ describe("wallet API", () => { expect(user.walletMode).toBeNull(); }); - it("fails closed when server-side Privy ownership verification is disabled", async () => { + it("fails closed when server-side CDP ownership verification is disabled", async () => { const user = await createTestUser({ email: "wallet-disabled@example.com" }); - config.privy.walletRegistrationEnabled = false; - const response = await api.request("/v1/wallets/privy", { - body: JSON.stringify({ address: WALLET_ADDRESS, providerWalletId: WALLET_ID }), + config.cdp.walletRegistrationEnabled = false; + const response = await api.request("/v1/wallets/cdp", { + body: JSON.stringify({ address: WALLET_ADDRESS, cdpUserId: CDP_USER_ID }), headers: headers(testUserToken(user.id, user.email)), method: "POST" }); From 3e62ca88478af165046e41a949ad424470f9ed72 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 29 Jul 2026 15:45:46 +0200 Subject: [PATCH 15/16] feat(repo): replace Privy wallet clients with CDP --- apps/cdp-spike/.env.example | 12 + apps/cdp-spike/README.md | 31 + apps/cdp-spike/index.html | 13 + apps/cdp-spike/package.json | 28 + apps/cdp-spike/server/verifyOwnership.test.ts | 104 +++ apps/cdp-spike/server/verifyOwnership.ts | 75 ++ apps/cdp-spike/src/HostPage.tsx | 133 ++++ apps/cdp-spike/src/WalletSpike.tsx | 559 ++++++++++++++ apps/cdp-spike/src/auth.ts | 98 +++ apps/cdp-spike/src/bscFees.test.ts | 11 + apps/cdp-spike/src/bscFees.ts | 9 + apps/cdp-spike/src/jwtMetadata.test.ts | 25 + apps/cdp-spike/src/jwtMetadata.ts | 24 + apps/cdp-spike/src/main.tsx | 41 ++ apps/cdp-spike/src/styles.css | 241 ++++++ apps/cdp-spike/src/vortexTypedData.test.ts | 43 ++ apps/cdp-spike/src/vortexTypedData.ts | 155 ++++ apps/cdp-spike/tsconfig.json | 23 + apps/cdp-spike/vite.config.ts | 75 ++ apps/dashboard/.env.example | 19 +- apps/dashboard/.gitignore | 1 + ...oice.spec.ts => wallet-cdp-choice.spec.ts} | 4 +- apps/dashboard/e2e/wallet-optionality.spec.ts | 2 +- apps/dashboard/package.json | 6 +- ...ivy.config.ts => playwright.cdp.config.ts} | 14 +- .../components/layout/ConnectWalletButton.tsx | 12 +- .../components/transfer/FundingMethods.tsx | 4 +- .../src/components/transfer/OnrampForm.tsx | 2 +- apps/dashboard/src/routes/_app/settings.tsx | 10 +- .../dashboard/src/services/api/wallets.api.ts | 8 +- apps/dashboard/src/services/auth.test.ts | 7 + apps/dashboard/src/services/auth.ts | 12 + .../src/services/transactions/userSigning.ts | 3 + .../src/wallets/CdpWalletRuntime.tsx | 216 ++++++ .../src/wallets/PrivyWalletRuntime.tsx | 242 ------ .../src/wallets/WalletExperienceProvider.tsx | 29 +- .../src/wallets/cdpSigningAdapter.ts | 120 +++ .../src/wallets/cdpWalletSelection.ts | 27 + apps/dashboard/src/wallets/config.test.ts | 34 +- apps/dashboard/src/wallets/config.ts | 28 +- .../src/wallets/privyWalletSelection.ts | 26 - apps/dashboard/src/wallets/signingAdapter.ts | 5 +- .../src/wallets/walletIdentity.test.ts | 24 +- .../wallets/walletSigning.contract.test.ts | 20 +- apps/frontend/.env.example | 16 +- ...oice.spec.ts => wallet-cdp-choice.spec.ts} | 2 +- apps/frontend/package.json | 6 +- ...ivy.config.ts => playwright.cdp.config.ts} | 10 +- .../buttons/EVMWalletButton/index.tsx | 4 +- apps/frontend/src/contexts/network.tsx | 2 +- apps/frontend/src/hooks/useVortexAccount.ts | 2 +- .../src/services/api/wallets.service.ts | 8 +- apps/frontend/src/services/auth.test.ts | 9 +- apps/frontend/src/services/auth.ts | 12 + .../src/services/transactions/userSigning.ts | 3 + .../src/wallets/CdpWidgetWalletRuntime.tsx | 218 ++++++ .../src/wallets/PrivyWidgetWalletRuntime.tsx | 240 ------ .../src/wallets/WidgetWalletProvider.tsx | 29 +- .../frontend/src/wallets/cdpSigningAdapter.ts | 111 +++ .../src/wallets/cdpWalletSelection.ts | 27 + apps/frontend/src/wallets/config.test.ts | 30 +- apps/frontend/src/wallets/config.ts | 35 +- .../src/wallets/privyWalletSelection.ts | 26 - apps/frontend/src/wallets/signingAdapter.ts | 5 +- .../src/wallets/walletIdentity.test.ts | 25 +- .../wallets/walletSigning.contract.test.ts | 11 +- bun.lock | 692 ++++++++---------- package.json | 4 +- 68 files changed, 2968 insertions(+), 1134 deletions(-) create mode 100644 apps/cdp-spike/.env.example create mode 100644 apps/cdp-spike/README.md create mode 100644 apps/cdp-spike/index.html create mode 100644 apps/cdp-spike/package.json create mode 100644 apps/cdp-spike/server/verifyOwnership.test.ts create mode 100644 apps/cdp-spike/server/verifyOwnership.ts create mode 100644 apps/cdp-spike/src/HostPage.tsx create mode 100644 apps/cdp-spike/src/WalletSpike.tsx create mode 100644 apps/cdp-spike/src/auth.ts create mode 100644 apps/cdp-spike/src/bscFees.test.ts create mode 100644 apps/cdp-spike/src/bscFees.ts create mode 100644 apps/cdp-spike/src/jwtMetadata.test.ts create mode 100644 apps/cdp-spike/src/jwtMetadata.ts create mode 100644 apps/cdp-spike/src/main.tsx create mode 100644 apps/cdp-spike/src/styles.css create mode 100644 apps/cdp-spike/src/vortexTypedData.test.ts create mode 100644 apps/cdp-spike/src/vortexTypedData.ts create mode 100644 apps/cdp-spike/tsconfig.json create mode 100644 apps/cdp-spike/vite.config.ts rename apps/dashboard/e2e/{wallet-privy-choice.spec.ts => wallet-cdp-choice.spec.ts} (81%) rename apps/dashboard/{playwright.privy.config.ts => playwright.cdp.config.ts} (64%) create mode 100644 apps/dashboard/src/wallets/CdpWalletRuntime.tsx delete mode 100644 apps/dashboard/src/wallets/PrivyWalletRuntime.tsx create mode 100644 apps/dashboard/src/wallets/cdpSigningAdapter.ts create mode 100644 apps/dashboard/src/wallets/cdpWalletSelection.ts delete mode 100644 apps/dashboard/src/wallets/privyWalletSelection.ts rename apps/frontend/e2e/{wallet-privy-choice.spec.ts => wallet-cdp-choice.spec.ts} (83%) rename apps/frontend/{playwright.privy.config.ts => playwright.cdp.config.ts} (73%) create mode 100644 apps/frontend/src/wallets/CdpWidgetWalletRuntime.tsx delete mode 100644 apps/frontend/src/wallets/PrivyWidgetWalletRuntime.tsx create mode 100644 apps/frontend/src/wallets/cdpSigningAdapter.ts create mode 100644 apps/frontend/src/wallets/cdpWalletSelection.ts delete mode 100644 apps/frontend/src/wallets/privyWalletSelection.ts diff --git a/apps/cdp-spike/.env.example b/apps/cdp-spike/.env.example new file mode 100644 index 000000000..d50903131 --- /dev/null +++ b/apps/cdp-spike/.env.example @@ -0,0 +1,12 @@ +# Public CDP project identifier. The project must use custom authentication. +VITE_CDP_PROJECT_ID= + +# Vortex API that issues and verifies the existing Supabase JWT. +VITE_API_URL=http://localhost:3000 + +# Optional RPC overrides. Public viem defaults are used when omitted. +VITE_BASE_SEPOLIA_RPC_URL= +VITE_BSC_TESTNET_RPC_URL= + +# Optional exact parent origin when testing against a deployed host. +VITE_SPIKE_PARENT_ORIGIN= diff --git a/apps/cdp-spike/README.md b/apps/cdp-spike/README.md new file mode 100644 index 000000000..895fb02f5 --- /dev/null +++ b/apps/cdp-spike/README.md @@ -0,0 +1,31 @@ +# CDP embedded-wallet compatibility spike + +This disposable app tests Coinbase CDP against Vortex's existing wallet invariants without changing either the +dashboard or widget wallet provider. + +It covers: + +- Supabase custom-auth restoration to the same EOA; +- independent server-side `sub` and address ownership verification; +- the current ERC-20 permit, salted permit, TokenRelayer payload, and Permit2 EIP-712 shapes; +- raw EVM signing for chains outside CDP's direct-send list; +- Base Sepolia direct send and BSC testnet raw-sign-and-broadcast paths; +- secure export inside a cross-origin parent iframe; +- six concurrent browser contexts to exercise Temporary Wallet Secret eviction. + +## Run + +1. Copy `.env.example` to `.env.local` and fill in the CDP project ID and Vortex API URL. +2. From the repository root, run `bun install`. +3. Run `bun run --cwd apps/cdp-spike dev`. +4. Open `http://127.0.0.1:5190/?role=host`. + +The host loads the wallet app from `http://localhost:5190`, making it cross-origin without requiring a second server. +Only the wallet origin needs CDP access, so `http://localhost:5190` must be allowlisted in the CDP project; CDP does +not return its CORS header for the equivalent `127.0.0.1` origin. The app uses Vortex's normal email OTP endpoints; +it proxies those requests through the local Vite server so the API's production CORS policy does not need to allow +a development origin. It never asks for or stores a CDP Wallet Secret, and it does not enable delegation or smart +accounts. + +The two broadcast gates are intentionally user-triggered. They send zero-value self-transfers on testnets but still +consume testnet gas. diff --git a/apps/cdp-spike/index.html b/apps/cdp-spike/index.html new file mode 100644 index 000000000..83c969f97 --- /dev/null +++ b/apps/cdp-spike/index.html @@ -0,0 +1,13 @@ + + + + + + + Vortex CDP compatibility spike + + +
+ + + diff --git a/apps/cdp-spike/package.json b/apps/cdp-spike/package.json new file mode 100644 index 000000000..6a13c256e --- /dev/null +++ b/apps/cdp-spike/package.json @@ -0,0 +1,28 @@ +{ + "dependencies": { + "@coinbase/cdp-core": "0.0.119", + "@coinbase/cdp-hooks": "0.0.119", + "@coinbase/cdp-react": "0.0.119", + "react": "19.2.0", + "react-dom": "19.2.0", + "viem": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.2.0", + "typescript": "catalog:", + "vite": "^7.3.5" + }, + "name": "vortex-cdp-spike", + "private": true, + "scripts": { + "build": "vite build", + "dev": "vite --host 0.0.0.0 --port 5190", + "test": "bun test", + "typecheck": "tsc --noEmit" + }, + "type": "module", + "version": "0.0.0" +} diff --git a/apps/cdp-spike/server/verifyOwnership.test.ts b/apps/cdp-spike/server/verifyOwnership.test.ts new file mode 100644 index 000000000..07a365ba6 --- /dev/null +++ b/apps/cdp-spike/server/verifyOwnership.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "bun:test"; +import { verifyCdpOwnership } from "./verifyOwnership"; + +const ADDRESS = "0x1111111111111111111111111111111111111111"; +const OTHER_ADDRESS = "0x2222222222222222222222222222222222222222"; + +function response(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { status }); +} + +function fetchSequence(responses: Response[]): typeof fetch { + return (async () => { + const next = responses.shift(); + if (!next) throw new Error("Unexpected fetch"); + return next; + }) as unknown as typeof fetch; +} + +describe("CDP ownership verification", () => { + it("accepts only when Vortex subject, CDP JWT subject, and address agree", async () => { + const evidence = await verifyCdpOwnership( + { + accessToken: "supabase-token", + address: ADDRESS, + cdpProjectId: "project-id", + cdpUserId: "cdp-user-1", + vortexApiUrl: "https://api.example" + }, + fetchSequence([ + response({ user_id: "supabase-user-1", valid: true }), + response({ + authenticationMethods: [{ kid: "key-1", sub: "supabase-user-1", type: "jwt" }], + evmAccountObjects: [{ address: ADDRESS }], + userId: "cdp-user-1" + }) + ]) + ); + + expect(evidence).toEqual({ + address: ADDRESS, + cdpUserId: "cdp-user-1", + supabaseSubject: "supabase-user-1" + }); + }); + + it("rejects a CDP user bound to another Supabase subject", async () => { + await expect( + verifyCdpOwnership( + { + accessToken: "supabase-token", + address: ADDRESS, + cdpProjectId: "project-id", + cdpUserId: "cdp-user-2", + vortexApiUrl: "https://api.example" + }, + fetchSequence([ + response({ user_id: "supabase-user-1", valid: true }), + response({ + authenticationMethods: [{ kid: "key-1", sub: "supabase-user-2", type: "jwt" }], + evmAccountObjects: [{ address: ADDRESS }], + userId: "cdp-user-2" + }) + ]) + ) + ).rejects.toThrow("not bound to the authenticated Supabase subject"); + }); + + it("rejects an address not returned for the authenticated CDP user", async () => { + await expect( + verifyCdpOwnership( + { + accessToken: "supabase-token", + address: ADDRESS, + cdpProjectId: "project-id", + cdpUserId: "cdp-user-1", + vortexApiUrl: "https://api.example" + }, + fetchSequence([ + response({ user_id: "supabase-user-1", valid: true }), + response({ + authenticationMethods: [{ kid: "key-1", sub: "supabase-user-1", type: "jwt" }], + evmAccountObjects: [{ address: OTHER_ADDRESS }], + userId: "cdp-user-1" + }) + ]) + ) + ).rejects.toThrow("does not own the requested EVM account"); + }); + + it("fails closed when CDP refuses a cross-user lookup", async () => { + await expect( + verifyCdpOwnership( + { + accessToken: "supabase-token", + address: ADDRESS, + cdpProjectId: "project-id", + cdpUserId: "another-users-id", + vortexApiUrl: "https://api.example" + }, + fetchSequence([response({ user_id: "supabase-user-1", valid: true }), response({}, 403)]) + ) + ).rejects.toThrow("CDP rejected the ownership lookup (403)"); + }); +}); diff --git a/apps/cdp-spike/server/verifyOwnership.ts b/apps/cdp-spike/server/verifyOwnership.ts new file mode 100644 index 000000000..162ce8461 --- /dev/null +++ b/apps/cdp-spike/server/verifyOwnership.ts @@ -0,0 +1,75 @@ +import { getAddress } from "viem"; + +interface CdpAuthenticationMethod { + sub?: string; + type: string; +} + +interface CdpEndUser { + authenticationMethods: CdpAuthenticationMethod[]; + evmAccountObjects: Array<{ address: string }>; + userId: string; +} + +interface VerifyOwnershipInput { + accessToken: string; + address: string; + cdpProjectId: string; + cdpUserId: string; + vortexApiUrl: string; +} + +export interface OwnershipEvidence { + address: string; + cdpUserId: string; + supabaseSubject: string; +} + +export async function verifyCdpOwnership( + input: VerifyOwnershipInput, + fetchImplementation: typeof fetch = fetch +): Promise { + const vortexResponse = await fetchImplementation(`${input.vortexApiUrl.replace(/\/$/, "")}/v1/auth/verify`, { + body: JSON.stringify({ access_token: input.accessToken }), + headers: { "Content-Type": "application/json" }, + method: "POST" + }); + if (!vortexResponse.ok) { + throw new Error(`Vortex rejected the Supabase token (${vortexResponse.status})`); + } + + const vortexIdentity = (await vortexResponse.json()) as { user_id?: string; valid?: boolean }; + if (!vortexIdentity.valid || !vortexIdentity.user_id) { + throw new Error("Vortex did not return a valid Supabase subject"); + } + + const cdpUrl = new URL( + `/platform/v2/embedded-wallet-api/end-users/${encodeURIComponent(input.cdpUserId)}`, + "https://api.cdp.coinbase.com" + ); + cdpUrl.searchParams.set("projectID", input.cdpProjectId); + const cdpResponse = await fetchImplementation(cdpUrl, { + headers: { Authorization: `Bearer ${input.accessToken}` } + }); + if (!cdpResponse.ok) { + throw new Error(`CDP rejected the ownership lookup (${cdpResponse.status})`); + } + + const cdpUser = (await cdpResponse.json()) as CdpEndUser; + const jwtIdentity = cdpUser.authenticationMethods.find(method => method.type === "jwt"); + if (cdpUser.userId !== input.cdpUserId || jwtIdentity?.sub !== vortexIdentity.user_id) { + throw new Error("CDP user is not bound to the authenticated Supabase subject"); + } + + const requestedAddress = getAddress(input.address); + const ownsAddress = cdpUser.evmAccountObjects.some(account => getAddress(account.address) === requestedAddress); + if (!ownsAddress) { + throw new Error("CDP user does not own the requested EVM account"); + } + + return { + address: requestedAddress, + cdpUserId: cdpUser.userId, + supabaseSubject: vortexIdentity.user_id + }; +} diff --git a/apps/cdp-spike/src/HostPage.tsx b/apps/cdp-spike/src/HostPage.tsx new file mode 100644 index 000000000..d687c281b --- /dev/null +++ b/apps/cdp-spike/src/HostPage.tsx @@ -0,0 +1,133 @@ +import { useEffect, useMemo, useRef, useState } from "react"; + +interface ContextStatus { + address?: string; + detail: string; + status: "fail" | "pass" | "pending"; + userId?: string; +} + +interface SpikeMessage { + address?: string; + contextId?: string; + detail?: string; + source?: string; + type?: string; + userId?: string; +} + +function alternativeLocalOrigin(): string { + const url = new URL(window.location.href); + url.hostname = url.hostname === "localhost" ? "127.0.0.1" : "localhost"; + url.pathname = "/"; + url.search = ""; + url.hash = ""; + return url.origin; +} + +export function HostPage() { + const walletOrigin = useMemo(alternativeLocalOrigin, []); + const [contextCount, setContextCount] = useState(1); + const [contexts, setContexts] = useState>({}); + const frames = useRef>({}); + + useEffect(() => { + const onMessage = (event: MessageEvent) => { + if (event.origin !== walletOrigin || event.data.source !== "vortex-cdp-spike" || !event.data.contextId) return; + const contextId = event.data.contextId; + if (event.data.type === "context-ready") { + setContexts(current => ({ + ...current, + [contextId]: { + address: event.data.address, + detail: "Authenticated and EOA restored", + status: "pass", + userId: event.data.userId + } + })); + } + if (event.data.type === "sign-result") { + setContexts(current => ({ + ...current, + [contextId]: { + ...current[contextId], + detail: event.data.detail ?? "No result detail", + status: event.data.detail?.startsWith("PASS") ? "pass" : "fail" + } + })); + } + }; + window.addEventListener("message", onMessage); + return () => window.removeEventListener("message", onMessage); + }, [walletOrigin]); + + const runFirstContextAfterEviction = () => { + setContexts(current => ({ + ...current, + "1": { ...current["1"], detail: "Running signature after all contexts authenticated", status: "pending" } + })); + frames.current["1"]?.contentWindow?.postMessage({ source: "vortex-cdp-spike", type: "run-sign-gate" }, walletOrigin); + }; + + const readyContexts = Object.values(contexts).filter(context => context.status === "pass").length; + + return ( +
+
+

Outer partner page · {window.location.origin}

+

CDP nested-widget and session stress harness

+

+ The wallet frames below run on {walletOrigin}, so Coinbase export is nested inside a real cross-origin + iframe. +

+
+ + Open dashboard-origin wallet + + + +
+

+ First authenticate and create the EOA in context 1. Its Vortex session is shared with the additional frames on the + wallet origin; they will authenticate with CDP automatically. +

+
+ +
+ {Array.from({ length: contextCount }, (_, index) => { + const contextId = String(index + 1); + const status = contexts[contextId]; + return ( +
+ Context {contextId} + {status?.detail ?? "Waiting"} + {status?.address && {status.address}} +
+ ); + })} +
+ +
+ {Array.from({ length: contextCount }, (_, index) => { + const contextId = String(index + 1); + const src = `${walletOrigin}/?auto=1&context=${contextId}&parentOrigin=${encodeURIComponent(window.location.origin)}`; + return ( +