diff --git a/.npmrc b/.npmrc index 5660f81..214c29d 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -registry=https://registry.npmjs.org/ \ No newline at end of file +registry=https://registry.npmjs.org/ diff --git a/README.md b/README.md index 8ea6fc0..d8c40de 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,18 @@ # sonar-example-nextjs -Example Next.js app showing how to inetgrate with the Sonar API via the `sonar-react` and `sonar-core` libraries. - -There is an integration guide for these libraries [here](https://docs.echo.xyz/sonar/integration-guides/react). - -The example app demonstrates how to: - -- Setup providers in `src/app/Provider.tsx` -- Authenticate with Sonar via the oauth flow - - See `src/app/components/auth/AuthenticationSection.tsx` on how to create the login/logout buttons - - See `src/app/oauth/callback/page.tsx` for an example of the oauth callback handler -- Prior to a sale going live, a way to list the state of all of a user's entities - - See `src/app/page.tsx` while in the `!saleIsLive` state -- When sale is live, display setup/eligibily state of the entity on Sonar that is linked to the currently connected wallet - - See `src/app/page.tsx` while in the `saleIsLive` state -- Surface the user's entity setup/eligibility state - - See components in `src/app/components/entity` -- Run prepurchase checks - - See `src/app/components/sale/PurchaseCard.tsx` for an example of how to run these checks and interpret the result -- Submit a purchase transaction to an example sale contract - - See the `ReadyToPurchaseSection` in `src/app/components/sale/PurchaseCard.tsx` for an example of how to generate a purchase permit and pass this to the contract, - using the `useSaleContract` hook in `src/app/hooks.ts` - -## Running the app locally +A **backend-focused** example Next.js app showing how to integrate with the Sonar API. + +There is an integration guide for the Sonar libraries [here](https://docs.echo.xyz/sonar/integration-guides/react). + +This example implements a backend OAuth flow where tokens are stored server-side and all Sonar API requests are proxied through the backend. For a simpler frontend-only approach where tokens are managed client-side, see [sonar-example-react](https://github.com/sunrisedotdev/sonar-example-react). + +## Why Use the Backend Approach? + +This approach is more secure than a frontend-only approach since the access tokens stay on the server and do not need to be sent to the client at all. + +However it does increase the complexity, which might not be worth it if you already have a frontend-only single page app. + +## Running the App Locally Set the required env vars listed in `src/app/config.ts` (or update that file). You can find the values for your sale on the [Echo founder dashboard](https://app.echo.xyz/founder). @@ -31,3 +21,137 @@ You can find the values for your sale on the [Echo founder dashboard](https://ap pnpm i pnpm dev ``` + +## What This Example Demonstrates + +- **OAuth authentication with Sonar** via a secure backend flow with PKCE +- **Token management** — server-side storage with automatic refresh +- **Entity state display** — prior to sale, list all user entities; during sale, show linked entity status +- **Pre-purchase checks** — validate eligibility before transactions +- **Purchase transactions** — generate permits and submit to the sale contract + +## Authentication Architecture + +For demonstration purposes, this example uses a minimal session system: + +- **Login** backend creates a random session ID stored in an HTTP-only cookie (no authentication required) +- **Logout** clears the session and any associated Sonar tokens + +### OAuth Flow + +The backend handles the complete OAuth flow, storing tokens securely server-side: + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Browser │ │ Next.js │ │ Echo │ +│ │ │ Backend │ │ OAuth │ +└────┬────┘ └────┬────┘ └────┬────┘ + │ │ │ + │ 1. Click "Connect" │ │ + ├────────────────────────────>│ │ + │ │ │ + │ │ 2. Generate PKCE │ + │ │ params & store │ + │ │ verifier │ + │ │ │ + │ 3. Return redirect │ │ + │ URL │ │ + │<────────────────────────────│ │ + │ │ │ + │ 4. Navigate to Echo OAuth │ │ + ├──────────────────────────────────────────────────────────>│ + │ │ │ + │ 5. User authenticates & authorizes │ + │ (interactive session) │ │ + │ │ │ + │ 6. Redirect to callback with auth code │ + │<──────────────────────────────────────────────────────────│ + │ │ │ + │ 7. Send auth code │ │ + │ to backend │ │ + ├────────────────────────────>│ │ + │ │ │ + │ │ 8. Exchange code │ + │ │ for tokens │ + │ ├────────────────────────────>│ + │ │ │ + │ │ 9. Return tokens │ + │ │<────────────────────────────│ + │ │ │ + │ │ 10. Store tokens │ + │ │ server-side │ + │ │ │ + │ 11. Success │ │ + │ response │ │ + │<────────────────────────────│ │ + │ │ │ +``` + +### Token Refresh + +Access tokens expire after a set time. The backend automatically refreshes them: + +- Before each Sonar API call, the backend checks if the token expires within 5 minutes +- If so, it uses `SonarClient.refreshToken()` to get new tokens +- Refreshed tokens are stored back in the token store +- **Concurrent request handling**: If multiple requests need to refresh simultaneously, promise coalescing ensures only one refresh API call is made + +### Proxied API Requests + +Once authenticated, all Sonar API calls go through the backend, which handles token refresh automatically: + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Browser │ │ Next.js │ │ Sonar │ +│ │ │ Backend │ │ API │ +└────┬────┘ └────┬────┘ └────┬────┘ + │ │ │ + │ 1. POST /api/sonar/entities │ │ + │ { saleUUID: "..." } │ │ + ├────────────────────────────>│ │ + │ │ │ + │ │ 2. Verify session │ + │ │ & get tokens │ + │ │ │ + │ │ 3. Refresh token │ + │ │ if expiring │ + │ │ │ + │ │ 4. GET /entities │ + │ │ Authorization: Bearer... │ + │ ├────────────────────────────>│ + │ │ │ + │ │ 5. Response │ + │ │<────────────────────────────│ + │ │ │ + │ 6. Forward response │ │ + │<────────────────────────────│ │ + │ │ │ +``` + +## Project Structure + +``` +src/ +├── app/ +│ ├── api/ +│ │ ├── auth/ # Minimal session management +│ │ └── sonar/ # Proxied Sonar API routes (entities, pre-purchase, etc.) +│ ├── components/ +│ │ ├── auth/ # Login/logout UI +│ │ ├── entity/ # Entity display components +│ │ ├── registration/ # Pre-sale entity list & eligibility +│ │ └── sale/ # Purchase flow UI +│ ├── hooks/ +│ │ ├── use-session.tsx # Session state context & hook +│ │ └── use-sonar-*.ts # React hooks for Sonar API calls +│ ├── oauth/callback/ # OAuth callback page (frontend) +│ ├── config.ts # Environment configuration +│ ├── page.tsx # Main page +│ └── Provider.tsx # App providers setup +└── lib/ + ├── session.ts # Cookie-based session management + ├── token-store.ts # In-memory token storage (swap for DB in production) + ├── pkce-store.ts # PKCE verifier storage for OAuth + ├── sonar-client.ts # SonarClient factory + └── sonar-route-handler.ts # Authenticated route handler with token refresh +``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3150202..fac8360 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: version: 0.12.0(react@18.3.1) '@tanstack/react-query': specifier: ^5.90.12 - version: 5.90.12(react@18.3.1) + version: 5.90.16(react@18.3.1) connectkit: specifier: ^1.9.1 - version: 1.9.1(@babel/core@7.28.5)(@tanstack/react-query@5.90.12(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + version: 1.9.1(@babel/core@7.28.5)(@tanstack/react-query@5.90.16(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) next: specifier: ^15.5.7 version: 15.5.7(@babel/core@7.28.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -34,7 +34,7 @@ importers: version: 18.3.1(react@18.3.1) wagmi: specifier: ^2.19.5 - version: 2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + version: 2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) devDependencies: '@eslint/eslintrc': specifier: ^3.3.3 @@ -44,7 +44,7 @@ importers: version: 4.1.17 '@types/node': specifier: ^20.19.26 - version: 20.19.26 + version: 20.19.27 '@types/react': specifier: ^18.3.27 version: 18.3.27 @@ -71,7 +71,7 @@ importers: version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@20.19.25)(jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2) + version: 2.1.9(@types/node@22.7.5)(jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2) ../sonar/packages/sonar-react: dependencies: @@ -102,7 +102,7 @@ importers: version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@20.19.25)(jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2) + version: 2.1.9(@types/node@22.7.5)(jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2) packages: @@ -213,8 +213,8 @@ packages: '@base-org/account@2.4.0': resolution: {integrity: sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug==} - '@coinbase/cdp-sdk@1.40.1': - resolution: {integrity: sha512-VZxAUYvWbqM4gw/ZHyr9fKBlCAKdMbBQzJxpV9rMUNkdulHIrj0cko2Mw3dyVyw+gdT62jAVxzVkPuQTRnECLw==} + '@coinbase/cdp-sdk@1.38.5': + resolution: {integrity: sha512-j8mvx1wMox/q2SjB7C09HtdRXVOpGpfkP7nG4+OjdowPj8pmQ03rigzycd86L8mawl6TUPXdm41YSQVmtc8SzQ==} '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} @@ -1586,11 +1586,11 @@ packages: '@tailwindcss/postcss@4.1.17': resolution: {integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==} - '@tanstack/query-core@5.90.12': - resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + '@tanstack/query-core@5.90.16': + resolution: {integrity: sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==} - '@tanstack/react-query@5.90.12': - resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + '@tanstack/react-query@5.90.16': + resolution: {integrity: sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==} peerDependencies: react: ^18 || ^19 @@ -1634,8 +1634,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/lodash@4.17.21': - resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1643,11 +1643,11 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@20.19.25': - resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@types/node@20.19.27': + resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} - '@types/node@20.19.26': - resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -1675,63 +1675,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.49.0': - resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.49.0 + '@typescript-eslint/parser': ^8.46.4 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.49.0': - resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.49.0': - resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.49.0': - resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.49.0': - resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.49.0': - resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.49.0': - resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.49.0': - resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.49.0': - resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.49.0': - resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -2006,8 +2006,8 @@ packages: zod: optional: true - abitype@1.2.2: - resolution: {integrity: sha512-4DOIMWscIB3j8hboLAUjLZCE8TMLdgecBpHFumfU4PdO/C1SBCVx4Nu1wPYXaL2iK8B0Jk3tiwnDLCpUtm3fZg==} + abitype@1.1.1: + resolution: {integrity: sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q==} peerDependencies: typescript: '>=5.0.4' zod: ^3.25.76 @@ -2165,8 +2165,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.6: - resolution: {integrity: sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==} + baseline-browser-mapping@2.9.5: + resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} hasBin: true bidi-js@1.0.3: @@ -2181,8 +2181,8 @@ packages: borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} - bowser@2.13.1: - resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2760,8 +2760,8 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} - family@0.1.6: - resolution: {integrity: sha512-ulHRThfhz+glfmVfSPcU16N1hZ/uj0Y1D79vl0PeJ3yFfgXlkiBbVwXRJhDQuP8oizxBCl16KT2PrMleiYwrPw==} + family@0.1.4: + resolution: {integrity: sha512-qRlQA7v0O36EorMeTJ3MzlQ5vjFUDF1zYT3qU0hHREls+lN6+YJZc0Ifv1JWWk/8jSUHZcroYrWKjBwucU6+HQ==} peerDependencies: react: 17.x || 18.x || 19.x react-dom: 17.x || 18.x || 19.x @@ -2784,6 +2784,10 @@ packages: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2862,8 +2866,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} framer-motion@6.5.1: @@ -2944,6 +2948,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -2984,8 +2991,8 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.10.8: - resolution: {integrity: sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww==} + hono@4.10.6: + resolution: {integrity: sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==} engines: {node: '>=16.9.0'} html-encoding-sniffer@4.0.0: @@ -3207,8 +3214,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + jose@6.1.1: + resolution: {integrity: sha512-GWSqjfOPf4cWOkBzw5THBjtGPhXKqYnfRBzh4Ni+ArTrQQ9unvmsA3oFLqaYKoKe5sjWmGu5wVKg9Ft1i+LQfg==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -3535,8 +3542,8 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-mock-http@1.0.4: - resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-mock-http@1.0.3: + resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -3619,8 +3626,8 @@ packages: typescript: optional: true - ox@0.9.17: - resolution: {integrity: sha512-rKAnhzhRU3Xh3hiko+i1ZxywZ55eWQzeS/Q4HRKLx2PqfHOolisZHErSsJVipGlmQKHW5qwOED/GighEw9dbLg==} + ox@0.9.14: + resolution: {integrity: sha512-lxZYCzGH00WtIPPrqXCrbSW/ZiKjigfII6R0Vu1eH2GpobmcwVheiivbCvsBZzmVZcNpwkabSamPP+ZNtdnKIQ==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -3957,8 +3964,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rpc-websockets@9.3.2: - resolution: {integrity: sha512-VuW2xJDnl1k8n8kjbdRSWawPRkwaVqUQNjE1TdeTawf0y0abGhtVJFTXCLfgpgGDBkO/Fj6kny8Dc/nvOW78MA==} + rpc-websockets@9.3.1: + resolution: {integrity: sha512-bY6a+i/lEtBJ/mUxwsCTgevoV1P0foXTVA7UoThzaIWbM+3NDqorf8NBWs5DmqKTFeA1IoNzgvkWjFCPgnzUiQ==} run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4367,6 +4374,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -4376,8 +4386,8 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unstorage@1.17.3: - resolution: {integrity: sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==} + unstorage@1.17.2: + resolution: {integrity: sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w==} peerDependencies: '@azure/app-configuration': ^1.8.0 '@azure/cosmos': ^4.2.0 @@ -4495,8 +4505,8 @@ packages: typescript: optional: true - viem@2.41.2: - resolution: {integrity: sha512-LYliajglBe1FU6+EH9mSWozp+gRA/QcHfxeD9Odf83AdH5fwUS7DroH4gHvlv6Sshqi1uXrYFA2B/EOczxd15g==} + viem@2.39.0: + resolution: {integrity: sha512-rCN+IfnMESlrg/iPyyVL+M9NS/BHzyyNy72470tFmbTuscY3iPaZGMtJDcHKKV8TC6HV9DjWk0zWX6cpu0juyA==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -4772,8 +4782,8 @@ packages: use-sync-external-store: optional: true - zustand@5.0.9: - resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -4931,14 +4941,14 @@ snapshots: '@base-org/account@2.4.0(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': dependencies: - '@coinbase/cdp-sdk': 1.40.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@coinbase/cdp-sdk': 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' @@ -4954,7 +4964,7 @@ snapshots: - ws - zod - '@coinbase/cdp-sdk@1.40.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/cdp-sdk@1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) @@ -4963,10 +4973,10 @@ snapshots: abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) axios: 1.13.2 axios-retry: 4.5.0(axios@1.13.2) - jose: 6.1.3 + jose: 6.1.1 md5: 2.3.0 uncrypto: 0.1.3 - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -4999,7 +5009,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' @@ -5291,11 +5301,11 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 - '@gemini-wallet/core@0.3.2(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@gemini-wallet/core@0.3.2(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -5481,7 +5491,7 @@ snapshots: '@metamask/onboarding@1.0.1': dependencies: - bowser: 2.13.1 + bowser: 2.12.1 '@metamask/providers@16.1.0': dependencies: @@ -5551,7 +5561,7 @@ snapshots: '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.16)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@metamask/sdk-install-modal-web': 0.32.1 '@paulmillr/qr': 0.2.1 - bowser: 2.13.1 + bowser: 2.12.1 cross-fetch: 4.1.0 debug: 4.3.4 eciesjs: 0.4.16 @@ -5579,7 +5589,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - '@types/lodash': 4.17.21 + '@types/lodash': 4.17.20 debug: 4.4.3(supports-color@5.5.0) lodash: 4.17.21 pony-cause: 2.1.11 @@ -5605,7 +5615,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.3.4 + debug: 4.4.3(supports-color@5.5.0) pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -5619,7 +5629,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.3.4 + debug: 4.4.3(supports-color@5.5.0) pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -5753,7 +5763,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -5766,7 +5776,7 @@ snapshots: '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@18.3.27)(react@18.3.1) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5916,7 +5926,7 @@ snapshots: '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@18.3.27)(react@18.3.1) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5970,7 +5980,7 @@ snapshots: '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@18.3.27)(react@18.3.1) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6082,7 +6092,7 @@ snapshots: '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -6109,7 +6119,7 @@ snapshots: '@scure/bip32@1.7.0': dependencies: - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 @@ -6527,7 +6537,7 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 '@noble/curves': 1.9.7 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) agentkeepalive: 4.6.0 @@ -6538,7 +6548,7 @@ snapshots: fast-stable-stringify: 1.0.0 jayson: 4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) node-fetch: 2.7.0 - rpc-websockets: 9.3.2 + rpc-websockets: 9.3.1 superstruct: 2.0.2 transitivePeerDependencies: - bufferutil @@ -6623,11 +6633,11 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.17 - '@tanstack/query-core@5.90.12': {} + '@tanstack/query-core@5.90.16': {} - '@tanstack/react-query@5.90.12(react@18.3.1)': + '@tanstack/react-query@5.90.16(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.90.12 + '@tanstack/query-core': 5.90.16 react: 18.3.1 '@testing-library/dom@10.4.1': @@ -6660,7 +6670,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 20.19.26 + '@types/node': 20.19.27 '@types/debug@4.1.12': dependencies: @@ -6672,20 +6682,20 @@ snapshots: '@types/json5@0.0.29': {} - '@types/lodash@4.17.21': {} + '@types/lodash@4.17.20': {} '@types/ms@2.1.0': {} '@types/node@12.20.55': {} - '@types/node@20.19.25': + '@types/node@20.19.27': dependencies: undici-types: 6.21.0 - optional: true - '@types/node@20.19.26': + '@types/node@22.7.5': dependencies: - undici-types: 6.21.0 + undici-types: 6.19.8 + optional: true '@types/prop-types@15.7.15': {} @@ -6713,21 +6723,22 @@ snapshots: '@types/ws@7.4.7': dependencies: - '@types/node': 20.19.26 + '@types/node': 20.19.27 '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.26 + '@types/node': 20.19.27 - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 eslint: 9.39.1(jiti@2.6.1) + graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -6735,41 +6746,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3(supports-color@5.5.0) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 debug: 4.4.3(supports-color@5.5.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.49.0': + '@typescript-eslint/scope-manager@8.46.4': dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3(supports-color@5.5.0) eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -6777,37 +6788,38 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.49.0': {} + '@typescript-eslint/types@8.46.4': {} - '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3(supports-color@5.5.0) + fast-glob: 3.3.3 + is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.3 - tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.49.0': + '@typescript-eslint/visitor-keys@8.46.4': dependencies: - '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/types': 8.46.4 eslint-visitor-keys: 4.2.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -6876,13 +6888,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.25)(lightningcss@1.30.2))': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.7.5)(lightningcss@1.30.2))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@20.19.25)(lightningcss@1.30.2) + vite: 5.4.21(@types/node@22.7.5)(lightningcss@1.30.2) '@vitest/pretty-format@2.1.9': dependencies: @@ -6909,19 +6921,19 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': dependencies: '@base-org/account': 2.4.0(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.3.2(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@gemini-wallet/core': 0.3.2(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -6963,14 +6975,14 @@ snapshots: - ws - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.0(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: - '@tanstack/query-core': 5.90.12 + '@tanstack/query-core': 5.90.16 typescript: 5.9.3 transitivePeerDependencies: - '@types/react' @@ -7162,7 +7174,7 @@ snapshots: dependencies: '@walletconnect/safe-json': 1.0.2 idb-keyval: 6.2.2 - unstorage: 1.17.3(idb-keyval@6.2.2) + unstorage: 1.17.2(idb-keyval@6.2.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7530,7 +7542,7 @@ snapshots: typescript: 5.9.3 zod: 3.25.76 - abitype@1.2.2(typescript@5.9.3)(zod@3.25.76): + abitype@1.1.1(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 zod: 3.25.76 @@ -7676,7 +7688,7 @@ snapshots: axios@1.13.2: dependencies: follow-redirects: 1.15.11 - form-data: 4.0.5 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -7705,7 +7717,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.9.6: {} + baseline-browser-mapping@2.9.5: {} bidi-js@1.0.3: dependencies: @@ -7721,7 +7733,7 @@ snapshots: bs58: 4.0.1 text-encoding-utf-8: 1.0.2 - bowser@2.13.1: {} + bowser@2.12.1: {} brace-expansion@1.1.12: dependencies: @@ -7738,7 +7750,7 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.6 + baseline-browser-mapping: 2.9.5 caniuse-lite: 1.0.30001760 electron-to-chromium: 1.5.267 node-releases: 2.0.27 @@ -7848,12 +7860,12 @@ snapshots: confbox@0.1.8: {} - connectkit@1.9.1(@babel/core@7.28.5)(@tanstack/react-query@5.90.12(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + connectkit@1.9.1(@babel/core@7.28.5)(@tanstack/react-query@5.90.16(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: - '@tanstack/react-query': 5.90.12(react@18.3.1) + '@tanstack/react-query': 5.90.16(react@18.3.1) buffer: 6.0.3 detect-browser: 5.3.0 - family: 0.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + family: 0.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) framer-motion: 6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) qrcode: 1.5.4 react: 18.3.1 @@ -7862,8 +7874,8 @@ snapshots: react-use-measure: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) resize-observer-polyfill: 1.5.1 styled-components: 5.3.11(@babel/core@7.28.5)(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@babel/core' - react-is @@ -8247,12 +8259,12 @@ snapshots: dependencies: '@next/eslint-plugin-next': 15.5.4 '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) @@ -8282,22 +8294,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -8308,7 +8320,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8320,7 +8332,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -8492,12 +8504,12 @@ snapshots: eyes@0.1.8: {} - family@0.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + family@0.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) fast-deep-equal@3.1.3: {} @@ -8509,6 +8521,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -8573,7 +8593,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.5: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -8676,6 +8696,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -8683,7 +8705,7 @@ snapshots: defu: 6.1.4 destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.4 + node-mock-http: 1.0.3 radix3: 1.1.2 ufo: 1.6.1 uncrypto: 0.1.3 @@ -8718,7 +8740,7 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.10.8: {} + hono@4.10.6: {} html-encoding-sniffer@4.0.0: dependencies: @@ -8953,7 +8975,7 @@ snapshots: jiti@2.6.1: {} - jose@6.1.3: {} + jose@6.1.1: {} joycon@3.1.1: {} @@ -9245,7 +9267,7 @@ snapshots: node-gyp-build@4.8.4: {} - node-mock-http@1.0.4: {} + node-mock-http@1.0.3: {} node-releases@2.0.27: {} @@ -9335,11 +9357,11 @@ snapshots: ox@0.6.7(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 - abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -9353,14 +9375,14 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.2(typescript@5.9.3)(zod@3.25.76) + abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.9.17(typescript@5.9.3)(zod@3.25.76): + ox@0.9.14(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -9368,7 +9390,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.2(typescript@5.9.3)(zod@3.25.76) + abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -9383,7 +9405,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.3)(zod@3.25.76) + abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -9485,21 +9507,21 @@ snapshots: style-value-types: 5.0.0 tslib: 2.8.1 - porto@0.2.35(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - hono: 4.10.8 + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + hono: 4.10.6 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) - ox: 0.9.17(typescript@5.9.3)(zod@3.25.76) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + ox: 0.9.14(typescript@5.9.3)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 3.25.76 - zustand: 5.0.9(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + zustand: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: - '@tanstack/react-query': 5.90.12(react@18.3.1) + '@tanstack/react-query': 5.90.16(react@18.3.1) react: 18.3.1 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer @@ -9708,7 +9730,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.2 fsevents: 2.3.3 - rpc-websockets@9.3.2: + rpc-websockets@9.3.1: dependencies: '@swc/helpers': 0.5.17 '@types/uuid': 8.3.4 @@ -10215,6 +10237,9 @@ snapshots: uncrypto@0.1.3: {} + undici-types@6.19.8: + optional: true + undici-types@6.21.0: {} undici-types@7.16.0: {} @@ -10243,7 +10268,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.3(idb-keyval@6.2.2): + unstorage@1.17.2(idb-keyval@6.2.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -10318,7 +10343,7 @@ snapshots: - utf-8-validate - zod - viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): + viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -10335,13 +10360,13 @@ snapshots: - utf-8-validate - zod - vite-node@2.1.9(@types/node@20.19.25)(lightningcss@1.30.2): + vite-node@2.1.9(@types/node@22.7.5)(lightningcss@1.30.2): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@5.5.0) es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.21(@types/node@20.19.25)(lightningcss@1.30.2) + vite: 5.4.21(@types/node@22.7.5)(lightningcss@1.30.2) transitivePeerDependencies: - '@types/node' - less @@ -10353,20 +10378,20 @@ snapshots: - supports-color - terser - vite@5.4.21(@types/node@20.19.25)(lightningcss@1.30.2): + vite@5.4.21(@types/node@22.7.5)(lightningcss@1.30.2): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.53.2 optionalDependencies: - '@types/node': 20.19.25 + '@types/node': 22.7.5 fsevents: 2.3.3 lightningcss: 1.30.2 - vitest@2.1.9(@types/node@20.19.25)(jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2): + vitest@2.1.9(@types/node@22.7.5)(jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.25)(lightningcss@1.30.2)) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.7.5)(lightningcss@1.30.2)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -10382,11 +10407,11 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 1.2.0 - vite: 5.4.21(@types/node@20.19.25)(lightningcss@1.30.2) - vite-node: 2.1.9(@types/node@20.19.25)(lightningcss@1.30.2) + vite: 5.4.21(@types/node@22.7.5)(lightningcss@1.30.2) + vite-node: 2.1.9(@types/node@22.7.5)(lightningcss@1.30.2) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.19.25 + '@types/node': 22.7.5 jsdom: 27.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - less @@ -10403,14 +10428,14 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): + wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: - '@tanstack/react-query': 5.90.12(react@18.3.1) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@tanstack/react-query': 5.90.16(react@18.3.1) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.16)(@tanstack/react-query@5.90.16(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.16)(@types/react@18.3.27)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - viem: 2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.39.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10612,7 +10637,7 @@ snapshots: react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - zustand@5.0.9(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + zustand@5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): optionalDependencies: '@types/react': 18.3.27 react: 18.3.1 diff --git a/src/app/Provider.tsx b/src/app/Provider.tsx index 1c13a9f..5e566cf 100644 --- a/src/app/Provider.tsx +++ b/src/app/Provider.tsx @@ -3,9 +3,8 @@ import { WagmiProvider, createConfig, http } from "wagmi"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConnectKitProvider, getDefaultConfig } from "connectkit"; -import { SonarProvider } from "@echoxyz/sonar-react"; -import { sonarConfig } from "./config"; import { sepolia } from "wagmi/chains"; +import { SessionProvider } from "./hooks/use-session"; const config = createConfig( getDefaultConfig({ @@ -19,8 +18,7 @@ const config = createConfig( // Required App Info appName: "Sonar Next.js example app", - appDescription: - "Next.js app showing how to integrate with the Sonar API via the sonar-react and sonar-core libraries.", + appDescription: "Next.js app showing how to integrate with the Sonar API via backend OAuth.", }) ); @@ -28,12 +26,12 @@ const queryClient = new QueryClient(); export const Provider = ({ children }: { children: React.ReactNode }) => { return ( - + {children} - + ); }; diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..dd46a7e --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { createSession } from "@/lib/session"; + +/** + * Create a new session (login). + * Given this is an example app - no authentication is required. + */ +export async function POST() { + const session = await createSession(); + + return NextResponse.json({ + success: true, + userId: session.userId, + }); +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..7c5d8b6 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { destroySession } from "@/lib/session"; + +/** + * Destroy the current session (logout). + * Clears session cookie and any associated Sonar tokens. + */ +export async function POST() { + await destroySession(); + + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/auth/session/route.ts b/src/app/api/auth/session/route.ts new file mode 100644 index 0000000..018335c --- /dev/null +++ b/src/app/api/auth/session/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import { getSession } from "@/lib/session"; +import { getTokenStore } from "@/lib/token-store"; + +/** + * Get current session status. + * Returns session info and Sonar connection status. + */ +export async function GET() { + const session = await getSession(); + + if (!session) { + return NextResponse.json({ + authenticated: false, + sonarConnected: false, + }); + } + + const tokens = getTokenStore().getTokens(session.userId); + + return NextResponse.json({ + authenticated: true, + sonarConnected: !!tokens, + }); +} diff --git a/src/app/api/auth/sonar/authorize/route.ts b/src/app/api/auth/sonar/authorize/route.ts new file mode 100644 index 0000000..6ad1ed8 --- /dev/null +++ b/src/app/api/auth/sonar/authorize/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from "next/server"; +import { getSession } from "@/lib/session"; +import { sonarConfig } from "@/lib/config"; +import { generatePKCEParams, buildAuthorizationUrl } from "@echoxyz/sonar-core"; +import { setPKCEVerifier } from "@/lib/pkce-store"; + +/** + * Generate Sonar OAuth authorization URL with PKCE and redirect user + */ +export async function GET() { + const session = await getSession(); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Generate PKCE parameters (includes state token from sonar-core) + const { codeVerifier, codeChallenge, state } = await generatePKCEParams(); + + // Store code verifier and user ID linked to state token (will be retrieved in callback) + await setPKCEVerifier(state, session.userId, codeVerifier); + + // Build authorization URL with PKCE + const authorizationUrl = buildAuthorizationUrl({ + clientUUID: sonarConfig.clientUUID, + redirectURI: sonarConfig.redirectURI, + state, + codeChallenge, + frontendURL: sonarConfig.frontendURL, + }); + + return NextResponse.json({ url: authorizationUrl.toString() }); +} diff --git a/src/app/api/auth/sonar/callback/route.ts b/src/app/api/auth/sonar/callback/route.ts new file mode 100644 index 0000000..8dd6553 --- /dev/null +++ b/src/app/api/auth/sonar/callback/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSession } from "@/lib/session"; +import { sonarConfig } from "@/lib/config"; +import { getTokenStore, SonarTokens } from "@/lib/token-store"; +import { getPKCEVerifier, clearPKCEVerifier } from "@/lib/pkce-store"; +import { createSonarClient } from "@/lib/sonar"; + +/** + * Handle OAuth callback from Sonar + * Exchange authorization code for access/refresh tokens using PKCE + */ +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const code = searchParams.get("code"); + const state = searchParams.get("state"); + const error = searchParams.get("error"); + + // Handle OAuth provider errors + if (error) { + return NextResponse.json({ error: "OAuth authorization failed", details: error }, { status: 400 }); + } + + // Validate required parameters + if (!code || !state) { + return NextResponse.json( + { error: "Missing required parameters", details: "code and state are required" }, + { status: 400 } + ); + } + + // Verify session exists + const session = await getSession(); + if (!session) { + return NextResponse.json({ error: "Unauthorized", details: "No active session" }, { status: 401 }); + } + + try { + // Retrieve code verifier and session ID from cookie store using state token + const stateData = await getPKCEVerifier(state); + if (!stateData) { + return NextResponse.json( + { error: "Invalid state", details: "OAuth state token not found or expired" }, + { status: 400 } + ); + } + + // Verify the state token belongs to the current session + if (stateData.userId !== session.userId) { + return NextResponse.json( + { error: "Invalid session", details: "State token does not match current session" }, + { status: 401 } + ); + } + + const { codeVerifier } = stateData; + + // Create a temporary client to exchange the authorization code + const client = createSonarClient(session.userId); + const tokenData = await client.exchangeAuthorizationCode({ + code, + codeVerifier, + redirectURI: sonarConfig.redirectURI, + }); + + const expiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in; + + const sonarTokens: SonarTokens = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresAt, + }; + + // Store tokens in token store + getTokenStore().setTokens(session.userId, sonarTokens); + + // Clear the code verifier (no longer needed) + await clearPKCEVerifier(state); + + // Return success - frontend will navigate to home + return NextResponse.json({ success: true }); + } catch (error) { + // Return proper error response with status code + if (error instanceof Error) { + return NextResponse.json({ error: "OAuth callback failed", details: error.message }, { status: 500 }); + } + return NextResponse.json({ error: "OAuth callback failed" }, { status: 500 }); + } +} diff --git a/src/app/api/auth/sonar/disconnect/route.ts b/src/app/api/auth/sonar/disconnect/route.ts new file mode 100644 index 0000000..bbbb795 --- /dev/null +++ b/src/app/api/auth/sonar/disconnect/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { getSession } from "@/lib/session"; +import { getTokenStore } from "@/lib/token-store"; + +/** + * Disconnect Sonar account (remove stored tokens) + */ +export async function POST() { + const session = await getSession(); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + getTokenStore().clearTokens(session.userId); + + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/sonar/entities/route.ts b/src/app/api/sonar/entities/route.ts new file mode 100644 index 0000000..4dcca54 --- /dev/null +++ b/src/app/api/sonar/entities/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { createSonarRouteHandler } from "@/lib/sonar"; + +type EntitiesRequest = { + saleUUID: string; +}; + +/** + * Proxy request to Sonar ReadEntities endpoint + * Returns all entities for the authenticated user + */ +export const POST = createSonarRouteHandler( + async (client, body) => { + const { saleUUID } = body; + + if (!saleUUID) { + return NextResponse.json({ error: "Missing saleUUID" }, { status: 400 }); + } + + const result = await client.listAvailableEntities({ saleUUID }); + return NextResponse.json(result); + } +); diff --git a/src/app/api/sonar/entity/route.ts b/src/app/api/sonar/entity/route.ts new file mode 100644 index 0000000..f74f955 --- /dev/null +++ b/src/app/api/sonar/entity/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { createSonarRouteHandler } from "@/lib/sonar"; +import { APIError } from "@echoxyz/sonar-core"; + +type EntityRequest = { + saleUUID: string; + walletAddress: string; +}; + +/** + * Proxy request to Sonar ReadEntity endpoint + */ +export const POST = createSonarRouteHandler( + async (client, body) => { + const { saleUUID, walletAddress } = body; + + if (!saleUUID || !walletAddress) { + return NextResponse.json( + { error: "Missing saleUUID or walletAddress" }, + { status: 400 } + ); + } + + try { + const result = await client.readEntity({ saleUUID, walletAddress }); + return NextResponse.json(result); + } catch (error) { + // Special handling: 404 returns null entity instead of error + if (error instanceof APIError && error.status === 404) { + return NextResponse.json({ Entity: null }, { status: 200 }); + } + throw error; + } + } +); diff --git a/src/app/api/sonar/generate-purchase-permit/route.ts b/src/app/api/sonar/generate-purchase-permit/route.ts new file mode 100644 index 0000000..0514413 --- /dev/null +++ b/src/app/api/sonar/generate-purchase-permit/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import { createSonarRouteHandler } from "@/lib/sonar"; + +type GeneratePurchasePermitRequest = { + saleUUID: string; + entityID: string; + walletAddress: string; +}; + +/** + * Proxy request to Sonar GenerateSalePurchasePermit endpoint + */ +export const POST = createSonarRouteHandler( + async (client, body) => { + const { saleUUID, entityID, walletAddress } = body; + + if (!saleUUID || !entityID || !walletAddress) { + return NextResponse.json( + { error: "Missing required parameters" }, + { status: 400 } + ); + } + + const result = await client.generatePurchasePermit({ + saleUUID, + entityID, + walletAddress, + }); + return NextResponse.json(result); + } +); diff --git a/src/app/api/sonar/pre-purchase-check/route.ts b/src/app/api/sonar/pre-purchase-check/route.ts new file mode 100644 index 0000000..7a6cb98 --- /dev/null +++ b/src/app/api/sonar/pre-purchase-check/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import { createSonarRouteHandler } from "@/lib/sonar"; + +type PrePurchaseCheckRequest = { + saleUUID: string; + entityID: string; + walletAddress: string; +}; + +/** + * Proxy request to Sonar PrePurchaseCheck endpoint + */ +export const POST = createSonarRouteHandler( + async (client, body) => { + const { saleUUID, entityID, walletAddress } = body; + + if (!saleUUID || !entityID || !walletAddress) { + return NextResponse.json( + { error: "Missing required parameters" }, + { status: 400 } + ); + } + + const result = await client.prePurchaseCheck({ + saleUUID, + entityID, + walletAddress, + }); + return NextResponse.json(result); + } +); diff --git a/src/app/components/auth/AuthenticationSection.tsx b/src/app/components/auth/AuthenticationSection.tsx index fa4400f..6ce44c7 100644 --- a/src/app/components/auth/AuthenticationSection.tsx +++ b/src/app/components/auth/AuthenticationSection.tsx @@ -1,47 +1,89 @@ "use client"; -import { usePathname } from "next/navigation"; +import { useSession } from "@/app/hooks/use-session"; -interface AuthenticationSectionProps { - ready: boolean; - authenticated: boolean; - login: () => void; - logout: () => void; -} +export function AuthenticationSection() { + const { authenticated, sonarConnected, loading, login, logout } = useSession(); -export function AuthenticationSection({ ready, authenticated, login, logout }: AuthenticationSectionProps) { - const pathname = usePathname(); + const handleConnectSonar = async () => { + try { + const response = await fetch("/api/auth/sonar/authorize"); + const data = await response.json(); + if (data.url) { + window.location.href = data.url; + } + } catch (error) { + console.error("Failed to get Sonar authorization URL:", error); + } + }; - const handleLogin = () => { - // Store current path before redirecting to OAuth - if (typeof window !== "undefined") { - localStorage.setItem("sonar_oauth_return_path", pathname); + const handleDisconnectSonar = async () => { + try { + await fetch("/api/auth/sonar/disconnect", { method: "POST" }); + window.location.reload(); + } catch (error) { + console.error("Failed to disconnect Sonar:", error); } - login(); }; - return ( -
- {!ready ? ( -

Loading authentication...

- ) : !authenticated ? ( -
-

Connect your Sonar account to check your eligibility status.

+ if (loading) { + return ( +
+

Loading...

+
+ ); + } + + if (!authenticated) { + return ( +
+
+

Login to continue.

+ +
+
+ ); + } + + if (!sonarConnected) { + return ( +
+
+
+

Connect your Sonar account to check your eligibility status.

+ +
- ) : ( -
-

✓ Sonar Connected

-
+ ); + } + + return ( +
+
+

✓ Sonar Connected

+
+ +
- )} +
); } diff --git a/src/app/components/registration/EntitiesList.tsx b/src/app/components/registration/EntitiesList.tsx index 17b01f3..67c1a2f 100644 --- a/src/app/components/registration/EntitiesList.tsx +++ b/src/app/components/registration/EntitiesList.tsx @@ -1,6 +1,6 @@ import { EntityDetails } from "@echoxyz/sonar-core"; import { EntityCard } from "../entity/EntityCard"; -import { sonarConfig } from "@/app/config"; +import { sonarConfig } from "@/lib/config"; interface EntitiesListProps { loading: boolean; diff --git a/src/app/components/sale/PurchaseCard.tsx b/src/app/components/sale/PurchaseCard.tsx index 2399f2c..ce3001d 100644 --- a/src/app/components/sale/PurchaseCard.tsx +++ b/src/app/components/sale/PurchaseCard.tsx @@ -1,14 +1,11 @@ "use client"; import { PrePurchaseFailureReason, GeneratePurchasePermitResponse, EntityID } from "@echoxyz/sonar-core"; +import { UseSonarPurchaseResultNotReadyToPurchase, UseSonarPurchaseResultReadyToPurchase } from "@echoxyz/sonar-react"; import { useState } from "react"; -import { saleUUID } from "../../config"; -import { - useSonarPurchase, - UseSonarPurchaseResultNotReadyToPurchase, - UseSonarPurchaseResultReadyToPurchase, -} from "@echoxyz/sonar-react"; -import { useSaleContract } from "../../hooks"; +import { saleUUID } from "@/lib/config"; +import { useSonarPurchase } from "../../hooks/use-sonar-purchase"; +import { useSaleContract } from "../../hooks/use-sale-contract"; function readinessConfig( sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase @@ -46,7 +43,7 @@ function readinessConfig( ); case PrePurchaseFailureReason.NO_RESERVED_ALLOCATION: return warningConfig( - "No reserved allocation — The connected wallet doesn’t have a reserved spot for this sale. Connect a different wallet." + "No reserved allocation — The connected wallet doesn't have a reserved spot for this sale. Connect a different wallet." ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -84,8 +81,8 @@ function ReadyToPurchaseSection({ // TODO: could support selecting the amount amount: BigInt(1e8), }); - } catch (error) { - setError(error as Error); + } catch (err) { + setError(err as Error); } finally { setLoading(false); } @@ -135,11 +132,13 @@ function PurchaseCard({ entityID, walletAddress }: { entityID: EntityID; walletA return

Loading...

; } - if (sonarPurchaser.error) { + if ("error" in sonarPurchaser && sonarPurchaser.error) { return

Error: {sonarPurchaser.error.message}

; } - const readinessCfg = readinessConfig(sonarPurchaser); + // At this point we know it's either ready or not-ready (not loading, not error) + const purchaser = sonarPurchaser as UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase; + const readinessCfg = readinessConfig(purchaser); return (
@@ -147,24 +146,23 @@ function PurchaseCard({ entityID, walletAddress }: { entityID: EntityID; walletA

{readinessCfg.description}

- {sonarPurchaser.readyToPurchase && ( + {purchaser.readyToPurchase && ( )} - {!sonarPurchaser.readyToPurchase && - sonarPurchaser.failureReason === PrePurchaseFailureReason.REQUIRES_LIVENESS && ( - - )} + {!purchaser.readyToPurchase && purchaser.failureReason === PrePurchaseFailureReason.REQUIRES_LIVENESS && ( + + )}
); } diff --git a/src/app/hooks.ts b/src/app/hooks/use-sale-contract.ts similarity index 96% rename from src/app/hooks.ts rename to src/app/hooks/use-sale-contract.ts index d3900bc..a106146 100644 --- a/src/app/hooks.ts +++ b/src/app/hooks/use-sale-contract.ts @@ -1,8 +1,8 @@ import { BasicPermitV2, GeneratePurchasePermitResponse } from "@echoxyz/sonar-core"; import { useCallback, useEffect, useState } from "react"; import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from "wagmi"; -import { saleContract } from "./config"; -import { examplSaleABI } from "./ExampleSaleABI"; +import { saleContract } from "@/lib/config"; +import { examplSaleABI } from "../ExampleSaleABI"; import { useConfig } from "wagmi"; import { simulateContract } from "wagmi/actions"; @@ -85,3 +85,4 @@ export const useSaleContract = (walletAddress: `0x${string}`) => { awaitingTxReceiptError, }; }; + diff --git a/src/app/hooks/use-session.tsx b/src/app/hooks/use-session.tsx new file mode 100644 index 0000000..15190e4 --- /dev/null +++ b/src/app/hooks/use-session.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react"; + +interface SessionState { + authenticated: boolean; + sonarConnected: boolean; + loading: boolean; +} + +interface SessionContextValue extends SessionState { + login: () => Promise; + logout: () => Promise; + refreshSession: () => Promise; +} + +const SessionContext = createContext(null); + +export function SessionProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState({ + authenticated: false, + sonarConnected: false, + loading: true, + }); + + const refreshSession = useCallback(async () => { + try { + const response = await fetch("/api/auth/session"); + const data = await response.json(); + setState({ + authenticated: data.authenticated, + sonarConnected: data.sonarConnected, + loading: false, + }); + } catch { + setState({ + authenticated: false, + sonarConnected: false, + loading: false, + }); + } + }, []); + + useEffect(() => { + refreshSession(); + }, [refreshSession]); + + const login = useCallback(async () => { + try { + await fetch("/api/auth/login", { method: "POST" }); + await refreshSession(); + } catch (error) { + console.error("Login failed:", error); + } + }, [refreshSession]); + + const logout = useCallback(async () => { + try { + await fetch("/api/auth/logout", { method: "POST" }); + await refreshSession(); + } catch (error) { + console.error("Logout failed:", error); + } + }, [refreshSession]); + + return ( + {children} + ); +} + +export function useSession() { + const context = useContext(SessionContext); + if (!context) { + throw new Error("useSession must be used within a SessionProvider"); + } + return context; +} diff --git a/src/app/hooks/use-sonar-entities.ts b/src/app/hooks/use-sonar-entities.ts new file mode 100644 index 0000000..6a6b14e --- /dev/null +++ b/src/app/hooks/use-sonar-entities.ts @@ -0,0 +1,18 @@ +"use client"; + +import { ListAvailableEntitiesResponse } from "@echoxyz/sonar-core"; +import { saleUUID } from "@/lib/config"; +import { useSonarQuery } from "./use-sonar-query"; + +/** + * Hook to fetch all Sonar entities for the authenticated user + */ +export function useSonarEntities() { + const { loading, data, error } = useSonarQuery("/api/sonar/entities", { saleUUID }); + + return { + loading, + entities: data?.Entities, + error, + }; +} diff --git a/src/app/hooks/use-sonar-entity.ts b/src/app/hooks/use-sonar-entity.ts new file mode 100644 index 0000000..a1b6699 --- /dev/null +++ b/src/app/hooks/use-sonar-entity.ts @@ -0,0 +1,29 @@ +"use client"; + +import { ReadEntityResponse } from "@echoxyz/sonar-core"; +import { saleUUID } from "@/lib/config"; +import { useSonarQuery } from "./use-sonar-query"; + +/** + * Hook to fetch Sonar entity details for a specific wallet + */ +export function useSonarEntity(walletAddress?: string) { + const query = useSonarQuery("/api/sonar/entity", { + saleUUID, + walletAddress: walletAddress ?? "", + }); + + // No wallet address - return idle state + if (!walletAddress) { + return { loading: false, entity: undefined, error: undefined }; + } + + // Treat 404 as "no entity" rather than an error + const is404 = query.error?.message.includes("404"); + + return { + loading: query.loading, + entity: query.data?.Entity, + error: is404 ? undefined : query.error, + }; +} diff --git a/src/app/hooks/use-sonar-purchase.ts b/src/app/hooks/use-sonar-purchase.ts new file mode 100644 index 0000000..e6e816a --- /dev/null +++ b/src/app/hooks/use-sonar-purchase.ts @@ -0,0 +1,72 @@ +"use client"; + +import { + EntityID, + GeneratePurchasePermitResponse, + PrePurchaseCheckResponse, + PrePurchaseFailureReason, +} from "@echoxyz/sonar-core"; +import { UseSonarPurchaseResult } from "@echoxyz/sonar-react"; +import { useSession } from "./use-session"; +import { useCallback } from "react"; +import { useSonarQuery } from "./use-sonar-query"; + +/** + * Hook for Sonar purchase flow + */ +export function useSonarPurchase(args: { + saleUUID: string; + entityID: EntityID; + walletAddress: string; +}): UseSonarPurchaseResult { + const { authenticated } = useSession(); + + const { loading, data, error } = useSonarQuery("/api/sonar/pre-purchase-check", { + saleUUID: args.saleUUID, + entityID: args.entityID, + walletAddress: args.walletAddress, + }); + + const generatePurchasePermit = useCallback(async (): Promise => { + if (!authenticated) { + throw new Error("Not authenticated"); + } + + const response = await fetch("/api/sonar/generate-purchase-permit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + saleUUID: args.saleUUID, + entityID: args.entityID, + walletAddress: args.walletAddress, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `Request failed: ${response.status}`); + } + + return response.json(); + }, [authenticated, args.saleUUID, args.entityID, args.walletAddress]); + + if (loading) { + return { loading: true, readyToPurchase: false, error: undefined }; + } + + if (error || !data) { + return { loading: false, readyToPurchase: false, error: error ?? new Error("No data") }; + } + + if (data.ReadyToPurchase) { + return { loading: false, readyToPurchase: true, error: undefined, generatePurchasePermit }; + } + + return { + loading: false, + readyToPurchase: false, + error: undefined, + failureReason: data.FailureReason as PrePurchaseFailureReason, + livenessCheckURL: data.LivenessCheckURL, + }; +} diff --git a/src/app/hooks/use-sonar-query.ts b/src/app/hooks/use-sonar-query.ts new file mode 100644 index 0000000..e30a72c --- /dev/null +++ b/src/app/hooks/use-sonar-query.ts @@ -0,0 +1,66 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useSession } from "./use-session"; + +export type SonarQueryState = + | { loading: true; data: undefined; error: undefined } + | { loading: false; data: T; error: undefined } + | { loading: false; data: undefined; error: Error } + | { loading: false; data: undefined; error: undefined }; // idle/skipped state + +/** + * Generic hook for Sonar API queries with shared session/auth handling + */ +export function useSonarQuery(endpoint: string, body: Record): SonarQueryState { + const { authenticated, sonarConnected, refreshSession } = useSession(); + const [state, setState] = useState>({ + loading: true, + data: undefined, + error: undefined, + }); + + // Serialize body for stable dependency comparison + const serializedBody = JSON.stringify(body); + + useEffect(() => { + if (!authenticated || !sonarConnected) { + setState({ loading: false, data: undefined, error: undefined }); + return; + } + + const fetchData = async () => { + setState({ loading: true, data: undefined, error: undefined }); + + try { + const response = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: serializedBody, + }); + + if (!response.ok) { + if (response.status === 401) { + await refreshSession(); + return; + } + const errorData = await response.json(); + throw new Error(errorData.error || `Request failed: ${response.status}`); + } + + const data = await response.json(); + setState({ loading: false, data, error: undefined }); + } catch (err) { + setState({ + loading: false, + data: undefined, + error: err instanceof Error ? err : new Error(String(err)), + }); + } + }; + + fetchData(); + }, [authenticated, sonarConnected, endpoint, serializedBody, refreshSession]); + + return state; +} diff --git a/src/app/oauth/callback/page.tsx b/src/app/oauth/callback/page.tsx index 2e0b82a..2e2e2cf 100644 --- a/src/app/oauth/callback/page.tsx +++ b/src/app/oauth/callback/page.tsx @@ -1,102 +1,86 @@ "use client"; -import { useEffect, useRef, useState } from "react"; -import { useSonarAuth } from "@echoxyz/sonar-react"; +import { Suspense, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; -export default function OAuthCallback() { - const { authenticated, completeOAuth, ready } = useSonarAuth(); - const oauthCompletionTriggered = useRef(false); - const [oauthError, setOAuthError] = useState(null); - const [timedOut, setTimedOut] = useState(false); +/** + * OAuth callback content - uses useSearchParams which requires Suspense + */ +function OAuthCallbackContent() { + const searchParams = useSearchParams(); + const [error, setError] = useState(null); - // complete the oauth flow and exchange the code for an access token useEffect(() => { - const processOAuthCallback = async () => { - const params = new URLSearchParams(window.location.search); - const code = params.get("code"); - const state = params.get("state"); + const handleCallback = async () => { + // Extract query parameters + const code = searchParams.get("code"); + const state = searchParams.get("state"); + const oauthError = searchParams.get("error"); - // the user is already authenticated, nothing to do - if (!ready || authenticated || !code || !state) { + if (oauthError) { + setError(`OAuth error: ${oauthError}`); return; } - // ensuring the oauth completion isn't called multiple times since subsequent ones are expected to fail - if (oauthCompletionTriggered.current) { + if (!code || !state) { + setError("Missing authorization code or state"); return; } - oauthCompletionTriggered.current = true; try { - await completeOAuth({ code, state }); - } catch (err) { - setOAuthError(err instanceof Error ? err.message : null); - } - }; - - processOAuthCallback(); - }, [authenticated, completeOAuth, ready]); - - // fetch the user's available entities after they've been authenticated - useEffect(() => { - if (!ready || !authenticated) { - return; - } + // Call backend callback handler + const response = await fetch(`/api/auth/sonar/callback?code=${code}&state=${state}`); - // Redirect to the stored return path, or default to home - const returnPath = localStorage.getItem("sonar_oauth_return_path"); - if (returnPath) { - localStorage.removeItem("sonar_oauth_return_path"); - window.location.href = returnPath; - } else { - window.location.href = "/"; - } - }, [authenticated, ready]); + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: "Unknown error" })); + setError(errorData.error || "Failed to complete OAuth flow"); + return; + } - // set a timeout, so we don't keep the user waiting indefinitely in case of an unexpected issue - useEffect(() => { - setTimedOut(false); - const timeoutId = setTimeout(() => setTimedOut(true), 20000); - return () => clearTimeout(timeoutId); - }, [setTimedOut]); - - if (timedOut) { - return ( -
-
-

Timed out

- -
-
- ); - } + // Success - do a hard navigation to home + // This ensures a full page load that picks up the updated session + window.location.href = "/"; + } catch { + setError("Failed to process OAuth callback"); + } + }; - if (oauthError) { - return ( -
-
-

{oauthError}

- -
-
- ); - } + handleCallback(); + }, [searchParams]); return (
-

Connecting to Echo

+ {error ? ( + <> +

Error

+

{error}

+

Redirecting to home...

+ + ) : ( +

Connecting to Sonar...

+ )}
); } + +/** + * OAuth callback page - redirects to backend callback handler + * Wrapped in Suspense because useSearchParams requires it for static generation + */ +export default function OAuthCallback() { + return ( + +
+

Loading...

+
+
+ } + > + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 57446d8..d946680 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,20 +1,24 @@ "use client"; import { useState, useEffect } from "react"; -import { ConnectKitButton } from "connectkit"; -import { useSonarAuth, useSonarEntity, useSonarEntities } from "@echoxyz/sonar-react"; -import { saleUUID, sonarHomeURL, sonarConfig } from "./config"; +import { saleUUID, sonarHomeURL } from "@/lib/config"; import { useAccount } from "wagmi"; -import PurchaseCard from "./components/sale/PurchaseCard"; import { SaleEligibility } from "@echoxyz/sonar-core"; -import { AuthenticationSection } from "./components/auth/AuthenticationSection"; import { EntityCard } from "./components/entity/EntityCard"; import { NotEligibleMessage } from "./components/sale/NotEligibleMessage"; +import { AuthenticationSection } from "./components/auth/AuthenticationSection"; +import { useSonarEntity } from "./hooks/use-sonar-entity"; +import { useSonarEntities } from "./hooks/use-sonar-entities"; +import { useSession } from "./hooks/use-session"; +import PurchaseCard from "./components/sale/PurchaseCard"; import { EntitiesList } from "./components/registration/EntitiesList"; import { EligibilityResults } from "./components/registration/EligibilityResults"; +import { ConnectKitButton } from "connectkit"; export default function Home() { const [saleIsLive, setSaleIsLive] = useState(false); + const { sonarConnected } = useSession(); + const { address } = useAccount(); // Load sale state from localStorage useEffect(() => { @@ -31,43 +35,26 @@ export default function Home() { localStorage.setItem("sale_is_live", String(newState)); }; - // Auth and data hooks - const { address } = useAccount(); - const { login, authenticated, logout, ready } = useSonarAuth(); - - // Registration data - const { - loading: entitiesLoading, - entities, - error: entitiesError, - } = useSonarEntities({ - saleUUID: saleUUID, - }); + // Registration data (all entities for the user) + const { loading: entitiesLoading, entities, error: entitiesError } = useSonarEntities(); const eligibleEntities = entities?.filter((entity) => entity.SaleEligibility === SaleEligibility.ELIGIBLE) || []; - // Sale data - const { - loading: entityLoading, - entity, - error: entityError, - } = useSonarEntity({ - saleUUID: saleUUID, - walletAddress: address, - }); + // Sale data (entity for current wallet) + const { loading: entityLoading, entity, error: entityError } = useSonarEntity(address); const isEligible = entity && entity.SaleEligibility === SaleEligibility.ELIGIBLE; const EntitySection = () => { - if (!address || !authenticated) { + if (!address) { return (

Connection Required

-

Connect your wallet and Sonar account to continue with your purchase.

+

Connect your wallet to continue with your purchase.

); } - + if (entityLoading) { return (
@@ -75,7 +62,7 @@ export default function Home() {
); } - + if (entityError) { return (
@@ -84,7 +71,7 @@ export default function Home() {
); } - + if (!entity) { return (
@@ -96,7 +83,7 @@ export default function Home() {
- {/* Registration Phase */} - {!saleIsLive && ( -
- +
+ - {authenticated && ( -
-

Check Your Eligibility

- - +

Check Your Eligibility

+ + + + {!entitiesLoading && !entitiesError && entities && ( + + )} +
+ )} - {!entitiesLoading && !entitiesError && entities && ( - - )} -
- )} -
- )} - - {/* Sale Phase */} - {saleIsLive && ( -
- {/* Connection Buttons */} - - - {/* Entity Information */} + {/* Sale Phase */} + {sonarConnected && saleIsLive && (

Your Entity Information

-
- {/* Purchase Panel */} - {isEligible && address && ( -
-

Make a Purchase

- -
- )} + {isEligible && address && ( +
+

Make a Purchase

+ +
+ )} - {/* Not Eligible Message */} - {entity && !isEligible && } -
- )} + {entity && !isEligible && } +
+ )} + diff --git a/src/app/config.ts b/src/lib/config.ts similarity index 90% rename from src/app/config.ts rename to src/lib/config.ts index 2862377..7dd1ab2 100644 --- a/src/app/config.ts +++ b/src/lib/config.ts @@ -1,12 +1,12 @@ import { Hex } from "@echoxyz/sonar-core"; import { SonarProviderConfig } from "@echoxyz/sonar-react"; -export const sonarConfig = { +export const sonarConfig: SonarProviderConfig & { apiURL: string } = { clientUUID: process.env.NEXT_PUBLIC_OAUTH_CLIENT_UUID ?? "", redirectURI: process.env.NEXT_PUBLIC_OAUTH_CLIENT_REDIRECT_URI ?? "", frontendURL: process.env.NEXT_PUBLIC_ECHO_FRONTEND_URL ?? "https://app.echo.xyz", apiURL: process.env.NEXT_PUBLIC_ECHO_API_URL ?? "https://api.echo.xyz", -} as SonarProviderConfig; +}; export const saleUUID = process.env.NEXT_PUBLIC_SALE_UUID ?? ""; export const saleContract = diff --git a/src/lib/pkce-store.ts b/src/lib/pkce-store.ts new file mode 100644 index 0000000..06e7312 --- /dev/null +++ b/src/lib/pkce-store.ts @@ -0,0 +1,111 @@ +/** + * PKCE store interface for managing OAuth PKCE code verifiers. + * This interface allows swapping between in-memory and persistent storage implementations. + */ +export interface PKCEEntry { + sessionId: string; + codeVerifier: string; + expiresAt: number; // Unix timestamp in milliseconds +} + +export interface PKCEStore { + /** + * Store PKCE verifier and session ID for a state token + */ + setEntry(state: string, entry: PKCEEntry): void; + + /** + * Retrieve PKCE entry for a state token + */ + getEntry(state: string): PKCEEntry | null; + + /** + * Remove PKCE entry for a state token + */ + clearEntry(state: string): void; +} + +/** + * In-memory PKCE store implementation. + * Entries are stored in a Map and will be lost on server restart. + * This can be easily swapped for a database-backed implementation. + */ +class InMemoryPKCEStore implements PKCEStore { + private entries: Map = new Map(); + + setEntry(state: string, entry: PKCEEntry): void { + this.entries.set(state, entry); + } + + getEntry(state: string): PKCEEntry | null { + return this.entries.get(state) || null; + } + + clearEntry(state: string): void { + this.entries.delete(state); + } +} + +// Singleton instance - can be swapped for a different implementation +let pkceStoreInstance: PKCEStore | null = null; + +/** + * Get the PKCE store instance. + * This factory function allows swapping implementations without changing call sites. + */ +export function getPKCEStore(): PKCEStore { + if (!pkceStoreInstance) { + pkceStoreInstance = new InMemoryPKCEStore(); + } + return pkceStoreInstance; +} + +/** + * Set a custom PKCE store implementation. + * Useful for swapping to a database-backed store. + */ +export function setPKCEStore(store: PKCEStore): void { + pkceStoreInstance = store; +} + +const PKCE_TTL_MS = 10 * 60 * 1000; // 10 minutes + +/** + * Store PKCE verifier and session ID linked to a state token + */ +export function setPKCEVerifier(state: string, sessionId: string, codeVerifier: string): void { + getPKCEStore().setEntry(state, { + sessionId, + codeVerifier, + expiresAt: Date.now() + PKCE_TTL_MS, + }); +} + +/** + * Get PKCE verifier and session ID for a state token + */ +export function getPKCEVerifier(state: string): { userId: string; codeVerifier: string } | null { + const entry = getPKCEStore().getEntry(state); + + if (!entry) { + return null; + } + + // Check if expired + if (entry.expiresAt < Date.now()) { + getPKCEStore().clearEntry(state); + return null; + } + + return { + userId: entry.sessionId, + codeVerifier: entry.codeVerifier, + }; +} + +/** + * Clear state entry (called after successful token exchange) + */ +export function clearPKCEVerifier(state: string): void { + getPKCEStore().clearEntry(state); +} diff --git a/src/lib/session.ts b/src/lib/session.ts new file mode 100644 index 0000000..e7b4d8e --- /dev/null +++ b/src/lib/session.ts @@ -0,0 +1,54 @@ +/** + * Simple session management using cookies. + * Sessions are just random UUIDs - no authentication required. + */ +import { cookies } from "next/headers"; +import { getTokenStore } from "./token-store"; + +const SESSION_COOKIE_NAME = "sonar_example_session_id"; +const SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days + +export interface Session { + id: string; + /** User identifier - in this example app, the same as the session ID */ + userId: string; +} + +export async function getSession(): Promise { + const cookieStore = await cookies(); + const sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value; + + if (!sessionId) { + return null; + } + + return { id: sessionId, userId: sessionId }; +} + +export async function createSession(): Promise { + const cookieStore = await cookies(); + const sessionId = crypto.randomUUID(); + + cookieStore.set(SESSION_COOKIE_NAME, sessionId, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: SESSION_COOKIE_MAX_AGE, + path: "/", + }); + + return { id: sessionId, userId: sessionId }; +} + +export async function destroySession(): Promise { + const cookieStore = await cookies(); + const sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value; + + // Clear Sonar tokens if session exists + if (sessionId) { + getTokenStore().clearTokens(sessionId); + } + + // Delete the session cookie + cookieStore.delete(SESSION_COOKIE_NAME); +} diff --git a/src/lib/sonar.ts b/src/lib/sonar.ts new file mode 100644 index 0000000..2e50bee --- /dev/null +++ b/src/lib/sonar.ts @@ -0,0 +1,115 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSession } from "@/lib/session"; +import { getTokenStore, SonarTokens } from "@/lib/token-store"; +import { sonarConfig } from "@/lib/config"; +import { APIError, SonarClient } from "@echoxyz/sonar-core"; + +/** + * Create a SonarClient instance for a specific user + * Sets the access token from our server-side token store + */ +export function createSonarClient(userId: string): SonarClient { + // Create a new client instance + const client = new SonarClient({ + apiURL: sonarConfig.apiURL, + opts: { + onUnauthorized: () => { + // Clear tokens on unauthorized + getTokenStore().clearTokens(userId); + }, + }, + }); + + // Set the token from our server-side store + const tokens = getTokenStore().getTokens(userId); + if (tokens?.accessToken) { + client.setToken(tokens.accessToken); + } + + return client; +} + + +// In-flight refresh promises by session ID - prevents concurrent refresh attempts +const refreshPromises = new Map>(); + +/** + * Refresh Sonar access token using refresh token. + * Uses promise coalescing to prevent concurrent refresh attempts for the same session. + */ +async function refreshSonarToken(sessionId: string, refreshToken: string): Promise { + // If a refresh is already in-flight for this session, reuse it + const existing = refreshPromises.get(sessionId); + if (existing) { + return existing; + } + + const doRefresh = async (): Promise => { + const client = new SonarClient({ apiURL: sonarConfig.apiURL }); + + const tokenData = await client.refreshToken({ refreshToken }); + const expiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in; + + return { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || refreshToken, + expiresAt, + }; + }; + + const promise = doRefresh(); + refreshPromises.set(sessionId, promise); + + try { + const result = await promise; + return result; + } finally { + refreshPromises.delete(sessionId); + } +} + +type RouteHandler = (client: SonarClient, body: T) => Promise; + +/** + * Creates a Sonar API route handler with authentication, token refresh, and error handling. + */ +export function createSonarRouteHandler(handler: RouteHandler): (request: NextRequest) => Promise { + return async (request: NextRequest) => { + // Check session authentication + const session = await getSession(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Get tokens from store + let tokens = getTokenStore().getTokens(session.userId); + if (!tokens) { + return NextResponse.json({ error: "Sonar account not connected" }, { status: 401 }); + } + + // Check if token needs refresh (within 5 minutes of expiry) + const now = Math.floor(Date.now() / 1000); + if (tokens.expiresAt - now < 300) { + try { + tokens = await refreshSonarToken(session.userId, tokens.refreshToken); + getTokenStore().setTokens(session.userId, tokens); + } catch { + return NextResponse.json({ error: "Failed to refresh token" }, { status: 401 }); + } + } + + try { + const body = (await request.json()) as T; + const client = createSonarClient(session.userId); + + return await handler(client, body); + } catch (error) { + if (error instanceof APIError) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + console.error("Error calling Sonar API"); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } + }; +} diff --git a/src/lib/token-store.ts b/src/lib/token-store.ts new file mode 100644 index 0000000..9ec8ae8 --- /dev/null +++ b/src/lib/token-store.ts @@ -0,0 +1,69 @@ +/** + * Token store interface for managing Sonar OAuth tokens. + * This interface allows swapping between in-memory and persistent storage implementations. + */ +export interface SonarTokens { + accessToken: string; + refreshToken: string; + expiresAt: number; // Unix timestamp in seconds +} + +export interface TokenStore { + /** + * Store tokens for a user identifier + */ + setTokens(userId: string, tokens: SonarTokens): void; + + /** + * Retrieve tokens for a user identifier + */ + getTokens(userId: string): SonarTokens | null; + + /** + * Remove tokens for a user identifier + */ + clearTokens(userId: string): void; +} + +/** + * In-memory token store implementation. + * Tokens are stored in a Map and will be lost on server restart. + * This can be easily swapped for a database-backed implementation. + */ +class InMemoryTokenStore implements TokenStore { + private tokens: Map = new Map(); + + setTokens(userId: string, tokens: SonarTokens): void { + this.tokens.set(userId, tokens); + } + + getTokens(userId: string): SonarTokens | null { + return this.tokens.get(userId) || null; + } + + clearTokens(userId: string): void { + this.tokens.delete(userId); + } +} + +// Singleton instance - can be swapped for a different implementation +let tokenStoreInstance: TokenStore | null = null; + +/** + * Get the token store instance. + * This factory function allows swapping implementations without changing call sites. + */ +export function getTokenStore(): TokenStore { + if (!tokenStoreInstance) { + tokenStoreInstance = new InMemoryTokenStore(); + } + return tokenStoreInstance; +} + +/** + * Set a custom token store implementation. + * Useful for swapping to a database-backed store. + */ +export function setTokenStore(store: TokenStore): void { + tokenStoreInstance = store; +} diff --git a/tsconfig.json b/tsconfig.json index 06ee0db..c133409 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] }