From 3d519049db6fc548be057ba87549864da1250774 Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Tue, 28 Jul 2026 19:18:37 -0700 Subject: [PATCH 01/11] Add Circle onramp integration and related updates - Introduced Circle onramp functionality, allowing users to buy crypto directly from the wallet interface. - Added new CircleProvider and related context for managing onramp sessions. - Implemented onramp RPC method for seamless integration with the wallet. - Updated AssetDetails and WalletActions components to include onramp options. - Enhanced documentation to reflect new onramp capabilities and usage instructions. - Added necessary environment configurations for Circle's AppKit integration. --- .../skills/oneshot-embedded-wallet/SKILL.md | 27 +- .github/workflows/Deploy Dev.yaml | 3 + .github/workflows/Deploy Prod.yaml | 3 + .gitignore | 1 + .npmrc | 2 + AGENTS.md | 4 +- Dockerfile | 8 +- README.md | 25 + host/src/components/WalletActions.tsx | 20 + host/src/hooks/useHostTestActions.ts | 23 + package-lock.json | 1853 ++++++++++++++++- package.json | 3 +- skills/oneshot-embedded-wallet/SKILL.md | 25 +- src/circle/CircleContext.tsx | 30 + src/circle/circleChains.ts | 39 + src/circle/circlePopup.ts | 17 + src/circle/onrampTypes.ts | 9 + src/circle/openOnramp.ts | 13 + src/components/AssetDetails.tsx | 28 +- src/components/ModalHost.tsx | 11 + src/components/OnrampView.tsx | 277 +++ .../modals/PurchaseComingSoonModal.tsx | 29 - .../implementations/utils/CircleProvider.ts | 42 + .../implementations/utils/ConfigProvider.ts | 3 + src/lib/implementations/utils/index.ts | 1 + src/lib/interfaces/utils/ICircleProvider.ts | 13 + src/lib/interfaces/utils/index.ts | 2 + src/lib/types/domain/WalletConfig.ts | 4 + src/wallet/WalletProvider.tsx | 8 +- src/wallet/modalTypes.ts | 8 + src/wallet/registerOnramp.ts | 66 + src/wallet/useWalletBoot.ts | 11 + 32 files changed, 2520 insertions(+), 88 deletions(-) create mode 100644 .npmrc create mode 100644 src/circle/CircleContext.tsx create mode 100644 src/circle/circleChains.ts create mode 100644 src/circle/circlePopup.ts create mode 100644 src/circle/onrampTypes.ts create mode 100644 src/circle/openOnramp.ts create mode 100644 src/components/OnrampView.tsx delete mode 100644 src/components/modals/PurchaseComingSoonModal.tsx create mode 100644 src/lib/implementations/utils/CircleProvider.ts create mode 100644 src/lib/interfaces/utils/ICircleProvider.ts create mode 100644 src/wallet/registerOnramp.ts diff --git a/.agents/skills/oneshot-embedded-wallet/SKILL.md b/.agents/skills/oneshot-embedded-wallet/SKILL.md index 4b0ec4f..9c8d8ea 100644 --- a/.agents/skills/oneshot-embedded-wallet/SKILL.md +++ b/.agents/skills/oneshot-embedded-wallet/SKILL.md @@ -3,7 +3,7 @@ name: oneshot-embedded-wallet description: >- Integrate the 1Shot embedded wallet (OWS Host Layer) with @1shotapi/ows-provider. Use when embedding wallet.1shotapi.com, wiring OWSProxy, EIP-1193, credentials, - or custom RPC such as setStyle / focusWallet / addAsset / createAccount for + or custom RPC such as setStyle / focusWallet / addAsset / createAccount / onramp for theming, host-driven focus mode, tracked assets, and first-party Safari create. license: MIT metadata: @@ -232,7 +232,7 @@ Users can also add assets from the Balances tab without a host RPC. The Balances ## Custom RPC — `createAccount` -Used by the first-party **`/create/`** host page (Safari passkey create). Hosts embedding the wallet normally do **not** call this — the branding layer opens `/create/` itself when needed. +Used by the first-party **`/create/`** host page (Safari passkey create). Hosts embedding the wallet normally do **not** call this — the branding layer opens `/create/` itself when needed. Not exposed as a playground button. ```typescript const result = await proxy.rpc("createAccount"); @@ -246,14 +246,32 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" }); |--------|--------|--------| | `createAccount` | `{ accountName?: string }` optional | Runs setup create (passkey + relayer register); returns credential id | +## Custom RPC — `onramp` + +Opens Circle fiat onramp fullscreen inside the Branding Layer for the unlocked EVM address. + +```typescript +await proxy.rpc("onramp", { + chainId: 8453, // optional — decimal chain id for catalog scoping + amount: "50", // optional — amount hint when supported by the kit +}); +// or: await proxy.rpc("onramp", {}); +``` + +| Method | Params | Behavior | +|--------|--------|----------| +| `onramp` | `{ chainId?: number, amount?: string }` | Shows wallet, mounts Circle AppKit onramp; session minted via Relayer `POST /wallet/onramp` | + +Returns `{ ok: true }` when the user closes the onramp view. The Relayer holds the Circle kit key; the browser only receives a single-use session. Inline iframe requires Circle CSP allowlisting of the wallet origin; for local/ngrok testing the branding layer honors `localStorage.setItem("circlePopup", "true")` and uses AppKit `openWindow` instead. + ## Other Host APIs | API | Use | |-----|-----| | `proxy.ethereum.request(...)` | EIP-1193 (accounts, sign, chain, …) | | `proxy.credentials.*` | OID4 offer / present (when enabled in wallet) | -| `proxy.showWallet()` / `hideWallet()` | Host-driven panel (flyout or full-screen drawer on small viewports) without an EIP-1193 call | -| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, …) | +| `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call | +| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, …) | ## Hard rules @@ -262,3 +280,4 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" }); - Theme with `setStyle`; do not ask integrators to fork CSS for basic brand colors / product name. - Use `focusWallet` / `unfocusWallet` for host-driven single-asset flows; do not expose mode switching in the wallet UI. - Use `addAsset` when the host wants a lasting Balances entry; expect a confirm modal (contrast with `focusWallet`). +- Use `onramp` (or the in-wallet Buy button) for fiat → crypto; do not put the Circle kit key in the Host or Branding Layer. diff --git a/.github/workflows/Deploy Dev.yaml b/.github/workflows/Deploy Dev.yaml index 51525b1..3dc2cbf 100644 --- a/.github/workflows/Deploy Dev.yaml +++ b/.github/workflows/Deploy Dev.yaml @@ -55,8 +55,11 @@ jobs: gcloud --quiet auth configure-docker $GAR_LOCATION-docker.pkg.dev - name: Build Wallet + env: + CLOUDSMITH_TOKEN: ${{ secrets.CLOUDSMITH_TOKEN }} run: |- docker build \ + --secret id=cloudsmith_token,env=CLOUDSMITH_TOKEN \ --tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:$GITHUB_SHA" \ --tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:${GITHUB_REF##*/}" \ --tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:latest" \ diff --git a/.github/workflows/Deploy Prod.yaml b/.github/workflows/Deploy Prod.yaml index 0ba27c3..0811ddd 100644 --- a/.github/workflows/Deploy Prod.yaml +++ b/.github/workflows/Deploy Prod.yaml @@ -53,8 +53,11 @@ jobs: gcloud --quiet auth configure-docker $GAR_LOCATION-docker.pkg.dev - name: Build Wallet + env: + CLOUDSMITH_TOKEN: ${{ secrets.CLOUDSMITH_TOKEN }} run: |- docker build \ + --secret id=cloudsmith_token,env=CLOUDSMITH_TOKEN \ --tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:$GITHUB_SHA" \ --tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:${GITHUB_REF##*/}" \ --tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:latest" \ diff --git a/.gitignore b/.gitignore index a8299ab..48bf058 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,4 @@ oneshot-wallet-*-deploy oneshot-wallet-*-deploy.pub *-deploy *-deploy.pub +Circle Onramp Kit Beta testing.md diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e87ec79 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@crcl-main:registry=https://npm.cloudsmith.io/circle/common-private/ +//npm.cloudsmith.io/circle/common-private/:_authToken=${CLOUDSMITH_TOKEN} diff --git a/AGENTS.md b/AGENTS.md index dae7592..218e7b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,11 +18,13 @@ Prefer **clean code over backwards compatibility**. Do not add legacy redirects, | `src/lib/types/domain/` | Domain DTOs (e.g. `KnownAsset`, `TrackedAsset`, `WalletConfig`) | | `src/lib/types/events/` | Domain event classes (one file each) | | `src/lib/interfaces/{business,data,utils}/` | Layer interfaces | -| `src/lib/implementations/{business,data,utils}/` | Layer implementations | +| `src/lib/implementations/{business,data,utils}/` | Layer implementations (`CircleProvider` for AppKit onramp) | | `src/assets/` | Static media only (SVGs, images) | Test Host Layer: `host/` (`npm run dev:host`). Style via Host RPC `setStyle`, not in-wallet debug knobs. +Fiat onramp: Asset Details **Buy** and host RPC `onramp({ chainId?, amount? })` open `OnrampView` (Circle AppKit). Sessions come from Relayer `POST /wallet/onramp` — never put the Circle kit key in this SPA. Default UI is `mountIframe`; for local/ngrok before Circle CSP allowlisting, set `localStorage.circlePopup = "true"` to use `openWindow` (prefetch session, then a sync click). Cloudsmith: set `CLOUDSMITH_TOKEN` before `npm install` (see README). + ### Form validation UX Primary submit actions (e.g. Send in `TransferTokensModal`) stay **disabled until every required field is valid**. Do not leave the button enabled and only reject on click. Empty fields show no error text; invalid non-empty input shows inline errors; the CTA enables only when the whole form is ready. diff --git a/Dockerfile b/Dockerfile index 0083a63..143f5e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,13 @@ WORKDIR /app COPY package.json package-lock.json ./ COPY host/package.json ./host/ -RUN npm ci +COPY .npmrc ./ + +# Circle @crcl-main/* requires CLOUDSMITH_TOKEN BuildKit secret +RUN --mount=type=secret,id=cloudsmith_token \ + CLOUDSMITH_TOKEN="$(cat /run/secrets/cloudsmith_token)" \ + npm ci \ + && rm -f .npmrc COPY index.html vite.config.ts tsconfig.json tsconfig.node.json components.json ./ COPY src ./src diff --git a/README.md b/README.md index c5cd481..f0931d8 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,36 @@ Production deliverable: a static **nginx** Docker image (no server-side runtime) ## Setup +Circle’s private AppKit canary requires Cloudsmith auth. The repo includes `.npmrc`; +export your token before installing: + +```bash +# Linux / macOS +export CLOUDSMITH_TOKEN= +``` + +```powershell +# Windows PowerShell +$env:CLOUDSMITH_TOKEN = "" +``` + +CI Docker builds expect GitHub Actions secret `CLOUDSMITH_TOKEN`. + ```bash npm install cp .env.example .env # set NGROK_AUTHTOKEN (and optional NGROK_DOMAIN) ``` +Fiat onramp uses Circle AppKit (`Buy` in Asset Details, or host RPC `onramp`). +Sessions are minted by the Relayer (`POST /wallet/onramp`); the kit key never +ships in this SPA. Inline iframe onramp requires your wallet domain to be +registered with Circle for CSP — see Circle’s beta docs. For local/ngrok +testing before CSP allowlisting, force the popup flow: + +```js +localStorage.setItem("circlePopup", "true") +``` + ## Develop ```bash diff --git a/host/src/components/WalletActions.tsx b/host/src/components/WalletActions.tsx index f6ff06d..1c093b5 100644 --- a/host/src/components/WalletActions.tsx +++ b/host/src/components/WalletActions.tsx @@ -47,6 +47,7 @@ export interface IWalletActionsProps { onUnfocusWallet: () => void; onAddUsdcArc: () => void; onAddUsdtBase: () => void; + onOnramp: () => void; } export function WalletActions({ @@ -78,6 +79,7 @@ export function WalletActions({ onUnfocusWallet, onAddUsdcArc, onAddUsdtBase, + onOnramp, }: IWalletActionsProps) { const meta = hostChainMeta(chainId); @@ -310,6 +312,24 @@ export function WalletActions({ + +
+ +

+ Open Circle fiat onramp via onramp (Buy crypto into the + unlocked wallet address). +

+
+ +
+
); } diff --git a/host/src/hooks/useHostTestActions.ts b/host/src/hooks/useHostTestActions.ts index d1d6bb4..4a30d2c 100644 --- a/host/src/hooks/useHostTestActions.ts +++ b/host/src/hooks/useHostTestActions.ts @@ -481,6 +481,28 @@ export function useHostTestActions({ })(); }; + const handleOnramp = () => { + const proxy = proxyRef.current; + if (!proxy) return; + setBusy(true); + reportStatus("Opening onramp…"); + void (async () => { + try { + await proxy.rpc("onramp", {}); + proxy.showWallet(); + setWalletVisible(true); + reportStatus("Onramp closed."); + } catch (error) { + reportStatus( + error instanceof Error ? error.message : "onramp failed", + true, + ); + } finally { + setBusy(false); + } + })(); + }; + const walletActionProps = { ready, busy, @@ -510,6 +532,7 @@ export function useHostTestActions({ onUnfocusWallet: handleUnfocusWallet, onAddUsdcArc: handleAddUsdcArc, onAddUsdtBase: handleAddUsdtBase, + onOnramp: handleOnramp, }; return { diff --git a/package-lock.json b/package-lock.json index c0b2e1c..aa8e059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@1shotapi/ows-signer-utils": "^0.4.0", "@1shotapi/ows-types": "^0.2.4", "@1shotapi/ows-wallet-utils": "^0.2.0", + "@crcl-main/app-kit": "^1.10.0-canary-feature-onramp-kit-sdk.1784835391", "@fontsource-variable/geist": "^5.2.9", "@metamask/smart-accounts-kit": "^1.7.0", "@simplewebauthn/browser": "^13.3.0", @@ -646,6 +647,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -769,6 +779,544 @@ "win32" ] }, + "node_modules/@coral-xyz/anchor": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.31.1.tgz", + "integrity": "sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@coral-xyz/anchor-errors": "^0.31.1", + "@coral-xyz/borsh": "^0.31.1", + "@noble/hashes": "^1.3.1", + "@solana/web3.js": "^1.69.0", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^6.3.0", + "cross-fetch": "^3.1.5", + "eventemitter3": "^4.0.7", + "pako": "^2.0.3", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=17" + } + }, + "node_modules/@coral-xyz/anchor-errors": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz", + "integrity": "sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/@coral-xyz/borsh": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.31.1.tgz", + "integrity": "sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.69.0" + } + }, + "node_modules/@crcl-main/app-kit": { + "version": "1.10.0-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/app-kit/-/app-kit-1.10.0-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-FfLR6nr/HmqaqSs7ZtCL4jmJzhF4sL+ru0oHGbtO+lcbb3TYBy5vYeuyYgWHQl79e4hCmJUN3xYw20l8PT5MVA==", + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@crcl-main/bridge-kit": "1.12.1-canary-feature-onramp-kit-sdk.1784835391", + "@crcl-main/earn-kit": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "@crcl-main/onramp-kit": "0.0.1-canary-feature-onramp-kit-sdk.1784835391", + "@crcl-main/provider-gateway-v1": "1.1.3-canary-feature-onramp-kit-sdk.1784835391", + "@crcl-main/swap-kit": "1.4.0-canary-feature-onramp-kit-sdk.1784835391", + "@crcl-main/unified-balance-kit": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@noble/curves": "1.4.2", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bn.js": "^5.2.3", + "bs58": "6.0.0", + "buffer": "^6.0.3", + "pino": "10.1.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/app-kit/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/app-kit/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/app-kit/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/bridge-kit": { + "version": "1.12.1-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/bridge-kit/-/bridge-kit-1.12.1-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-z59Mus6FOdFJloXNi5hkzKwHjDFdblave8+e5sDqrDy6tqNBu2aq7uez3vjMfvnqRu5elaBEAAASonm8jQeyGQ==", + "dependencies": { + "@crcl-main/provider-cctp-v2": "1.10.0-canary-feature-onramp-kit-sdk.1784835391", + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bs58": "6.0.0", + "pino": "10.1.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/bridge-kit/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/earn-kit": { + "version": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/earn-kit/-/earn-kit-1.3.0-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-IBcDCDN32X3Jy28SP9e5XC8TbJ/UgzBqn8dA/9cAH7ttA0tyCeZTPwMiLXeNI0nHQ5zrJTTY1Sw3latGAFWn/A==", + "dependencies": { + "@crcl-main/provider-earn-service": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bs58": "6.0.0", + "pino": "10.1.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/earn-kit/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/onramp-kit": { + "version": "0.0.1-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/onramp-kit/-/onramp-kit-0.0.1-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-72nyxHll7kEMcbL/U3scA39njcVpDQCVfCEQ6EcH+7E964/63m0fHEHfPFmvnNjph5Abneqq32dOsbKlWeYzFg==", + "dependencies": { + "pino": "10.1.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/onramp-kit/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/provider-cctp-v2": { + "version": "1.10.0-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/provider-cctp-v2/-/provider-cctp-v2-1.10.0-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-232LEFAViDGzIhpC9rD7yiREc1MmvexErEh1lQ/5lkax08/qTULWwi6O70GhMtfIKq9AB0rV8OnpC5GsY4UCeQ==", + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@noble/curves": "1.4.2", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bs58": "6.0.0", + "buffer": "^6.0.3", + "pino": "10.1.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@solana/web3.js": "^1.98.2" + }, + "peerDependenciesMeta": { + "@solana/web3.js": { + "optional": true + } + } + }, + "node_modules/@crcl-main/provider-cctp-v2/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/provider-cctp-v2/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/provider-cctp-v2/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/provider-earn-service": { + "version": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/provider-earn-service/-/provider-earn-service-1.3.0-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-93TzDgvhFMXBn/DOOB0XE0LKg2NGsmn4nfuJhlrpTdtuYVzvRANPthdv7gooDL1St3EfOxnh68gt5YkJlcrBnQ==", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bs58": "6.0.0", + "pino": "10.1.0", + "viem": "^2.30.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/provider-earn-service/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/provider-gateway-v1": { + "version": "1.1.3-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/provider-gateway-v1/-/provider-gateway-v1-1.1.3-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-KoDN0Fh1qSt9URuc+/pAEc3szHvB2zk1fNuFgKh6yTAwI70G4orNivo7GpWp6OkPq7vboxpGXvLW0ptepi6KyQ==", + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@noble/curves": "1.4.2", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bn.js": "^5.2.3", + "bs58": "6.0.0", + "buffer": "^6.0.3", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/provider-gateway-v1/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/provider-gateway-v1/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/provider-gateway-v1/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/provider-stablecoin-service-swap": { + "version": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/provider-stablecoin-service-swap/-/provider-stablecoin-service-swap-1.3.0-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-DcE2yoS7CPbsmkbJpX+QMoLVSjMPrXkQub8xNVN+YzKxKPwHmKwLN5naQ6QV5Te6soxbMUPOBpHqkDo/yKO/lA==", + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@noble/curves": "1.4.2", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bn.js": "^5.2.3", + "bs58": "^6.0.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/provider-stablecoin-service-swap/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/provider-stablecoin-service-swap/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/provider-stablecoin-service-swap/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/swap-kit": { + "version": "1.4.0-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/swap-kit/-/swap-kit-1.4.0-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-MWDSp2iJ5aMSer+mlRiDPteqUo9QLNY/krQGS4ZlaEz5ndf7ceGCtBjHdNh7Yf1Fp828BYBoQQE1EFcNk13ALA==", + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@crcl-main/provider-stablecoin-service-swap": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@noble/curves": "1.4.2", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bn.js": "^5.2.3", + "bs58": "6.0.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/swap-kit/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/swap-kit/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/swap-kit/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@crcl-main/unified-balance-kit": { + "version": "1.3.0-canary-feature-onramp-kit-sdk.1784835391", + "resolved": "https://npm.cloudsmith.io/circle/common-private/@crcl-main/unified-balance-kit/-/unified-balance-kit-1.3.0-canary-feature-onramp-kit-sdk.1784835391.tgz", + "integrity": "sha512-OX19leoAPWlSygx75jbROHZahcMI6y27wlUaBdaCe1IeUHgGM1YrqGuLngmC1eTGC5fmA4Sl+SrJ4v5HrVKpCQ==", + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@crcl-main/provider-gateway-v1": "1.1.3-canary-feature-onramp-kit-sdk.1784835391", + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/units": "^5.8.0", + "@noble/curves": "1.4.2", + "@solana/web3.js": "^1.98.4", + "abitype": "^1.1.0", + "bn.js": "^5.2.3", + "bs58": "6.0.0", + "zod": "3.25.67" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@crcl-main/unified-balance-kit/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/unified-balance-kit/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@crcl-main/unified-balance-kit/node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.75.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz", @@ -1559,49 +2107,462 @@ "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", "license": "MIT", "dependencies": { - "@ethereumjs/util": "^8.1.0", - "crc-32": "^1.2.0" + "@ethereumjs/util": "^8.1.0", + "crc-32": "^1.2.0" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/tx": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", + "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/common": "^3.2.0", + "@ethereumjs/rlp": "^4.0.1", + "@ethereumjs/util": "^8.1.0", + "ethereum-cryptography": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethereumjs/rlp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", - "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", - "license": "MPL-2.0", - "bin": { - "rlp": "bin/rlp" - }, - "engines": { - "node": ">=14" + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" } }, - "node_modules/@ethereumjs/tx": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", - "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", - "license": "MPL-2.0", + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", "dependencies": { - "@ethereumjs/common": "^3.2.0", - "@ethereumjs/rlp": "^4.0.1", - "@ethereumjs/util": "^8.1.0", - "ethereum-cryptography": "^2.0.0" - }, - "engines": { - "node": ">=14" + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethereumjs/util": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", - "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", - "license": "MPL-2.0", + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", "dependencies": { - "@ethereumjs/rlp": "^4.0.1", - "ethereum-cryptography": "^2.0.0", - "micro-ftch": "^0.3.1" - }, - "engines": { - "node": ">=14" + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" } }, "node_modules/@floating-ui/core": { @@ -3398,6 +4359,12 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", @@ -5455,6 +6422,127 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@solana/web3.js/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", @@ -5827,6 +6915,15 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -5874,7 +6971,6 @@ "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -5910,12 +7006,27 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/types": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", @@ -6028,6 +7139,18 @@ "agent-install": "bin/agent-install.mjs" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -6150,6 +7273,15 @@ "astring": "bin/astring" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/atomically": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", @@ -6181,6 +7313,32 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.43", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", @@ -6193,6 +7351,12 @@ "node": ">=6.0.0" } }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -6230,6 +7394,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/borsh/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/borsh/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -6254,6 +7447,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.28.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", @@ -6287,6 +7486,62 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-layout": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz", + "integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==", + "license": "MIT", + "engines": { + "node": ">=4.5" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -6806,6 +8061,15 @@ "node": ">=0.8" } }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6972,6 +8236,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7081,6 +8357,27 @@ "integrity": "sha512-YmCu4856jkgKT1Nh6fwRdeVrM6Ydf/fBnq51tpmSfX+jOcUMTxh31yH6hjKScRenhB2oDSvA9oooxcpjogPeig==", "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -7197,10 +8494,25 @@ "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", "dev": true, "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } }, "node_modules/esbuild": { "version": "0.28.1", @@ -7779,6 +9091,14 @@ "express": ">= 4.11" } }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7817,6 +9137,12 @@ "license": "MIT", "peer": true }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", @@ -8164,6 +9490,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -8193,6 +9529,17 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hono": { "version": "4.12.30", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", @@ -8231,6 +9578,15 @@ "node": ">=18.18.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -8247,6 +9603,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8724,6 +10100,15 @@ "node": ">=18" } }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isows": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", @@ -8739,6 +10124,90 @@ "ws": "*" } }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/jayson/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -8758,6 +10227,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8832,6 +10307,12 @@ "license": "MIT", "peer": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9384,6 +10865,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -9456,6 +10949,38 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", @@ -9538,6 +11063,15 @@ "node": ">= 10" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -10016,6 +11550,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -10140,6 +11711,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -10239,6 +11826,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/radix-ui": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.2.tgz", @@ -10609,6 +12202,15 @@ } } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/recast": { "version": "0.23.12", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", @@ -10774,6 +12376,51 @@ "node": ">= 18" } }, + "node_modules/rpc-websockets": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz", + "integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^14.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -10809,6 +12456,35 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11106,6 +12782,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11124,6 +12809,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -11168,6 +12862,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -11291,6 +13000,12 @@ "dev": true, "license": "MIT" }, + "node_modules/superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==", + "license": "MIT" + }, "node_modules/systeminformation": { "version": "5.31.17", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.17.tgz", @@ -11374,6 +13089,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -11418,6 +13147,18 @@ "node": ">=0.6" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-brand": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ts-brand/-/ts-brand-0.2.0.tgz", @@ -11529,7 +13270,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11565,7 +13305,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -11682,6 +13421,20 @@ } } }, + "node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11880,6 +13633,22 @@ "dev": true, "license": "MIT" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", diff --git a/package.json b/package.json index 9aabf7a..5e49227 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "clean": "node scripts/clean.mjs && npm run clean -w @1shotapi/oneshot-wallet-host", "lint": "tsc -p tsconfig.json --noEmit", "preview": "vite preview", - "dockerize": "docker build -t oneshot-wallet .", + "dockerize": "docker build --secret id=cloudsmith_token,env=CLOUDSMITH_TOKEN -t oneshot-wallet .", "doctor": "npx react-doctor@latest" }, "dependencies": { @@ -26,6 +26,7 @@ "@1shotapi/ows-signer-utils": "^0.4.0", "@1shotapi/ows-types": "^0.2.4", "@1shotapi/ows-wallet-utils": "^0.2.0", + "@crcl-main/app-kit": "^1.10.0-canary-feature-onramp-kit-sdk.1784835391", "@fontsource-variable/geist": "^5.2.9", "@metamask/smart-accounts-kit": "^1.7.0", "@simplewebauthn/browser": "^13.3.0", diff --git a/skills/oneshot-embedded-wallet/SKILL.md b/skills/oneshot-embedded-wallet/SKILL.md index b9db67e..9c8d8ea 100644 --- a/skills/oneshot-embedded-wallet/SKILL.md +++ b/skills/oneshot-embedded-wallet/SKILL.md @@ -3,7 +3,7 @@ name: oneshot-embedded-wallet description: >- Integrate the 1Shot embedded wallet (OWS Host Layer) with @1shotapi/ows-provider. Use when embedding wallet.1shotapi.com, wiring OWSProxy, EIP-1193, credentials, - or custom RPC such as setStyle / focusWallet / addAsset / createAccount for + or custom RPC such as setStyle / focusWallet / addAsset / createAccount / onramp for theming, host-driven focus mode, tracked assets, and first-party Safari create. license: MIT metadata: @@ -232,7 +232,7 @@ Users can also add assets from the Balances tab without a host RPC. The Balances ## Custom RPC — `createAccount` -Used by the first-party **`/create/`** host page (Safari passkey create). Hosts embedding the wallet normally do **not** call this — the branding layer opens `/create/` itself when needed. +Used by the first-party **`/create/`** host page (Safari passkey create). Hosts embedding the wallet normally do **not** call this — the branding layer opens `/create/` itself when needed. Not exposed as a playground button. ```typescript const result = await proxy.rpc("createAccount"); @@ -246,6 +246,24 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" }); |--------|--------|--------| | `createAccount` | `{ accountName?: string }` optional | Runs setup create (passkey + relayer register); returns credential id | +## Custom RPC — `onramp` + +Opens Circle fiat onramp fullscreen inside the Branding Layer for the unlocked EVM address. + +```typescript +await proxy.rpc("onramp", { + chainId: 8453, // optional — decimal chain id for catalog scoping + amount: "50", // optional — amount hint when supported by the kit +}); +// or: await proxy.rpc("onramp", {}); +``` + +| Method | Params | Behavior | +|--------|--------|----------| +| `onramp` | `{ chainId?: number, amount?: string }` | Shows wallet, mounts Circle AppKit onramp; session minted via Relayer `POST /wallet/onramp` | + +Returns `{ ok: true }` when the user closes the onramp view. The Relayer holds the Circle kit key; the browser only receives a single-use session. Inline iframe requires Circle CSP allowlisting of the wallet origin; for local/ngrok testing the branding layer honors `localStorage.setItem("circlePopup", "true")` and uses AppKit `openWindow` instead. + ## Other Host APIs | API | Use | @@ -253,7 +271,7 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" }); | `proxy.ethereum.request(...)` | EIP-1193 (accounts, sign, chain, …) | | `proxy.credentials.*` | OID4 offer / present (when enabled in wallet) | | `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call | -| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, …) | +| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, …) | ## Hard rules @@ -262,3 +280,4 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" }); - Theme with `setStyle`; do not ask integrators to fork CSS for basic brand colors / product name. - Use `focusWallet` / `unfocusWallet` for host-driven single-asset flows; do not expose mode switching in the wallet UI. - Use `addAsset` when the host wants a lasting Balances entry; expect a confirm modal (contrast with `focusWallet`). +- Use `onramp` (or the in-wallet Buy button) for fiat → crypto; do not put the Circle kit key in the Host or Branding Layer. diff --git a/src/circle/CircleContext.tsx b/src/circle/CircleContext.tsx new file mode 100644 index 0000000..bff0537 --- /dev/null +++ b/src/circle/CircleContext.tsx @@ -0,0 +1,30 @@ +import { + createContext, + useContext, + useMemo, + type ReactNode, +} from "react"; +import type { ICircleProvider } from "../lib/interfaces/utils/ICircleProvider"; + +const CircleContext = createContext(null); + +export function CircleContextProvider({ + provider, + children, +}: { + provider: ICircleProvider; + children: ReactNode; +}) { + const value = useMemo(() => provider, [provider]); + return ( + {children} + ); +} + +export function useCircle(): ICircleProvider { + const ctx = useContext(CircleContext); + if (!ctx) { + throw new Error("useCircle must be used within CircleContextProvider"); + } + return ctx; +} diff --git a/src/circle/circleChains.ts b/src/circle/circleChains.ts new file mode 100644 index 0000000..8c7b4c2 --- /dev/null +++ b/src/circle/circleChains.ts @@ -0,0 +1,39 @@ +/** Map EVM decimal chain id → Circle onramp chain label (`Blockchain` enum). */ +const CIRCLE_CHAIN_BY_DECIMAL: ReadonlyMap = new Map([ + [1, "Ethereum"], + [10, "Optimism"], + [137, "Polygon"], + [8453, "Base"], + [42161, "Arbitrum"], + [43114, "Avalanche"], + [59144, "Linea"], + [130, "Unichain"], +]); + +/** + * Convert a hex (`0x…`) or decimal chain id to Circle's display chain label. + * Returns null when unsupported (widget shows full catalog). + */ +export function circleChainLabelFromChainId( + chainId: string | number | bigint, +): string | null { + let decimal: number; + if (typeof chainId === "number") { + decimal = chainId; + } else if (typeof chainId === "bigint") { + decimal = Number(chainId); + } else { + const trimmed = chainId.trim(); + if (/^0x[0-9a-fA-F]+$/i.test(trimmed)) { + decimal = Number(BigInt(trimmed)); + } else if (/^\d+$/.test(trimmed)) { + decimal = Number(trimmed); + } else { + return null; + } + } + if (!Number.isFinite(decimal)) { + return null; + } + return CIRCLE_CHAIN_BY_DECIMAL.get(decimal) ?? null; +} diff --git a/src/circle/circlePopup.ts b/src/circle/circlePopup.ts new file mode 100644 index 0000000..0da9123 --- /dev/null +++ b/src/circle/circlePopup.ts @@ -0,0 +1,17 @@ +/** Dev override key: `localStorage.setItem("circlePopup", "true")`. */ +export const CIRCLE_POPUP_STORAGE_KEY = "circlePopup"; + +/** + * When true, onramp uses AppKit `openWindow` instead of `mountIframe`. + * Needed for local/ngrok hosts that are not yet on Circle’s iframe CSP allowlist. + */ +export function isCirclePopupPreferred(): boolean { + try { + return ( + typeof localStorage !== "undefined" && + localStorage.getItem(CIRCLE_POPUP_STORAGE_KEY) === "true" + ); + } catch { + return false; + } +} diff --git a/src/circle/onrampTypes.ts b/src/circle/onrampTypes.ts new file mode 100644 index 0000000..5c2b61d --- /dev/null +++ b/src/circle/onrampTypes.ts @@ -0,0 +1,9 @@ +import type { EVMAccountAddress } from "@1shotapi/ows-types"; + +/** Params for opening the Circle onramp fullscreen view. */ +export type IOnrampOpenRequest = { + destinationAddress: EVMAccountAddress; + chainId?: number; + amount?: string; + tokenSymbol?: string; +}; diff --git a/src/circle/openOnramp.ts b/src/circle/openOnramp.ts new file mode 100644 index 0000000..6d1fc82 --- /dev/null +++ b/src/circle/openOnramp.ts @@ -0,0 +1,13 @@ +import { pushModal } from "../wallet/pushModal"; +import type { IOnrampOpenRequest } from "./onrampTypes"; + +/** Open the shared fullscreen Circle onramp (Buy + host `onramp` RPC). */ +export function openOnramp(request: IOnrampOpenRequest): Promise { + return pushModal(({ id, resolve, reject }) => ({ + id, + kind: "onramp", + request, + resolve, + reject, + })); +} diff --git a/src/components/AssetDetails.tsx b/src/components/AssetDetails.tsx index 249accc..feedf07 100644 --- a/src/components/AssetDetails.tsx +++ b/src/components/AssetDetails.tsx @@ -13,10 +13,10 @@ import { useWallet } from "../wallet/WalletProvider"; import { resolveActiveAddress } from "../wallet/activeAddress"; import { useLiveTrackedBalance } from "../wallet/useLiveTrackedBalance"; import { useWalletSessionStore } from "../wallet/sessionStore"; +import { openOnramp } from "../circle/openOnramp"; import { BalanceDisplay } from "./BalanceDisplay"; import { TransactionHistory } from "./TransactionHistory"; import { ReceiveModal } from "./modals/ReceiveModal"; -import { PurchaseComingSoonModal } from "./modals/PurchaseComingSoonModal"; import { TransferTokensModal } from "./modals/TransferTokensModal"; export interface IAssetDetailsProps { @@ -39,7 +39,7 @@ export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { ); const [receiveOpen, setReceiveOpen] = useState(false); const [sendOpen, setSendOpen] = useState(false); - const [purchaseOpen, setPurchaseOpen] = useState(false); + const [buyBusy, setBuyBusy] = useState(false); const { balance, decimals } = useLiveTrackedBalance( assetProp.id, @@ -72,12 +72,30 @@ export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { solanaAddress, }); const canSend = asset.type === EAssetType.Erc20; + const canBuy = + Boolean(evmAddress) && String(evmAddress).toLowerCase() !== "0x0"; const openSend = useCallback(() => { void requestBalanceRefresh(asset.id); setSendOpen(true); }, [asset.id, requestBalanceRefresh]); + const openBuy = useCallback(() => { + if (!evmAddress || buyBusy) return; + setBuyBusy(true); + void openOnramp({ + destinationAddress: evmAddress, + chainId: Number(BigInt(asset.chainId)), + tokenSymbol: asset.symbol, + }) + .catch(() => { + /* user closed or mint failed — OnrampView surfaces errors */ + }) + .finally(() => { + setBuyBusy(false); + }); + }, [asset.chainId, asset.symbol, buyBusy, evmAddress]); + return (
@@ -113,7 +131,8 @@ export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { setPurchaseOpen(true)} + disabled={!canBuy || buyBusy} + onClick={openBuy} > @@ -152,9 +171,6 @@ export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { }} /> ) : null} - {purchaseOpen ? ( - setPurchaseOpen(false)} /> - ) : null}
); } diff --git a/src/components/ModalHost.tsx b/src/components/ModalHost.tsx index ea4e6db..ec33ddb 100644 --- a/src/components/ModalHost.tsx +++ b/src/components/ModalHost.tsx @@ -19,6 +19,7 @@ import { ImportPrivateKeyModal } from "./modals/ImportPrivateKeyModal"; import { AdvancedOptionsModal } from "./modals/AdvancedOptionsModal"; import { AddAssetModal } from "./modals/AddAssetModal"; import { OpenCreateTabModal } from "./modals/OpenCreateTabModal"; +import { OnrampView } from "./OnrampView"; export function ModalHost() { const activeModal = useModalStore((state) => state.activeModal); @@ -114,6 +115,16 @@ export function ModalHost() { onResolve={activeModal.resolve} /> ); + case "onramp": + return ( + activeModal.resolve()} + /> + ); default: return null; } diff --git a/src/components/OnrampView.tsx b/src/components/OnrampView.tsx new file mode 100644 index 0000000..e78ac59 --- /dev/null +++ b/src/components/OnrampView.tsx @@ -0,0 +1,277 @@ +import { useEffect, useRef, useState } from "react"; +import type { + AppKitOnrampOperations, + OnrampSession, + OnrampWidget, +} from "@crcl-main/app-kit"; +import type { EVMAccountAddress } from "@1shotapi/ows-types"; +import { Modal, type ModalAction } from "./Modal"; +import { useCircle } from "../circle/CircleContext"; +import { circleChainLabelFromChainId } from "../circle/circleChains"; +import { isCirclePopupPreferred } from "../circle/circlePopup"; +import type { IOnrampOpenRequest } from "../circle/onrampTypes"; + +export type IOnrampViewProps = IOnrampOpenRequest & { + onClose: () => void; +}; + +/** + * Full-screen Circle AppKit onramp inside the Branding Layer shell. + * Default: inline iframe. With `localStorage.circlePopup === "true"`: popup + * window (session is prefetched; open must be a sync click — Circle requirement). + */ +export function OnrampView({ + destinationAddress, + chainId, + amount, + tokenSymbol, + onClose, +}: IOnrampViewProps) { + const circle = useCircle(); + const usePopup = isCirclePopupPreferred(); + const containerRef = useRef(null); + const widgetRef = useRef(null); + const onrampRef = useRef(null); + const sessionRef = useRef(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [popupReady, setPopupReady] = useState(false); + const [popupOpened, setPopupOpened] = useState(false); + + useEffect(() => { + let cancelled = false; + const body = buildSessionBody({ + destinationAddress, + chainId, + amount, + tokenSymbol, + }); + + if (usePopup) { + void (async () => { + try { + const [onramp, url] = await Promise.all([ + circle.getOnramp(), + circle.getSessionUrl(), + ]); + if (cancelled) return; + onrampRef.current = onramp; + sessionRef.current = await onramp.fetchSession({ url, body }); + if (cancelled) return; + setLoading(false); + setPopupReady(true); + setError(null); + } catch (err: unknown) { + if (!cancelled) { + setLoading(false); + setPopupReady(false); + setError( + err instanceof Error ? err.message : "Failed to prepare onramp", + ); + } + } + })(); + + return () => { + cancelled = true; + widgetRef.current?.close(); + widgetRef.current = null; + onrampRef.current = null; + sessionRef.current = null; + }; + } + + const container = containerRef.current; + if (!container) { + return; + } + + void (async () => { + try { + const [onramp, url] = await Promise.all([ + circle.getOnramp(), + circle.getSessionUrl(), + ]); + if (cancelled) return; + + const mount = async () => { + const session = await onramp.fetchSession({ url, body }); + if (cancelled) return; + widgetRef.current?.close(); + widgetRef.current = onramp.mountIframe({ + session, + container, + onSessionExpired: () => { + void mount().catch((err: unknown) => { + if (!cancelled) { + setError( + err instanceof Error + ? err.message + : "Failed to refresh onramp session", + ); + } + }); + }, + }); + if (!cancelled) { + setLoading(false); + setError(null); + } + }; + + await mount(); + } catch (err: unknown) { + if (!cancelled) { + setLoading(false); + setError( + err instanceof Error ? err.message : "Failed to open onramp", + ); + } + } + })(); + + return () => { + cancelled = true; + widgetRef.current?.close(); + widgetRef.current = null; + }; + }, [amount, chainId, circle, destinationAddress, tokenSymbol, usePopup]); + + const openPopup = () => { + const onramp = onrampRef.current; + const session = sessionRef.current; + if (!onramp || !session) { + return; + } + + const result = onramp.openWindow({ + session, + onSessionExpired: () => { + setPopupOpened(false); + setPopupReady(false); + setLoading(true); + setError(null); + void (async () => { + try { + const url = await circle.getSessionUrl(); + const body = buildSessionBody({ + destinationAddress, + chainId, + amount, + tokenSymbol, + }); + sessionRef.current = await onramp.fetchSession({ url, body }); + setLoading(false); + setPopupReady(true); + } catch (err: unknown) { + setLoading(false); + setError( + err instanceof Error + ? err.message + : "Failed to refresh onramp session", + ); + } + })(); + }, + }); + + if (result.status === "blocked") { + setError(result.errorMessage); + setPopupOpened(false); + return; + } + + widgetRef.current?.close(); + widgetRef.current = result.widget; + setError(null); + setPopupOpened(true); + }; + + const actions: ModalAction[] = []; + if (usePopup && popupReady) { + actions.push({ + label: popupOpened ? "Reopen onramp" : "Open onramp", + onClick: openPopup, + variant: "primary", + }); + } + actions.push({ label: "Close", onClick: onClose, variant: "secondary" }); + + return ( + +
+ {error ? ( +

+ {error} +

+ ) : null} + {loading && !error ? ( +

+ {usePopup ? "Preparing onramp…" : "Loading onramp…"} +

+ ) : null} + {usePopup && popupReady && !popupOpened && !error ? ( +

+ Circle opens in a popup (local/ngrok CSP bypass). Click Open onramp + — browsers block popups after an async delay. +

+ ) : null} + {usePopup && popupOpened && !error ? ( +

+ Onramp opened in a popup. Complete the purchase there, then close + this dialog. +

+ ) : null} + {!usePopup ? ( +
+ ) : null} +
+ + ); +} + +function buildSessionBody(request: { + destinationAddress: EVMAccountAddress; + chainId?: number; + amount?: string; + tokenSymbol?: string; +}) { + const address = String(request.destinationAddress).toLowerCase(); + const chains: string[] = []; + const chainLabel = + request.chainId != null + ? circleChainLabelFromChainId(request.chainId) + : null; + if (chainLabel) { + chains.push(chainLabel); + } + + const tokens = + request.tokenSymbol && request.tokenSymbol.trim() + ? [request.tokenSymbol.trim().toUpperCase()] + : undefined; + + const assets = + chains.length > 0 || (tokens && tokens.length > 0) + ? { + ...(chains.length > 0 ? { chains } : {}), + ...(tokens && tokens.length > 0 ? { tokens } : {}), + } + : undefined; + + return { + userId: address, + destinationAddress: address, + ...(assets ? { assets } : {}), + ...(request.amount ? { amount: request.amount } : {}), + }; +} diff --git a/src/components/modals/PurchaseComingSoonModal.tsx b/src/components/modals/PurchaseComingSoonModal.tsx deleted file mode 100644 index f56ecb2..0000000 --- a/src/components/modals/PurchaseComingSoonModal.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Modal } from "../Modal"; - -export interface IPurchaseComingSoonModalProps { - onClose: () => void; -} - -/** Placeholder until an onramp provider is integrated. */ -export function PurchaseComingSoonModal({ - onClose, -}: IPurchaseComingSoonModalProps) { - return ( - -

- Purchase capabilities coming soon -

-
- ); -} diff --git a/src/lib/implementations/utils/CircleProvider.ts b/src/lib/implementations/utils/CircleProvider.ts new file mode 100644 index 0000000..57c3194 --- /dev/null +++ b/src/lib/implementations/utils/CircleProvider.ts @@ -0,0 +1,42 @@ +import { AppKit } from "@crcl-main/app-kit"; +import type { ICircleProvider } from "../../interfaces/utils/ICircleProvider"; +import type { IConfigProvider } from "../../interfaces/utils/IConfigProvider"; + +/** + * Lazily constructs Circle {@link AppKit} and caches the onramp handle. + */ +export class CircleProvider implements ICircleProvider { + private kit: AppKit | null = null; + private onrampPromise: Promise | null = null; + + constructor(private readonly configProvider: IConfigProvider) {} + + async getOnramp(): Promise { + if (this.onrampPromise) { + return this.onrampPromise; + } + this.onrampPromise = this.createOnramp(); + try { + return await this.onrampPromise; + } catch (error) { + this.onrampPromise = null; + this.kit = null; + throw error; + } + } + + async getSessionUrl(): Promise { + const config = await this.configProvider.getConfig(); + return `${config.relayerBaseUrl.replace(/\/$/, "")}/wallet/onramp`; + } + + private async createOnramp(): Promise { + const config = await this.configProvider.getConfig(); + this.kit = new AppKit({ + onramp: { + widgetBaseUrl: config.onrampWidgetBaseUrl, + }, + }); + return this.kit.onramp; + } +} diff --git a/src/lib/implementations/utils/ConfigProvider.ts b/src/lib/implementations/utils/ConfigProvider.ts index fbb522c..8707c42 100644 --- a/src/lib/implementations/utils/ConfigProvider.ts +++ b/src/lib/implementations/utils/ConfigProvider.ts @@ -10,6 +10,8 @@ const DEFAULT_TRACKED_ASSETS_STORAGE_KEY = "ows.tracked-assets.v2"; const DEFAULT_CREDENTIALS_STORAGE_KEY = "ows.credentials.v2"; const DEFAULT_ASSET_ACTIVITY_LIMIT = 10; const DEFAULT_ASSET_ACTIVITY_MAX_OPTIMISTIC = 100; +const DEFAULT_ONRAMP_WIDGET_BASE_URL = + "https://onramp.arc.io/launch/onramp/v1"; /** * Resolves {@link WalletConfig} from the Branding Layer iframe host. @@ -30,6 +32,7 @@ export class ConfigProvider implements IConfigProvider { DEFAULT_CREDENTIALS_STORAGE_KEY, DEFAULT_ASSET_ACTIVITY_LIMIT, DEFAULT_ASSET_ACTIVITY_MAX_OPTIMISTIC, + DEFAULT_ONRAMP_WIDGET_BASE_URL, ); return this.cached; } diff --git a/src/lib/implementations/utils/index.ts b/src/lib/implementations/utils/index.ts index 9b7bd60..0204cae 100644 --- a/src/lib/implementations/utils/index.ts +++ b/src/lib/implementations/utils/index.ts @@ -1,4 +1,5 @@ export { ConfigProvider } from "./ConfigProvider"; +export { CircleProvider } from "./CircleProvider"; export { OWSProvider } from "./OWSProvider"; export { SupportedChainsBlockchainProvider } from "./SupportedChainsBlockchainProvider"; export { EventBus } from "./EventBus"; diff --git a/src/lib/interfaces/utils/ICircleProvider.ts b/src/lib/interfaces/utils/ICircleProvider.ts new file mode 100644 index 0000000..a1b43cf --- /dev/null +++ b/src/lib/interfaces/utils/ICircleProvider.ts @@ -0,0 +1,13 @@ +import type { AppKit } from "@crcl-main/app-kit"; + +/** + * Utility-level Circle AppKit lifecycle. Lazily constructs a single AppKit and + * exposes its onramp handle for OnrampView. + */ +export interface ICircleProvider { + getOnramp(): Promise; + /** Relayer session mint URL (`POST /wallet/onramp`). */ + getSessionUrl(): Promise; +} + +export const ICircleProviderType = Symbol.for("ICircleProvider"); diff --git a/src/lib/interfaces/utils/index.ts b/src/lib/interfaces/utils/index.ts index 650f3ea..2f72d66 100644 --- a/src/lib/interfaces/utils/index.ts +++ b/src/lib/interfaces/utils/index.ts @@ -1,5 +1,7 @@ export type { IConfigProvider } from "./IConfigProvider"; export { IConfigProviderType } from "./IConfigProvider"; +export type { ICircleProvider } from "./ICircleProvider"; +export { ICircleProviderType } from "./ICircleProvider"; export type { IEventBus } from "./IEventBus"; export { IEventBusType } from "./IEventBus"; export type { IOWSProvider } from "./IOWSProvider"; diff --git a/src/lib/types/domain/WalletConfig.ts b/src/lib/types/domain/WalletConfig.ts index 3d3c379..8d6aca9 100644 --- a/src/lib/types/domain/WalletConfig.ts +++ b/src/lib/types/domain/WalletConfig.ts @@ -16,5 +16,9 @@ export class WalletConfig { public readonly assetActivityDefaultLimit: number, /** Max optimistic send rows retained in localStorage. */ public readonly assetActivityMaxOptimistic: number, + /** + * Circle onramp widget origin (must match Relayer `ONRAMP_WIDGET_BASE_URL`). + */ + public readonly onrampWidgetBaseUrl: string, ) {} } diff --git a/src/wallet/WalletProvider.tsx b/src/wallet/WalletProvider.tsx index d6c69bd..4ec0904 100644 --- a/src/wallet/WalletProvider.tsx +++ b/src/wallet/WalletProvider.tsx @@ -37,6 +37,7 @@ import { OneshotRelayerRepository } from "../lib/implementations/data/OneshotRel import { TransactionService } from "../lib/implementations/business"; import { ConfigProvider, + CircleProvider, OWSProvider, SupportedChainsBlockchainProvider, EventBus, @@ -52,6 +53,7 @@ import type { } from "../lib/interfaces/data"; import type { ITransactionService } from "../lib/interfaces/business"; import type { + ICircleProvider, IConfigProvider, IEventBus, IOWSProvider, @@ -74,8 +76,10 @@ import { useWalletAuth } from "./useWalletAuth"; import { useWalletAssets } from "./useWalletAssets"; import { useWalletBoot } from "./useWalletBoot"; import { useWalletSessionStore } from "./sessionStore"; +import { CircleContextProvider } from "../circle/CircleContext"; /** Filled once the Signing Layer iframe finishes loading / wallet handshake. */ const configProvider: IConfigProvider = new ConfigProvider(); +const circleProvider: ICircleProvider = new CircleProvider(configProvider); const owsProvider: IOWSProvider = new OWSProvider(); const chainRepository: IChainRepository = new HardcodedChainRepository(); const blockchainProvider: IBlockchainProvider = @@ -546,6 +550,8 @@ export function WalletProvider({ children }: { children: ReactNode }) { ); return ( - {children} + + {children} + ); } diff --git a/src/wallet/modalTypes.ts b/src/wallet/modalTypes.ts index 84fa8f4..6b71d8e 100644 --- a/src/wallet/modalTypes.ts +++ b/src/wallet/modalTypes.ts @@ -12,6 +12,7 @@ import type { EVMTransactionHash, } from "@1shotapi/ows-types"; import type { IAddAssetApprovalRequest } from "./registerAddAsset"; +import type { IOnrampOpenRequest } from "../circle/onrampTypes"; export type WalletSetupChoice = "login" | "create" | "import" | "cancel"; @@ -132,6 +133,13 @@ export type ModalRequest = createUrl: string; /** true when user confirms open; false when cancelled. */ resolve: (opened: boolean) => void; + } + | { + id: string; + kind: "onramp"; + request: IOnrampOpenRequest; + resolve: () => void; + reject: (error: unknown) => void; }; export type ActiveModal = ModalRequest; diff --git a/src/wallet/registerOnramp.ts b/src/wallet/registerOnramp.ts new file mode 100644 index 0000000..2b4fbb9 --- /dev/null +++ b/src/wallet/registerOnramp.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; +import type { OWSWallet } from "@1shotapi/ows-wallet-utils"; +import { + OwsUserRejectedError, + type EVMAccountAddress, +} from "@1shotapi/ows-types"; +import { openOnramp } from "../circle/openOnramp"; + +/** Custom RPC — host: `await proxy.rpc("onramp", { chainId?, amount? })`. */ +export const ONRAMP_RPC_METHOD = "onramp"; + +const onrampParamsSchema = z + .strictObject({ + chainId: z.number().int().positive().optional(), + amount: z.string().min(1).optional(), + }) + .default({}); + +export type IOnrampParams = z.infer; + +export type RegisterOnrampOptions = { + getOwnerAddress: () => EVMAccountAddress | null; +}; + +/** + * Register host `onramp` RPC — opens Circle onramp fullscreen for the + * unlocked EVM address. + */ +export function registerOnrampRpc( + wallet: OWSWallet, + options: RegisterOnrampOptions, +): void { + wallet.registerRpc( + ONRAMP_RPC_METHOD, + async (params) => { + const { chainId, amount } = params as IOnrampParams; + const owner = options.getOwnerAddress(); + if (!owner) { + throw new Error("Wallet is locked — unlock before onramp"); + } + + const display = await wallet.requestDisplay(); + try { + await openOnramp({ + destinationAddress: owner, + chainId, + amount, + }); + return { ok: true as const }; + } catch (error: unknown) { + if ( + error instanceof OwsUserRejectedError || + (error instanceof Error && /reject/i.test(error.message)) + ) { + throw error instanceof OwsUserRejectedError + ? error + : new OwsUserRejectedError("User closed onramp"); + } + throw error; + } finally { + await display.hide(); + } + }, + onrampParamsSchema, + ); +} diff --git a/src/wallet/useWalletBoot.ts b/src/wallet/useWalletBoot.ts index bf684e1..aa1df72 100644 --- a/src/wallet/useWalletBoot.ts +++ b/src/wallet/useWalletBoot.ts @@ -45,6 +45,7 @@ import { registerAddAssetRpc } from "./registerAddAsset"; import { registerCreateAccountRpc } from "./registerCreateAccount"; import type { IPasskeyRegistrationResult } from "./registerCreateAccount"; import { registerFocusModeRpc } from "./registerFocusMode"; +import { registerOnrampRpc } from "./registerOnramp"; import { loadCredentialId } from "../storage"; import { pushModal } from "./pushModal"; import type { ActiveModal, IRelayerConfirmSendResult } from "./modalTypes"; @@ -235,6 +236,16 @@ export function useWalletBoot({ registerFocusModeRpc(wallet, rpcHelper); + registerOnrampRpc(wallet, { + getOwnerAddress: () => { + const address = useWalletSessionStore.getState().evmAddress; + if (!address || String(address).toLowerCase() === "0x0") { + return null; + } + return address; + }, + }); + registerAddAssetRpc(wallet, { knownAssetRepository, trackedAssetRepository, From 71ac3fb631f2bec654e0a9271ca69deb6e98d006 Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Thu, 6 Aug 2026 17:04:26 -0700 Subject: [PATCH 02/11] Audit fix --- package-lock.json | 68 +++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53042f9..fbd3c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2613,12 +2613,12 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz", + "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" @@ -2900,12 +2900,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -7424,15 +7424,15 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -9144,9 +9144,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "funding": [ { "type": "github", @@ -9541,9 +9541,9 @@ } }, "node_modules/hono": { - "version": "4.12.30", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", - "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.0.tgz", + "integrity": "sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -9878,9 +9878,9 @@ } }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "license": "MIT", "engines": { "node": ">= 12" @@ -10240,9 +10240,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "funding": [ { "type": "github", @@ -10915,9 +10915,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "funding": [ { "type": "github", @@ -11627,9 +11627,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -11646,7 +11646,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -13293,9 +13293,9 @@ } }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "license": "MIT", "engines": { "node": ">=20.18.1" From a06fbdb68f16da3ea011bdeccb5500646b26e966 Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Mon, 10 Aug 2026 11:28:36 -0700 Subject: [PATCH 03/11] Update chain references from Arc Testnet to Arc mainnet - Changed chain ID references in the wallet integration and related components to reflect the transition from Arc Testnet (0x4cef52) to Arc mainnet (0x13b2). - Updated documentation and comments to accurately describe the current network status. - Adjusted asset tracking and default chain settings accordingly. --- .../skills/oneshot-embedded-wallet/SKILL.md | 4 ++-- host/src/components/hostChains.ts | 10 ++++++++-- host/src/hooks/useHostTestActions.ts | 2 +- host/src/styleForm.ts | 1 + skills/oneshot-embedded-wallet/SKILL.md | 4 ++-- .../data/HardcodedChainRepository.ts | 18 +++++++++++++++--- .../data/HardcodedKnownAssetRepository.ts | 1 + .../implementations/data/relayerKnownAssets.ts | 12 +++++++++++- 8 files changed, 41 insertions(+), 11 deletions(-) diff --git a/.agents/skills/oneshot-embedded-wallet/SKILL.md b/.agents/skills/oneshot-embedded-wallet/SKILL.md index 11d68e5..344a962 100644 --- a/.agents/skills/oneshot-embedded-wallet/SKILL.md +++ b/.agents/skills/oneshot-embedded-wallet/SKILL.md @@ -191,7 +191,7 @@ Host-controlled shell modes. Callers (not end users) switch between **General** ```typescript // Lock to one chain + ERC-20 (or other) asset await proxy.rpc("focusWallet", { - chainId: "0x4cef52", // Arc Testnet + chainId: "0x13b2", // Arc assetAddress: "0x3600000000000000000000000000000000000000", // USDC }); proxy.showWallet(); @@ -216,7 +216,7 @@ Propose a tracked **ERC-20** for the Balances tab. The wallet resolves the token ```typescript await proxy.rpc("addAsset", { - chainId: "0x4cef52", // Arc Testnet + chainId: "0x13b2", // Arc assetAddress: "0x3600000000000000000000000000000000000000", // USDC }); proxy.showWallet(); diff --git a/host/src/components/hostChains.ts b/host/src/components/hostChains.ts index 1aeec3b..0d393bf 100644 --- a/host/src/components/hostChains.ts +++ b/host/src/components/hostChains.ts @@ -1,4 +1,10 @@ export const HOST_CHAINS = [ + { + value: "0x13b2", + label: "Arc", + usdc: "0x3600000000000000000000000000000000000000", + blockExplorerUrl: "https://explorer.arc.io", + }, { value: "0x4cef52", label: "Arc Testnet", @@ -25,9 +31,9 @@ export const HOST_CHAINS = [ }, ] as const; -/** Focus demo: Arc Testnet USDC. */ +/** Focus demo: Arc mainnet USDC. */ export const FOCUS_USDC_ARC = { - chainId: "0x4cef52", + chainId: "0x13b2", assetAddress: "0x3600000000000000000000000000000000000000", label: "USDC (Arc)", } as const; diff --git a/host/src/hooks/useHostTestActions.ts b/host/src/hooks/useHostTestActions.ts index 1e13fae..6264579 100644 --- a/host/src/hooks/useHostTestActions.ts +++ b/host/src/hooks/useHostTestActions.ts @@ -547,7 +547,7 @@ export function useHostTestActions({ setChainId(FOCUS_USDC_ARC.chainId); proxy.showWallet(); setWalletVisible(true); - reportStatus("Wallet focused on USDC (Arc Testnet)."); + reportStatus("Wallet focused on USDC (Arc)."); } catch (error) { reportStatus( error instanceof Error ? error.message : "focusWallet failed", diff --git a/host/src/styleForm.ts b/host/src/styleForm.ts index 4dbdc39..87858da 100644 --- a/host/src/styleForm.ts +++ b/host/src/styleForm.ts @@ -186,6 +186,7 @@ export const CATALOG_CHAIN_OPTIONS: ReadonlyArray<{ chainId: string; label: string; }> = [ + { chainId: "0x13b2", label: "Arc" }, { chainId: "0x4cef52", label: "Arc Testnet" }, { chainId: "0xaa36a7", label: "Sepolia" }, { chainId: "0x14a34", label: "Base Sepolia" }, diff --git a/skills/oneshot-embedded-wallet/SKILL.md b/skills/oneshot-embedded-wallet/SKILL.md index a31ccb7..4207955 100644 --- a/skills/oneshot-embedded-wallet/SKILL.md +++ b/skills/oneshot-embedded-wallet/SKILL.md @@ -191,7 +191,7 @@ Host-controlled shell modes. Callers (not end users) switch between **General** ```typescript // Lock to one chain + ERC-20 (or other) asset await proxy.rpc("focusWallet", { - chainId: "0x4cef52", // Arc Testnet + chainId: "0x13b2", // Arc assetAddress: "0x3600000000000000000000000000000000000000", // USDC }); proxy.showWallet(); @@ -216,7 +216,7 @@ Propose a tracked **ERC-20** for the Balances tab. The wallet resolves the token ```typescript await proxy.rpc("addAsset", { - chainId: "0x4cef52", // Arc Testnet + chainId: "0x13b2", // Arc assetAddress: "0x3600000000000000000000000000000000000000", // USDC }); proxy.showWallet(); diff --git a/src/lib/implementations/data/HardcodedChainRepository.ts b/src/lib/implementations/data/HardcodedChainRepository.ts index 9dff1b2..606828e 100644 --- a/src/lib/implementations/data/HardcodedChainRepository.ts +++ b/src/lib/implementations/data/HardcodedChainRepository.ts @@ -27,10 +27,22 @@ const DEVELOPMENT_RELAYER_URL = "https://relayer.1shotapi.dev"; const ALCHEMY_KEY = "jqLUTbHeN_cVsIX2W7tJk"; /** - * Public Relayer docs networks + Arc Testnet. + * Public Relayer docs networks + Arc (mainnet / testnet). + * Arc has no MetaMask Delegation Framework yet — `useRelayer: false`. * @see https://1shotapi.com/docs/relayer/get-started/overview */ const CATALOG: readonly SupportedChain[] = [ + new SupportedChain( + EVMChainId("0x13b2"), + EChainNetworkType.Mainnet, + PRODUCTION_RELAYER_URL, + false, + arcLogo, + true, + `https://arc-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, + "Arc", + "https://explorer.arc.io", + ), new SupportedChain( EVMChainId("0x4cef52"), EChainNetworkType.Testnet, @@ -187,8 +199,8 @@ const CATALOG: readonly SupportedChain[] = [ ), ]; -/** Default chain for a fresh session (Arc Testnet). */ -export const DEFAULT_CHAIN_ID = EVMChainId("0x4cef52"); +/** Default chain for a fresh session (Arc mainnet). */ +export const DEFAULT_CHAIN_ID = EVMChainId("0x13b2"); export class HardcodedChainRepository implements IChainRepository { private allowedChains: Set | null = null; diff --git a/src/lib/implementations/data/HardcodedKnownAssetRepository.ts b/src/lib/implementations/data/HardcodedKnownAssetRepository.ts index 1be5f29..1b20111 100644 --- a/src/lib/implementations/data/HardcodedKnownAssetRepository.ts +++ b/src/lib/implementations/data/HardcodedKnownAssetRepository.ts @@ -19,6 +19,7 @@ const BY_KEY = new Map( ); const DEFAULT_TRACKED_USDC_CHAIN_IDS = new Set([ + "0x13b2", "0x4cef52", "0xaa36a7", "0x14a34", diff --git a/src/lib/implementations/data/relayerKnownAssets.ts b/src/lib/implementations/data/relayerKnownAssets.ts index 901d525..cb6ade7 100644 --- a/src/lib/implementations/data/relayerKnownAssets.ts +++ b/src/lib/implementations/data/relayerKnownAssets.ts @@ -33,10 +33,20 @@ function seed(row: ISeedRow): KnownAsset { } /** - * Static snapshot from `relayer_getCapabilities` (prod + dev) plus Arc Testnet USDC. + * Static snapshot from `relayer_getCapabilities` (prod + dev) plus Arc USDC. * @see https://www.1shotapi.com/docs/relayer/get-started/overview */ const SEED_ROWS: readonly ISeedRow[] = [ + // Arc mainnet — no MetaMask contracts / relayer yet. + { + chainId: EVMChainId("0x13b2"), + address: EVMAccountAddress( + "0x3600000000000000000000000000000000000000", + ), + symbol: "USDC", + name: "USDC", + decimals: 6, + }, // Arc Testnet — demo network, not returned by relayer. { chainId: EVMChainId("0x4cef52"), From a0a6b6f8965eb6c9b286f7ac76f2370e4e31d56b Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Mon, 24 Aug 2026 15:55:18 -0700 Subject: [PATCH 04/11] Update package-lock.json --- package-lock.json | 322 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/package-lock.json b/package-lock.json index c50476f..eb43397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1160,6 +1160,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@crcl-main/app-kit/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "node_modules/@crcl-main/app-kit/node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", @@ -1189,6 +1211,28 @@ "node": ">=20.0.0" } }, + "node_modules/@crcl-main/bridge-kit/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "node_modules/@crcl-main/bridge-kit/node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", @@ -1219,6 +1263,28 @@ "node": ">=20.0.0" } }, + "node_modules/@crcl-main/earn-kit/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "node_modules/@crcl-main/earn-kit/node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", @@ -1240,6 +1306,28 @@ "node": ">=20.0.0" } }, + "node_modules/@crcl-main/onramp-kit/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "node_modules/@crcl-main/onramp-kit/node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", @@ -1303,6 +1391,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@crcl-main/provider-cctp-v2/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "node_modules/@crcl-main/provider-cctp-v2/node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", @@ -1332,6 +1442,28 @@ "node": ">=20.0.0" } }, + "node_modules/@crcl-main/provider-earn-service/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "node_modules/@crcl-main/provider-earn-service/node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", @@ -7203,6 +7335,15 @@ "@types/har-format": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -7495,6 +7636,21 @@ "ws": "^7.5.1" } }, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { "version": "7.5.13", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", @@ -8128,6 +8284,12 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8166,6 +8328,12 @@ "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", "license": "MIT" }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -8217,6 +8385,35 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/borsh/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/borsh/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, "node_modules/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -8241,6 +8438,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/brotli": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", @@ -9180,6 +9383,18 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -10174,6 +10389,14 @@ "dev": true, "license": "MIT" }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10847,6 +11070,26 @@ "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", "license": "Apache-2.0" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -12319,12 +12562,44 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", @@ -13959,6 +14234,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -14526,6 +14821,12 @@ "node": ">= 14" } }, + "node_modules/superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==", + "license": "MIT" + }, "node_modules/systeminformation": { "version": "5.31.17", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.17.tgz", @@ -14619,6 +14920,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "node_modules/thread-stream": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", @@ -15460,6 +15766,12 @@ "dev": true, "license": "MIT" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -15467,6 +15779,16 @@ "dev": true, "license": "MIT" }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", From d247c57b563755071b004a5bc75b6babb313b090 Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Wed, 26 Aug 2026 22:28:19 -0700 Subject: [PATCH 05/11] Add CCTP bridge functionality for gasless USDC transfers - Introduced a new `bridge` RPC method to facilitate gasless CCTP USDC transfers between networks. - Updated UI components to include bridge actions and labels, enhancing user interaction. - Implemented a dedicated `CCTPBridge` modal for user input and transaction confirmation. - Enhanced `BridgeService` to handle quoting and executing bridge transactions, integrating with Circle's Forwarding Service. - Added necessary types and utility functions to support the new bridging feature across the application. --- .../skills/oneshot-embedded-wallet/SKILL.md | 24 +- AGENTS.md | 28 + host/src/components/WalletActions.tsx | 16 +- ...alletConfiguratorTextTabWalletSections.tsx | 42 ++ host/src/components/hostChains.ts | 1 + host/src/hooks/useHostTestActions.ts | 23 + host/src/styleForm.ts | 33 + skills/oneshot-embedded-wallet/SKILL.md | 24 +- src/circle/cctpBridgeTypes.ts | 16 + src/circle/openCctpBridge.ts | 18 + src/components/AssetDetails.tsx | 55 +- src/components/ModalHost.tsx | 9 + src/components/modals/CCTPBridge.tsx | 650 +++++++++++++++++ .../implementations/business/BridgeService.ts | 288 ++++++++ src/lib/implementations/business/index.ts | 2 + .../business/utils/CCTPUtils.ts | 293 ++++++++ .../business/utils/TransactionUtils.ts | 62 +- .../implementations/data/CircleRepository.ts | 250 +++++++ .../data/HardcodedChainRepository.ts | 52 +- .../data/HardcodedKnownAssetRepository.ts | 22 +- src/lib/implementations/data/index.ts | 7 + .../data/relayerKnownAssets.ts | 680 +++++++++--------- src/lib/interfaces/business/IBridgeService.ts | 77 ++ .../business/ITransactionService.ts | 2 +- src/lib/interfaces/business/index.ts | 18 + .../interfaces/business/utils/ICCTPUtils.ts | 104 +++ .../business/utils/ITransactionUtils.ts | 5 +- src/lib/interfaces/business/utils/index.ts | 9 + src/lib/interfaces/data/ICircleRepository.ts | 60 ++ .../interfaces/data/IKnownAssetRepository.ts | 3 + src/lib/interfaces/data/index.ts | 7 + src/lib/types/domain/KnownAsset.ts | 1 + src/lib/types/domain/SupportedChain.ts | 1 + src/lib/types/enum/ECctpTransferSpeed.ts | 5 + src/lib/types/enum/EChain.ts | 25 + src/lib/types/enum/ECircleDomainId.ts | 30 + src/lib/types/enum/index.ts | 4 + src/style/applyStyle.ts | 5 + src/style/defaults.ts | 42 ++ src/style/index.ts | 1 + src/style/registerConfigure.ts | 50 +- src/style/types.ts | 47 ++ src/wallet/WalletProvider.tsx | 44 ++ src/wallet/modalTypes.ts | 11 + src/wallet/registerBridge.ts | 145 ++++ src/wallet/useWalletBoot.ts | 18 + .../business/utils/CCTPUtils.test.ts | 223 ++++++ .../data/CircleRepository.test.ts | 40 ++ .../implementations/data/cctpPersist.test.ts | 43 ++ test/wallet/registerBridge.test.ts | 20 + 50 files changed, 3236 insertions(+), 399 deletions(-) create mode 100644 src/circle/cctpBridgeTypes.ts create mode 100644 src/circle/openCctpBridge.ts create mode 100644 src/components/modals/CCTPBridge.tsx create mode 100644 src/lib/implementations/business/BridgeService.ts create mode 100644 src/lib/implementations/business/utils/CCTPUtils.ts create mode 100644 src/lib/implementations/data/CircleRepository.ts create mode 100644 src/lib/interfaces/business/IBridgeService.ts create mode 100644 src/lib/interfaces/business/utils/ICCTPUtils.ts create mode 100644 src/lib/interfaces/data/ICircleRepository.ts create mode 100644 src/lib/types/enum/ECctpTransferSpeed.ts create mode 100644 src/lib/types/enum/EChain.ts create mode 100644 src/lib/types/enum/ECircleDomainId.ts create mode 100644 src/wallet/registerBridge.ts create mode 100644 test/lib/implementations/business/utils/CCTPUtils.test.ts create mode 100644 test/lib/implementations/data/CircleRepository.test.ts create mode 100644 test/lib/implementations/data/cctpPersist.test.ts create mode 100644 test/wallet/registerBridge.test.ts diff --git a/.agents/skills/oneshot-embedded-wallet/SKILL.md b/.agents/skills/oneshot-embedded-wallet/SKILL.md index 20d9e40..066fb67 100644 --- a/.agents/skills/oneshot-embedded-wallet/SKILL.md +++ b/.agents/skills/oneshot-embedded-wallet/SKILL.md @@ -3,7 +3,7 @@ name: oneshot-embedded-wallet description: >- Integrate the 1Shot embedded wallet (OWS Host Layer) with @1shotapi/ows-provider. Use when embedding wallet.1shotapi.com, wiring OWSProxy, EIP-1193, credentials, - or custom RPC such as configure / focusWallet / addAsset / createAccount / onramp for + or custom RPC such as configure / focusWallet / addAsset / createAccount / onramp / bridge for theming, host-driven focus mode, tracked assets, and first-party Safari create. license: MIT metadata: @@ -268,6 +268,25 @@ await proxy.rpc("onramp", { Returns `{ ok: true }` when the user closes the onramp view. The Relayer holds the Circle kit key; the browser only receives a single-use session. Inline iframe requires Circle CSP allowlisting of the wallet origin; for local/ngrok testing the branding layer honors `localStorage.setItem("circlePopup", "true")` and uses AppKit `openWindow` instead. +## Custom RPC — `bridge` + +Opens the gasless CCTP USDC bridge (native TokenMessengerV2 + Circle Forwarding Service, submitted through the 1Shot relayer). Locked wallet → error. Close before confirm → `OwsUserRejectedError`. Omit `sourceChainId` to use the session chain. + +```typescript +await proxy.rpc("bridge", { + amount: "10.50", // optional human USDC + sourceChainId: 8453, // optional decimal; omit → session chain + destinationChainId: 1, // optional; omit → user picks +}); +// or: await proxy.rpc("bridge", {}); +``` + +| Method | Params | Behavior | +|--------|--------|----------| +| `bridge` | `{ amount?: string, sourceChainId?: number, destinationChainId?: number }` | Shows wallet, opens CCTP bridge for native USDC on a relayer CCTP source. Dest must be a same-network CCTP chain. | + +Returns `{ ok: true, burnTxHash, forwardTxHash? }` when the source burn is submitted (and destination mint if Iris has completed). The user pays the relayer USDC fee (same path as Send); destination mint is Circle’s Forwarding Service — no dest-chain signature and no native gas. + ## Other Host APIs | API | Use | @@ -276,7 +295,7 @@ Returns `{ ok: true }` when the user closes the onramp view. The Relayer holds t | `proxy.ethereum.on` / `removeListener` | Branding→Host EIP-1193 notifications (`chainChanged`, `accountsChanged` via `ows:eip1193`) | | `proxy.credentials.*` | OID4 offer / present (when enabled in wallet) | | `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call | -| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, …) | +| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, `bridge`, …) | | `proxy.analytics.on(listener)` / `.on(name, listener)` / `.off(listener)` | Branding→Host product analytics (`ows:analytics`) | | `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call | | `proxy.rpc(method, params)` | Custom Branding RPC (`configure`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, …) | @@ -329,3 +348,4 @@ include a live Analytics panel fed by `proxy.analytics.on` (filter by `name`). - Use `focusWallet` / `unfocusWallet` for host-driven single-asset flows; do not expose mode switching in the wallet UI. - Use `addAsset` when the host wants a lasting Balances entry; expect a confirm modal (contrast with `focusWallet`). - Use `onramp` (or the in-wallet Buy button) for fiat → crypto; do not put the Circle kit key in the Host or Branding Layer. +- Use `bridge` (or the in-wallet Bridge button) for gasless CCTP USDC; do not require native gas or dest-chain `receiveMessage`. diff --git a/AGENTS.md b/AGENTS.md index cce32ca..33d9b4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,8 @@ Test Host Layer: `host/` (`npm run dev:host`). Browser extension: `extension/` ( Fiat onramp: Asset Details **Buy** and host RPC `onramp({ chainId?, amount? })` open `OnrampView` (Circle AppKit). Sessions come from Relayer `POST /wallet/onramp` — never put the Circle kit key in this SPA. Default UI is `mountIframe`; for local/ngrok before Circle CSP allowlisting, set `localStorage.circlePopup = "true"` to use `openWindow` (prefetch session, then a sync click). Cloudsmith: set `CLOUDSMITH_TOKEN` before `npm install` (see README). +CCTP bridge: Asset Details **Bridge** (native USDC with `useCCTPBridge`) and host RPC `bridge({ amount?, sourceChainId?, destinationChainId? })` open `CCTPBridge`. Source omit → session chain. Burns via `TokenMessengerV2.depositForBurnWithHook` + `cctp-forward` hook through the EIP-7710 relayer (same USDC fee as Send). Destination mint is Circle’s Forwarding Service — no dest-chain signature, no native gas, no BridgeKit. + ### Form validation UX Primary submit actions (e.g. Send in `TransferTokensModal`) stay **disabled until every required field is valid**. Do not leave the button enabled and only reject on click. Empty fields show no error text; invalid non-empty input shows inline errors; the CTA enables only when the whole form is ready. @@ -50,6 +52,32 @@ When adding or changing UI strings or host-tunable options: - **Data:** `IKnownAssetRepository`, `ITrackedAssetRepository`, `IOneshotRelayerRepository` (`src/lib`) and their implementations - **Business:** services that orchestrate domain logic (add as needed) +### Injectable classes (constructor DI) + +Prefer **direct constructor parameter properties** for injectable services/utils — not an `XXXOptions` bag. Call sites pass dependencies positionally; implementations use `this.chainRepository` (etc.), never `this.options.*`. + +```ts +// Prefer +export class BridgeService implements IBridgeService { + constructor( + protected readonly chainRepository: IChainRepository, + protected readonly knownAssetRepository: IKnownAssetRepository, + protected readonly circleRepository: ICircleRepository, + protected readonly transactionUtils: ITransactionUtils, + protected readonly cctpUtils: ICCTPUtils, + protected readonly blockchain: IBlockchainProvider, + ) {} +} + +// Avoid +export type BridgeServiceOptions = { chainRepository: IChainRepository; /* … */ }; +export class BridgeService { + constructor(private readonly options: BridgeServiceOptions) {} +} +``` + +Wire at the composition root (e.g. `WalletProvider`) with positional args: `new BridgeService(chainRepository, knownAssetRepository, …)`. + `IOneshotRelayerRepository.sendTransaction` owns prepare + passkey sign + broadcast (interim: `eth_sendRawTransaction`). Host EIP-1193 sends go SignHelper → branding `approveAndSignTransaction` (ConfirmTransfer / SendTransaction consent) → relayer. In-wallet Send uses `TransferTokensModal` → `WalletProvider.sendTransaction` → relayer, then `SentTransactionModal` (hash + explorer link). Host-driven sends do not show that confirmation — the host surfaces the hash itself. ## Branded types diff --git a/host/src/components/WalletActions.tsx b/host/src/components/WalletActions.tsx index 5fb6cea..35e651c 100644 --- a/host/src/components/WalletActions.tsx +++ b/host/src/components/WalletActions.tsx @@ -58,6 +58,7 @@ export interface IWalletActionsProps { onAddUsdcArc: () => void; onAddUsdtBase: () => void; onOnramp: () => void; + onBridge: () => void; /** In-memory grants from this session (`wallet_requestExecutionPermissions`). */ sessionGrants: ReadonlyArray<{ id: string; @@ -108,6 +109,7 @@ export function WalletActions({ onAddUsdcArc, onAddUsdtBase, onOnramp, + onBridge, sessionGrants, delegationsOutput, onRequestDelegation, @@ -408,10 +410,10 @@ export function WalletActions({
- +

- Open Circle fiat onramp via onramp (Buy crypto into the - unlocked wallet address). + Open Circle fiat onramp via onramp, or gasless CCTP USDC + bridge via bridge.

+
diff --git a/host/src/components/WalletConfiguratorTextTabWalletSections.tsx b/host/src/components/WalletConfiguratorTextTabWalletSections.tsx index dd4854c..f6ccaef 100644 --- a/host/src/components/WalletConfiguratorTextTabWalletSections.tsx +++ b/host/src/components/WalletConfiguratorTextTabWalletSections.tsx @@ -87,6 +87,48 @@ export function WalletConfiguratorTextTabWalletSections({ value={form.sendLabel} onChange={(value) => patch("sendLabel", value)} /> + patch("bridgeLabel", value)} + /> + patch("cctpBridgeTitle", value)} + /> + patch("cctpBridgeBody", value)} + /> + patch("cctpBridgeGetQuote", value)} + /> + patch("cctpBridgeConfirm", value)} + /> + patch("cctpBridgeCancel", value)} + /> + patch("cctpBridgeSentTitle", value)} + /> { + const proxy = proxyRef.current; + if (!proxy) return; + setBusy(true); + reportStatus("Opening CCTP bridge…"); + void (async () => { + try { + await proxy.rpc("bridge", {}); + proxy.showWallet(); + setWalletVisible(true); + reportStatus("Bridge closed."); + } catch (error) { + reportStatus( + error instanceof Error ? error.message : "bridge failed", + true, + ); + } finally { + setBusy(false); + } + })(); + }; + const handleRequestDelegation = () => { const proxy = proxyRef.current; if (!proxy) return; @@ -865,6 +887,7 @@ export function useHostTestActions({ onAddUsdcArc: handleAddUsdcArc, onAddUsdtBase: handleAddUsdtBase, onOnramp: handleOnramp, + onBridge: handleBridge, sessionGrants: sessionGrants.map((g) => ({ id: g.id, summary: grantSummary(g.response), diff --git a/host/src/styleForm.ts b/host/src/styleForm.ts index 201d2fa..6a9d483 100644 --- a/host/src/styleForm.ts +++ b/host/src/styleForm.ts @@ -143,6 +143,13 @@ export interface IStyleFormState { receiveCopyFailedLabel: string; receiveCloseLabel: string; sendLabel: string; + bridgeLabel: string; + cctpBridgeTitle: string; + cctpBridgeBody: string; + cctpBridgeGetQuote: string; + cctpBridgeConfirm: string; + cctpBridgeCancel: string; + cctpBridgeSentTitle: string; // Text — Confirm transfer (host ERC-20) confirmTransferTitle: string; @@ -327,6 +334,14 @@ export const ACME_PRESET: IStyleFormState = { receiveCopyFailedLabel: "Copy failed", receiveCloseLabel: "Close", sendLabel: "Send", + bridgeLabel: "Bridge", + cctpBridgeTitle: "Bridge USDC", + cctpBridgeBody: + "Send USDC to another network. Circle mints on the destination — you never pay native gas.", + cctpBridgeGetQuote: "Get quote", + cctpBridgeConfirm: "Confirm bridge", + cctpBridgeCancel: "Cancel", + cctpBridgeSentTitle: "Bridge complete", confirmTransferTitle: "Confirm Transfer", confirmTransferBody: "{domain} is requesting to send tokens from your wallet. Review the amount and recipient before confirming.", @@ -481,6 +496,14 @@ export const DEFAULTS_PRESET: IStyleFormState = { receiveCopyFailedLabel: "Copy failed", receiveCloseLabel: "Close", sendLabel: "Send", + bridgeLabel: "Bridge", + cctpBridgeTitle: "Bridge USDC", + cctpBridgeBody: + "Send USDC to another network. Circle mints on the destination — you never pay native gas.", + cctpBridgeGetQuote: "Get quote", + cctpBridgeConfirm: "Confirm bridge", + cctpBridgeCancel: "Cancel", + cctpBridgeSentTitle: "Bridge complete", confirmTransferTitle: "Confirm transfer", confirmTransferBody: "{domain} is requesting to send tokens from your wallet. Review the amount and recipient before confirming.", @@ -753,8 +776,18 @@ export function buildConfigurePayload( put(balances, "receiveCopyFailedLabel", form.receiveCopyFailedLabel); put(balances, "receiveCloseLabel", form.receiveCloseLabel); put(balances, "sendLabel", form.sendLabel); + put(balances, "bridgeLabel", form.bridgeLabel); if (Object.keys(balances).length > 0) copy.balances = balances; + const cctpBridge: Record = {}; + put(cctpBridge, "title", form.cctpBridgeTitle); + put(cctpBridge, "body", form.cctpBridgeBody); + put(cctpBridge, "getQuoteLabel", form.cctpBridgeGetQuote); + put(cctpBridge, "confirmLabel", form.cctpBridgeConfirm); + put(cctpBridge, "cancelLabel", form.cctpBridgeCancel); + put(cctpBridge, "successTitle", form.cctpBridgeSentTitle); + if (Object.keys(cctpBridge).length > 0) copy.cctpBridge = cctpBridge; + const exportPrivateKey: Record = {}; put(exportPrivateKey, "title", form.exportPrivateKeyTitle); put(exportPrivateKey, "body", form.exportPrivateKeyBody); diff --git a/skills/oneshot-embedded-wallet/SKILL.md b/skills/oneshot-embedded-wallet/SKILL.md index 2333e44..687a70b 100644 --- a/skills/oneshot-embedded-wallet/SKILL.md +++ b/skills/oneshot-embedded-wallet/SKILL.md @@ -3,7 +3,7 @@ name: oneshot-embedded-wallet description: >- Integrate the 1Shot embedded wallet (OWS Host Layer) with @1shotapi/ows-provider. Use when embedding wallet.1shotapi.com, wiring OWSProxy, EIP-1193, credentials, - or custom RPC such as configure / focusWallet / addAsset / createAccount / onramp for + or custom RPC such as configure / focusWallet / addAsset / createAccount / onramp / bridge for theming, host-driven focus mode, tracked assets, and first-party Safari create. license: MIT metadata: @@ -268,6 +268,25 @@ await proxy.rpc("onramp", { Returns `{ ok: true }` when the user closes the onramp view. The Relayer holds the Circle kit key; the browser only receives a single-use session. Inline iframe requires Circle CSP allowlisting of the wallet origin; for local/ngrok testing the branding layer honors `localStorage.setItem("circlePopup", "true")` and uses AppKit `openWindow` instead. +## Custom RPC — `bridge` + +Opens the gasless CCTP USDC bridge (native TokenMessengerV2 + Circle Forwarding Service, submitted through the 1Shot relayer). Locked wallet → error. Close before confirm → `OwsUserRejectedError`. Omit `sourceChainId` to use the session chain. + +```typescript +await proxy.rpc("bridge", { + amount: "10.50", // optional human USDC + sourceChainId: 8453, // optional decimal; omit → session chain + destinationChainId: 1, // optional; omit → user picks +}); +// or: await proxy.rpc("bridge", {}); +``` + +| Method | Params | Behavior | +|--------|--------|----------| +| `bridge` | `{ amount?: string, sourceChainId?: number, destinationChainId?: number }` | Shows wallet, opens CCTP bridge for native USDC on a relayer CCTP source. Dest must be a same-network CCTP chain. | + +Returns `{ ok: true, burnTxHash, forwardTxHash? }` when the source burn is submitted (and destination mint if Iris has completed). The user pays the relayer USDC fee (same path as Send); destination mint is Circle’s Forwarding Service — no dest-chain signature and no native gas. + ## Other Host APIs | API | Use | @@ -276,7 +295,7 @@ Returns `{ ok: true }` when the user closes the onramp view. The Relayer holds t | `proxy.credentials.*` | OID4 offer / present (when enabled in wallet) | | `proxy.analytics.on(listener)` / `.on(name, listener)` / `.off(listener)` | Branding→Host product analytics (`ows:analytics`) | | `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call | -| `proxy.rpc(method, params)` | Custom Branding RPC (`configure`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, …) | +| `proxy.rpc(method, params)` | Custom Branding RPC (`configure`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, `bridge`, …) | ## Analytics (`proxy.analytics`) @@ -315,3 +334,4 @@ include a live Analytics panel fed by `proxy.analytics.on` (filter by `name`). - Use `focusWallet` / `unfocusWallet` for host-driven single-asset flows; do not expose mode switching in the wallet UI. - Use `addAsset` when the host wants a lasting Balances entry; expect a confirm modal (contrast with `focusWallet`). - Use `onramp` (or the in-wallet Buy button) for fiat → crypto; do not put the Circle kit key in the Host or Branding Layer. +- Use `bridge` (or the in-wallet Bridge button) for gasless CCTP USDC; do not require native gas or dest-chain `receiveMessage`. diff --git a/src/circle/cctpBridgeTypes.ts b/src/circle/cctpBridgeTypes.ts new file mode 100644 index 0000000..68d6e10 --- /dev/null +++ b/src/circle/cctpBridgeTypes.ts @@ -0,0 +1,16 @@ +import type { EVMAccountAddress, EVMChainId } from "@1shotapi/ows-types"; +import type { ICctpInFlightBurn } from "../lib/interfaces/data/ICircleRepository"; +import type { ICctpBridgeResult } from "../lib/interfaces/business/IBridgeService"; + +/** Params for opening the shared CCTP bridge modal (in-wallet + host `bridge`). */ +export type ICctpBridgeOpenRequest = { + sourceChainId: EVMChainId; + ownerAddress: EVMAccountAddress; + balance?: bigint | null; + amountAtoms?: bigint; + destinationChainId?: EVMChainId; + /** When set, skip the form and resume Iris polling. */ + resume?: ICctpInFlightBurn; +}; + +export type ICctpBridgeModalResult = ICctpBridgeResult; diff --git a/src/circle/openCctpBridge.ts b/src/circle/openCctpBridge.ts new file mode 100644 index 0000000..2f8002b --- /dev/null +++ b/src/circle/openCctpBridge.ts @@ -0,0 +1,18 @@ +import { pushModal } from "../wallet/pushModal"; +import type { + ICctpBridgeModalResult, + ICctpBridgeOpenRequest, +} from "./cctpBridgeTypes"; + +/** Open the shared CCTP USDC bridge (Asset Details + host `bridge` RPC). */ +export function openCctpBridge( + request: ICctpBridgeOpenRequest, +): Promise { + return pushModal(({ id, resolve, reject }) => ({ + id, + kind: "cctpBridge", + request, + resolve, + reject, + })); +} diff --git a/src/components/AssetDetails.tsx b/src/components/AssetDetails.tsx index a9136f9..4e9ff56 100644 --- a/src/components/AssetDetails.tsx +++ b/src/components/AssetDetails.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import type { ReactNode } from "react"; import { useShallow } from "zustand/react/shallow"; import { + ArrowLeftRightIcon, PlusIcon, QrCodeIcon, SendIcon, @@ -14,6 +15,7 @@ import { resolveActiveAddress } from "../wallet/activeAddress"; import { useLiveTrackedBalance } from "../wallet/useLiveTrackedBalance"; import { useWalletSessionStore } from "../wallet/sessionStore"; import { openOnramp } from "../circle/openOnramp"; +import { openCctpBridge } from "../circle/openCctpBridge"; import { AssetIdentityMark } from "./AssetIdentityMark"; import { BalanceDisplay } from "./BalanceDisplay"; import { TransactionHistory } from "./TransactionHistory"; @@ -31,7 +33,7 @@ export interface IAssetDetailsProps { export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { const { style } = useStyle(); const { balances: copy } = style.copy; - const { requestBalanceRefresh, resolveChain } = useWallet(); + const { requestBalanceRefresh, resolveChain, getKnownAsset } = useWallet(); const { evmAddress, solanaAddress } = useWalletSessionStore( useShallow((state) => ({ evmAddress: state.evmAddress, @@ -41,6 +43,8 @@ export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { const [receiveOpen, setReceiveOpen] = useState(false); const [sendOpen, setSendOpen] = useState(false); const [buyBusy, setBuyBusy] = useState(false); + const [bridgeBusy, setBridgeBusy] = useState(false); + const [canBridge, setCanBridge] = useState(false); const { balance, decimals } = useLiveTrackedBalance( assetProp.id, @@ -65,6 +69,18 @@ export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { void requestBalanceRefresh(assetProp.id); }, [assetProp.id, requestBalanceRefresh]); + useEffect(() => { + let cancelled = false; + void getKnownAsset(assetProp.chainId, assetProp.address).then((known) => { + if (!cancelled) { + setCanBridge(known?.useCCTPBridge === true); + } + }); + return () => { + cancelled = true; + }; + }, [assetProp.address, assetProp.chainId, getKnownAsset]); + const chain = resolveChain(asset.chainId); const network = chain?.label ?? String(asset.chainId); const active = resolveActiveAddress({ @@ -97,6 +113,31 @@ export function AssetDetails({ asset: assetProp }: IAssetDetailsProps) { }); }, [asset.chainId, asset.symbol, buyBusy, evmAddress]); + const openBridge = useCallback(() => { + if (!evmAddress || bridgeBusy || !canBridge) return; + setBridgeBusy(true); + void openCctpBridge({ + sourceChainId: asset.chainId, + ownerAddress: evmAddress, + balance, + }) + .catch(() => { + /* user closed or rejected */ + }) + .finally(() => { + setBridgeBusy(false); + void requestBalanceRefresh(asset.id); + }); + }, [ + asset.chainId, + asset.id, + balance, + bridgeBusy, + canBridge, + evmAddress, + requestBalanceRefresh, + ]); + return (
diff --git a/src/components/ModalHost.tsx b/src/components/ModalHost.tsx index c19fdaf..ea25f74 100644 --- a/src/components/ModalHost.tsx +++ b/src/components/ModalHost.tsx @@ -21,6 +21,7 @@ import { AdvancedOptionsModal } from "./modals/AdvancedOptionsModal"; import { AddAssetModal } from "./modals/AddAssetModal"; import { OpenCreateTabModal } from "./modals/OpenCreateTabModal"; import { OnrampView } from "./OnrampView"; +import { CCTPBridge } from "./modals/CCTPBridge"; import { GrantExecutionPermissionModal } from "./modals/GrantExecutionPermissionModal"; import { CancelDelegationModal } from "./modals/CancelDelegationModal"; @@ -155,6 +156,14 @@ export function ModalHost() { onClose={() => activeModal.resolve()} /> ); + case "cctpBridge": + return ( + + ); default: return null; } diff --git a/src/components/modals/CCTPBridge.tsx b/src/components/modals/CCTPBridge.tsx new file mode 100644 index 0000000..1db80d6 --- /dev/null +++ b/src/components/modals/CCTPBridge.tsx @@ -0,0 +1,650 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { formatUnits, parseUnits, erc20Abi } from "viem"; +import { + EVMChainId, + OwsUserRejectedError, + type EVMTransactionHash, +} from "@1shotapi/ows-types"; +import type { ICctpBridgeOpenRequest } from "../../circle/cctpBridgeTypes"; +import type { + ICctpBridgeQuote, + ICctpBridgeResult, +} from "../../lib/interfaces/business/IBridgeService"; +import type { IPaymentQuote } from "../../lib/interfaces/business"; +import type { ICctpInFlightBurn } from "../../lib/interfaces/data/ICircleRepository"; +import type { KnownAsset } from "../../lib/types/domain/KnownAsset"; +import type { SupportedChain } from "../../lib/types/domain/SupportedChain"; +import { ECctpTransferSpeed } from "../../lib/types/enum/ECctpTransferSpeed"; +import { makeTrackedAssetId } from "../../lib/types/primitives"; +import { useStyle } from "../../style/StyleProvider"; +import { useWallet } from "../../wallet/WalletProvider"; +import { Modal } from "../Modal"; +import { PaymentFeePicker } from "../PaymentFeePicker"; +import { TokenAmountInput } from "../TokenAmountInput"; +import { CopyableText } from "../CopyableText"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; + +export interface ICCTPBridgeProps { + request: ICctpBridgeOpenRequest; + onResolve: (result: ICctpBridgeResult) => void; + onReject: (error: unknown) => void; +} + +type BridgePhase = + | "form" + | "quoting" + | "quoted" + | "submitting" + | "polling" + | "success" + | "timeout"; + +function amountValidationError( + raw: string, + decimals: number, + copy: { invalidAmountError: string }, +): string | null { + const trimmed = raw.trim(); + if (!trimmed) { + return null; + } + try { + const parsed = parseUnits(trimmed, decimals); + if (parsed <= 0n) { + return copy.invalidAmountError; + } + } catch { + return copy.invalidAmountError; + } + return null; +} + +/** + * Gasless CCTP USDC bridge. Quote → relayer approve/burn → Iris dest mint. + */ +export function CCTPBridge({ + request, + onResolve, + onReject, +}: ICCTPBridgeProps) { + const { style } = useStyle(); + const copy = style.copy.cctpBridge; + const { + bridgeService, + knownAssetRepository, + blockchainProvider, + resolveChain, + requestBalanceRefresh, + switchChain, + } = useWallet(); + + const [sourceUsdc, setSourceUsdc] = useState(null); + const [destinations, setDestinations] = useState([]); + const [amount, setAmount] = useState(() => + request.amountAtoms !== undefined && request.amountAtoms > 0n + ? formatUnits(request.amountAtoms, 6) + : "", + ); + const [destChainId, setDestChainId] = useState( + request.destinationChainId ? String(request.destinationChainId) : "", + ); + const [speed, setSpeed] = useState( + ECctpTransferSpeed.Fast, + ); + const [balance, setBalance] = useState( + request.balance ?? null, + ); + const [irisQuote, setIrisQuote] = useState(null); + const [paymentQuote, setPaymentQuote] = useState(null); + const [paymentError, setPaymentError] = useState(null); + const [phase, setPhase] = useState( + request.resume ? "polling" : "form", + ); + const [error, setError] = useState(null); + const [burnTxHash, setBurnTxHash] = useState( + request.resume?.burnTxHash ?? null, + ); + const [forwardTxHash, setForwardTxHash] = useState( + null, + ); + const [inFlight, setInFlight] = useState( + request.resume ?? null, + ); + + const sourceChain = resolveChain(request.sourceChainId); + const destLocked = Boolean(request.destinationChainId); + const decimals = sourceUsdc?.decimals ?? 6; + const amountError = useMemo( + () => amountValidationError(amount, decimals, copy), + [amount, decimals, copy], + ); + + useEffect(() => { + let cancelled = false; + void (async () => { + const [asset, dests] = await Promise.all([ + knownAssetRepository.getCctpBridgeAsset(request.sourceChainId), + bridgeService.listDestinations(request.sourceChainId), + ]); + if (cancelled) return; + setSourceUsdc(asset); + setDestinations(dests); + if (asset && request.balance === undefined) { + try { + const client = blockchainProvider.getPublicClient( + request.sourceChainId, + ); + const live = await client.readContract({ + address: asset.address, + abi: erc20Abi, + functionName: "balanceOf", + args: [request.ownerAddress], + }); + if (!cancelled) setBalance(live); + } catch { + if (!cancelled) setBalance(null); + } + } + })(); + return () => { + cancelled = true; + }; + }, [ + blockchainProvider, + bridgeService, + knownAssetRepository, + request.balance, + request.ownerAddress, + request.sourceChainId, + ]); + + useEffect(() => { + if (!request.resume) return; + let cancelled = false; + setPhase("polling"); + void bridgeService + .pollUntilForwarded(request.resume, (progress) => { + if (cancelled) return; + setBurnTxHash(progress.burnTxHash); + if (progress.forwardTxHash) { + setForwardTxHash(progress.forwardTxHash); + } + }) + .then((hash) => { + if (cancelled) return; + setForwardTxHash(hash); + setPhase("success"); + void requestBalanceRefresh(); + }) + .catch((err: unknown) => { + if (cancelled) return; + setError(err instanceof Error ? err.message : copy.timeoutError); + setPhase("timeout"); + }); + return () => { + cancelled = true; + }; + }, [bridgeService, copy.timeoutError, request, requestBalanceRefresh]); + + const onPaymentQuoteChange = useCallback( + (next: IPaymentQuote | null, quoteError: string | null) => { + setPaymentQuote(next); + setPaymentError(quoteError); + }, + [], + ); + + const requiredUsdc = useMemo(() => { + if (!irisQuote || !paymentQuote || !sourceUsdc) return null; + const same = + String(paymentQuote.selectedToken).toLowerCase() === + String(sourceUsdc.address).toLowerCase(); + return same + ? irisQuote.totalBurn + paymentQuote.feeAtoms + : irisQuote.totalBurn; + }, [irisQuote, paymentQuote, sourceUsdc]); + + const insufficient = + requiredUsdc !== null && + balance !== null && + balance < requiredUsdc; + + const canQuote = + Boolean(amount.trim()) && + !amountError && + Boolean(destChainId) && + phase !== "quoting" && + phase !== "submitting" && + phase !== "polling"; + + const canConfirm = + phase === "quoted" && + irisQuote !== null && + paymentQuote !== null && + !paymentError && + !insufficient; + + function clearQuote(): void { + setIrisQuote(null); + if (phase === "quoted" || phase === "quoting") { + setPhase("form"); + } + } + + async function handleGetQuote(): Promise { + if (!canQuote || !destChainId) return; + setError(null); + setPhase("quoting"); + try { + const parsed = parseUnits(amount.trim(), decimals); + const next = await bridgeService.quote({ + sourceChainId: request.sourceChainId, + destChainId: EVMChainId(destChainId as `0x${string}`), + amountAtoms: parsed, + speed, + owner: request.ownerAddress, + }); + setIrisQuote(next); + setPhase("quoted"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : copy.quoteFailedError); + setPhase("form"); + } + } + + async function pollInFlight(record: ICctpInFlightBurn): Promise { + setPhase("polling"); + setError(null); + try { + const hash = await bridgeService.pollUntilForwarded(record, (progress) => { + setBurnTxHash(progress.burnTxHash); + if (progress.forwardTxHash) { + setForwardTxHash(progress.forwardTxHash); + } + }); + setForwardTxHash(hash); + setPhase("success"); + if (sourceUsdc) { + void requestBalanceRefresh( + makeTrackedAssetId(request.sourceChainId, sourceUsdc.address), + ); + } else { + void requestBalanceRefresh(); + } + } catch (err: unknown) { + setError(err instanceof Error ? err.message : copy.timeoutError); + setPhase("timeout"); + } + } + + async function handleConfirm(): Promise { + if (!canConfirm || !irisQuote || !paymentQuote) return; + setError(null); + setPhase("submitting"); + try { + await switchChain(request.sourceChainId); + const result = await bridgeService.execute( + irisQuote, + { + paymentToken: paymentQuote.selectedToken, + feeAtoms: paymentQuote.feeAtoms, + }, + (progress) => { + setBurnTxHash(progress.burnTxHash); + setPhase("polling"); + if (progress.forwardTxHash) { + setForwardTxHash(progress.forwardTxHash); + } + }, + ); + setBurnTxHash(result.burnTxHash); + if (result.forwardTxHash) { + setForwardTxHash(result.forwardTxHash); + setPhase("success"); + if (sourceUsdc) { + void requestBalanceRefresh( + makeTrackedAssetId(request.sourceChainId, sourceUsdc.address), + ); + } else { + void requestBalanceRefresh(); + } + return; + } + const stored = await bridgeService.resume(request.ownerAddress); + setInFlight(stored); + if (stored) { + await pollInFlight(stored); + } else { + setPhase("timeout"); + setError(copy.timeoutError); + } + } catch (err: unknown) { + const stored = await bridgeService.resume(request.ownerAddress); + if (stored) { + setInFlight(stored); + setBurnTxHash(stored.burnTxHash); + setError(err instanceof Error ? err.message : copy.timeoutError); + setPhase("timeout"); + return; + } + setError(err instanceof Error ? err.message : copy.submitFailedError); + setPhase("quoted"); + } + } + + function handleCancel(): void { + if (burnTxHash) { + onResolve({ + burnTxHash, + ...(forwardTxHash ? { forwardTxHash } : {}), + }); + return; + } + onReject(new OwsUserRejectedError("User closed bridge")); + } + + function handleDone(): void { + if (!burnTxHash) { + onReject(new OwsUserRejectedError("User closed bridge")); + return; + } + onResolve({ + burnTxHash, + ...(forwardTxHash ? { forwardTxHash } : {}), + }); + } + + const busy = + phase === "quoting" || + phase === "submitting" || + phase === "polling"; + const destChain = destChainId + ? destinations.find( + (chain) => + String(chain.chainId).toLowerCase() === destChainId.toLowerCase(), + ) + : undefined; + + if (phase === "success" && burnTxHash) { + const destExplorer = destChain && forwardTxHash + ? destChain.txExplorerUrl(forwardTxHash) + : undefined; + const sourceExplorer = sourceChain?.txExplorerUrl(burnTxHash); + return ( + +

{copy.successBody}

+
+ + {forwardTxHash ? ( + + ) : null} +
+
+ ); + } + + const primaryAction = + phase === "timeout" + ? { + label: copy.retryLabel, + variant: "primary" as const, + disabled: !inFlight && !request.resume, + onClick: () => { + const record = inFlight ?? request.resume; + if (record) void pollInFlight(record); + }, + } + : phase === "quoted" + ? { + label: copy.confirmLabel, + variant: "primary" as const, + autoFocus: true, + disabled: !canConfirm, + onClick: () => void handleConfirm(), + } + : { + label: phase === "quoting" ? copy.quotingLabel : copy.getQuoteLabel, + variant: "primary" as const, + autoFocus: true, + disabled: !canQuote || busy, + onClick: () => void handleGetQuote(), + }; + + return ( + +

{copy.body}

+
+ { + setAmount(next); + setError(null); + clearQuote(); + }} + error={amountError} + disabled={busy || Boolean(request.resume)} + /> +
+ + {copy.destinationLabel} + + +
+
+ + {copy.speedLabel} + + +
+
+ + {copy.recipientLabel} + + +

{copy.recipientHint}

+
+ {irisQuote && + (phase === "quoted" || + phase === "submitting" || + phase === "polling") ? ( + + ) : null} + {irisQuote && request.ownerAddress ? ( + + ) : null} + {phase === "submitting" ? ( +

{copy.submittingLabel}

+ ) : null} + {phase === "polling" ? ( +

{copy.pollingLabel}

+ ) : null} + {burnTxHash && (phase === "polling" || phase === "timeout") ? ( + + ) : null} + {insufficient ? ( +

+ {copy.insufficientBalanceError} +

+ ) : null} + {error ? ( +

+ {error} +

+ ) : null} +
+
+ ); +} + +function QuoteBreakdown({ + quote, + payment, + copy, +}: { + quote: ICctpBridgeQuote; + payment: IPaymentQuote | null; + copy: { + transferAmountLabel: string; + cctpFeeLabel: string; + relayerFeeLabel: string; + netReceivedLabel: string; + }; +}) { + const decimals = quote.sourceUsdc.decimals; + return ( +
+
+
{copy.transferAmountLabel}
+
{formatUnits(quote.amountAtoms, decimals)} USDC
+
+
+
{copy.cctpFeeLabel}
+
{formatUnits(quote.maxFee, decimals)} USDC
+
+ {payment ? ( +
+
{copy.relayerFeeLabel}
+
+ {payment.feeFormatted}{" "} + {payment.tokens.find( + (token) => + String(token.address).toLowerCase() === + String(payment.selectedToken).toLowerCase(), + )?.symbol ?? "USDC"} +
+
+ ) : null} +
+
{copy.netReceivedLabel}
+
{formatUnits(quote.netReceivedAtoms, decimals)} USDC
+
+
+ ); +} + +function HashRow({ + label, + hash, + explorerUrl, + viewLabel, +}: { + label: string; + hash: string; + explorerUrl?: string; + viewLabel: string; +}) { + return ( +
+ + {label} + + + {explorerUrl ? ( + + {viewLabel} + + ) : null} +
+ ); +} diff --git a/src/lib/implementations/business/BridgeService.ts b/src/lib/implementations/business/BridgeService.ts new file mode 100644 index 0000000..949e1ba --- /dev/null +++ b/src/lib/implementations/business/BridgeService.ts @@ -0,0 +1,288 @@ +import { + EVMTransactionHash, + UriString, + type EVMAccountAddress, + type EVMChainId, +} from "@1shotapi/ows-types"; +import { erc20Abi } from "viem"; +import type { IBlockchainProvider } from "@1shotapi/ows-wallet-utils"; +import type { IChainRepository } from "../../interfaces/data/IChainRepository"; +import type { + ICctpInFlightBurn, + ICircleRepository, +} from "../../interfaces/data/ICircleRepository"; +import type { IKnownAssetRepository } from "../../interfaces/data/IKnownAssetRepository"; +import type { ICCTPUtils } from "../../interfaces/business/utils/ICCTPUtils"; +import type { ITransactionUtils } from "../../interfaces/business/utils/ITransactionUtils"; +import type { + IBridgeService, + ICctpBridgePayment, + ICctpBridgeQuote, + ICctpBridgeResult, + ICctpPollProgress, + ICctpQuoteParams, +} from "../../interfaces/business/IBridgeService"; +import type { ECircleDomainId } from "../../types/enum/ECircleDomainId"; + +const POLL_MS = 3000; +const MAX_POLL_ATTEMPTS = 400; + +/** + * Quote / execute / resume CCTP V2 USDC burns via Iris + the public relayer. + * Destination mint is Circle’s Forwarding Service — never `receiveMessage`. + */ +export class BridgeService implements IBridgeService { + constructor( + protected readonly chainRepository: IChainRepository, + protected readonly knownAssetRepository: IKnownAssetRepository, + protected readonly circleRepository: ICircleRepository, + protected readonly transactionUtils: ITransactionUtils, + protected readonly cctpUtils: ICCTPUtils, + protected readonly blockchain: IBlockchainProvider, + ) {} + + async listDestinations(sourceChainId: EVMChainId) { + const source = await this.requireChain(sourceChainId); + const chains = await this.chainRepository.list(); + return this.cctpUtils.listDestinations(chains, source); + } + + async quote(params: ICctpQuoteParams): Promise { + const { dest, sourceUsdc, sourceRoute, destRoute } = + await this.requireRoute(params.sourceChainId, params.destChainId); + + if (params.amountAtoms <= 0n) { + throw new Error("Bridge amount must be greater than zero"); + } + + const fees = await this.fetchBurnFees( + sourceRoute.irisBaseUrl, + sourceRoute.domain, + destRoute.domain, + params.speed, + params.amountAtoms, + ); + const paymentQuote = await this.transactionUtils.quotePayment( + params.sourceChainId, + params.owner, + sourceUsdc.address, + ); + const burnCalldata = this.cctpUtils.encodeDepositForBurnWithHook({ + totalBurn: fees.totalBurn, + destDomain: destRoute.domain, + mintRecipient: params.owner, + burnToken: sourceUsdc.address, + maxFee: fees.maxFee, + minFinalityThreshold: fees.minFinalityThreshold, + }); + + return { + sourceChainId: params.sourceChainId, + destChainId: params.destChainId, + amountAtoms: params.amountAtoms, + speed: params.speed, + owner: params.owner, + sourceUsdc, + destChain: dest, + minFinalityThreshold: fees.minFinalityThreshold, + forwardFee: fees.forwardFee, + protocolFee: fees.protocolFee, + maxFee: fees.maxFee, + totalBurn: fees.totalBurn, + netReceivedAtoms: params.amountAtoms, + paymentQuote, + burnCalldata, + }; + } + + async execute( + quote: ICctpBridgeQuote, + payment: ICctpBridgePayment, + onProgress?: (progress: ICctpPollProgress) => void, + ): Promise { + const { source, sourceUsdc, sourceRoute, destRoute } = + await this.requireRoute(quote.sourceChainId, quote.destChainId); + + const fees = await this.fetchBurnFees( + sourceRoute.irisBaseUrl, + sourceRoute.domain, + destRoute.domain, + quote.speed, + quote.amountAtoms, + ); + const contracts = this.cctpUtils.getContracts( + sourceRoute.domain, + sourceRoute.networkType, + ); + const allowance = await this.readAllowance( + quote.sourceChainId, + sourceUsdc.address, + quote.owner, + contracts.tokenMessengerV2, + ); + const burnData = this.cctpUtils.encodeDepositForBurnWithHook({ + totalBurn: fees.totalBurn, + destDomain: destRoute.domain, + mintRecipient: quote.owner, + burnToken: sourceUsdc.address, + maxFee: fees.maxFee, + minFinalityThreshold: fees.minFinalityThreshold, + }); + const approveData = this.cctpUtils.encodeUsdcApprove( + contracts.tokenMessengerV2, + fees.totalBurn, + ); + const work = this.cctpUtils.buildRelayerWork({ + allowance, + totalBurn: fees.totalBurn, + usdcAddress: sourceUsdc.address, + tokenMessenger: contracts.tokenMessengerV2, + approveData, + burnData, + }); + + const submitted = await this.transactionUtils.sendViaRelayer({ + chainId: quote.sourceChainId, + work, + paymentToken: payment.paymentToken, + feeAtoms: payment.feeAtoms, + relayerUrl: source.relayerUrl, + }); + + const inFlight: ICctpInFlightBurn = { + burnTxHash: submitted.transactionHash, + sourceDomain: sourceRoute.domain, + sourceChainId: quote.sourceChainId, + destChainId: quote.destChainId, + amountAtoms: quote.amountAtoms, + address: quote.owner, + }; + this.circleRepository.saveInFlight(inFlight); + onProgress?.({ burnTxHash: submitted.transactionHash }); + + const forwardTxHash = await this.pollUntilForwarded(inFlight, onProgress); + return { + burnTxHash: submitted.transactionHash, + forwardTxHash, + }; + } + + async resume(owner: EVMAccountAddress): Promise { + return this.circleRepository.loadInFlight(owner); + } + + async pollUntilForwarded( + inFlight: ICctpInFlightBurn, + onProgress?: (progress: ICctpPollProgress) => void, + ): Promise { + const sourceRoute = this.cctpUtils.requireRoute( + inFlight.sourceChainId, + ); + onProgress?.({ burnTxHash: inFlight.burnTxHash }); + + for (let i = 0; i < MAX_POLL_ATTEMPTS; i += 1) { + const message = await this.circleRepository.getMessageByBurnTx( + sourceRoute.irisBaseUrl, + inFlight.sourceDomain, + inFlight.burnTxHash, + ); + const hash = message?.forwardTxHash; + if (hash && hash.startsWith("0x") && hash.length >= 66) { + const forwardTxHash = EVMTransactionHash(hash as `0x${string}`); + onProgress?.({ + burnTxHash: inFlight.burnTxHash, + forwardTxHash, + }); + this.circleRepository.clearInFlight(inFlight.address); + return forwardTxHash; + } + await sleep(POLL_MS); + } + + throw new Error("Timed out waiting for Circle to mint on the destination"); + } + + private async fetchBurnFees( + irisBaseUrl: UriString, + sourceDomain: ECircleDomainId, + destDomain: ECircleDomainId, + speed: ICctpQuoteParams["speed"], + amountAtoms: bigint, + ) { + const rows = await this.circleRepository.getForwardingFees( + irisBaseUrl, + sourceDomain, + destDomain, + ); + const minFinalityThreshold = this.cctpUtils.finalityThresholdForSpeed(speed); + const row = this.cctpUtils.pickFeeForThreshold(rows, minFinalityThreshold); + return { + minFinalityThreshold, + ...this.cctpUtils.computeBurnFees(amountAtoms, row), + }; + } + + private async readAllowance( + chainId: EVMChainId, + usdc: EVMAccountAddress, + owner: EVMAccountAddress, + spender: EVMAccountAddress, + ): Promise { + const client = this.blockchain.getPublicClient(chainId); + try { + return await client.readContract({ + address: usdc, + abi: erc20Abi, + functionName: "allowance", + args: [owner, spender], + }); + } catch { + return 0n; + } + } + + private async requireChain(chainId: EVMChainId) { + const chain = await this.chainRepository.get(chainId); + if (!chain) { + throw new Error(`Unsupported chain: ${chainId}`); + } + if (!chain.useRelayer) { + throw new Error(`Chain ${chainId} does not support the 1Shot relayer`); + } + return chain; + } + + private async requireRoute( + sourceChainId: EVMChainId, + destChainId: EVMChainId, + ) { + const source = await this.requireChain(sourceChainId); + const dest = await this.chainRepository.get(destChainId); + if (!dest) { + throw new Error(`Unsupported destination chain: ${destChainId}`); + } + if (!this.cctpUtils.isValidDestination(source, dest)) { + throw new Error( + "Destination must be a same-network CCTP chain other than the source", + ); + } + const sourceUsdc = + await this.knownAssetRepository.getCctpBridgeAsset( + sourceChainId, + ); + if (!sourceUsdc) { + throw new Error(`No CCTP USDC on ${source.label}`); + } + return { + source, + dest, + sourceUsdc, + sourceRoute: this.cctpUtils.requireRoute(sourceChainId), + destRoute: this.cctpUtils.requireRoute(destChainId), + }; + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/lib/implementations/business/index.ts b/src/lib/implementations/business/index.ts index 81a36aa..39d9e42 100644 --- a/src/lib/implementations/business/index.ts +++ b/src/lib/implementations/business/index.ts @@ -1,6 +1,8 @@ export { TransactionService } from "./TransactionService"; export type { TransactionServiceOptions } from "./TransactionService"; +export { BridgeService } from "./BridgeService"; export { DelegationService } from "./DelegationService"; export type { DelegationServiceOptions } from "./DelegationService"; export { TransactionUtils as BusinessTransactionUtils } from "./utils/TransactionUtils"; export type { TransactionUtilsOptions as BusinessTransactionUtilsOptions } from "./utils/TransactionUtils"; +export { CCTPUtils } from "./utils/CCTPUtils"; diff --git a/src/lib/implementations/business/utils/CCTPUtils.ts b/src/lib/implementations/business/utils/CCTPUtils.ts new file mode 100644 index 0000000..75f3c91 --- /dev/null +++ b/src/lib/implementations/business/utils/CCTPUtils.ts @@ -0,0 +1,293 @@ +import { + EVMAccountAddress, + HexString, + UriString, + type EVMAccountAddress as EVMAccountAddressType, + type EVMChainId as EVMChainIdType, + type HexString as HexStringType, +} from "@1shotapi/ows-types"; +import { + encodeFunctionData, + erc20Abi, + pad, + padHex, + stringToHex, + type Hex, +} from "viem"; +import type { SupportedChain } from "../../../types/domain/SupportedChain"; +import { ECctpTransferSpeed } from "../../../types/enum/ECctpTransferSpeed"; +import { EChain } from "../../../types/enum/EChain"; +import { EChainNetworkType } from "../../../types/enum/EChainNetworkType"; +import { ECircleDomainId } from "../../../types/enum/ECircleDomainId"; +import type { IIrisForwardingFee } from "../../../interfaces/data/ICircleRepository"; +import type { ITransactionWork } from "../../../interfaces/business/ITransactionService"; +import type { + IBuildCctpRelayerWorkParams, + ICctpBurnFees, + ICctpContracts, + ICctpRoute, + ICCTPUtils, + IEncodeDepositForBurnWithHookParams, +} from "../../../interfaces/business/utils/ICCTPUtils"; + +const IRIS_API_MAINNET = UriString("https://iris-api.circle.com"); +const IRIS_API_TESTNET = UriString("https://iris-api-sandbox.circle.com"); +const CCTP_FAST_FINALITY_THRESHOLD = 1000; +const CCTP_SLOW_FINALITY_THRESHOLD = 2000; + +// The deployment addresses for the CCTP V2 contracts are identical across all networks we care about. There are some exceptions but they are for chains we don't support. +const MAINNET_TOKEN_MESSENGER = EVMAccountAddress( + "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", +); +const MAINNET_MESSAGE_TRANSMITTER = EVMAccountAddress( + "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", +); +const TESTNET_TOKEN_MESSENGER = EVMAccountAddress( + "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA", +); +const TESTNET_MESSAGE_TRANSMITTER = EVMAccountAddress( + "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275", +); + +/** TokenMessengerV2 fragment used for gasless CCTP burns. */ +const tokenMessengerV2Abi = [ + { + type: "function", + name: "depositForBurnWithHook", + stateMutability: "nonpayable", + inputs: [ + { name: "amount", type: "uint256" }, + { name: "destinationDomain", type: "uint32" }, + { name: "mintRecipient", type: "bytes32" }, + { name: "burnToken", type: "address" }, + { name: "destinationCaller", type: "bytes32" }, + { name: "maxFee", type: "uint256" }, + { name: "minFinalityThreshold", type: "uint32" }, + { name: "hookData", type: "bytes" }, + ], + outputs: [], + }, +] as const; + +const SUPPORTED_DOMAINS = new Set([ + ECircleDomainId.Ethereum, + ECircleDomainId.Optimism, + ECircleDomainId.Arbitrum, + ECircleDomainId.Base, + ECircleDomainId.Polygon, + ECircleDomainId.Unichain, + ECircleDomainId.Linea, + ECircleDomainId.Sonic, + ECircleDomainId.Monad, + ECircleDomainId.Arc, +]); + +const FORWARD_HOOK_DATA = HexString( + stringToHex("cctp-forward", { size: 32 }) as Hex, +); + +function route( + chainId: EVMChainIdType, + domain: ECircleDomainId, + networkType: EChainNetworkType, +): ICctpRoute { + return { + chainId, + domain, + networkType, + irisBaseUrl: + networkType === EChainNetworkType.Mainnet + ? IRIS_API_MAINNET + : IRIS_API_TESTNET, + }; +} + +const ROUTES: readonly ICctpRoute[] = [ + route(EChain.ArcTestnet, ECircleDomainId.Arc, EChainNetworkType.Testnet), + route(EChain.Sepolia, ECircleDomainId.Ethereum, EChainNetworkType.Testnet), + route(EChain.BaseSepolia, ECircleDomainId.Base, EChainNetworkType.Testnet), + route(EChain.Ethereum, ECircleDomainId.Ethereum, EChainNetworkType.Mainnet), + route(EChain.Optimism, ECircleDomainId.Optimism, EChainNetworkType.Mainnet), + route(EChain.Arbitrum, ECircleDomainId.Arbitrum, EChainNetworkType.Mainnet), + route(EChain.Base, ECircleDomainId.Base, EChainNetworkType.Mainnet), + route(EChain.Polygon, ECircleDomainId.Polygon, EChainNetworkType.Mainnet), + route(EChain.Linea, ECircleDomainId.Linea, EChainNetworkType.Mainnet), + route(EChain.Monad, ECircleDomainId.Monad, EChainNetworkType.Mainnet), + route(EChain.Sonic, ECircleDomainId.Sonic, EChainNetworkType.Mainnet), + route(EChain.Unichain, ECircleDomainId.Unichain, EChainNetworkType.Mainnet), +]; + +const BY_CHAIN = new Map( + ROUTES.map((entry) => [String(entry.chainId).toLowerCase(), entry]), +); + +/** + * CCTP V2 routes, Iris fee math, contract addresses, burn encoding, and + * destination filtering. Stateless — construct once and inject. + */ +export class CCTPUtils implements ICCTPUtils { + readonly irisApiMainnet = IRIS_API_MAINNET; + readonly irisApiTestnet = IRIS_API_TESTNET; + readonly fastFinalityThreshold = CCTP_FAST_FINALITY_THRESHOLD; + readonly slowFinalityThreshold = CCTP_SLOW_FINALITY_THRESHOLD; + readonly forwardHookData = FORWARD_HOOK_DATA; + + getRoute(chainId: EVMChainIdType): ICctpRoute | null { + return BY_CHAIN.get(String(chainId).toLowerCase()) ?? null; + } + + requireRoute(chainId: EVMChainIdType): ICctpRoute { + const found = this.getRoute(chainId); + if (!found) { + throw new Error(`Chain ${chainId} is not a CCTP V2 source`); + } + return found; + } + + finalityThresholdForSpeed(speed: ECctpTransferSpeed): number { + return speed === ECctpTransferSpeed.Fast + ? CCTP_FAST_FINALITY_THRESHOLD + : CCTP_SLOW_FINALITY_THRESHOLD; + } + + irisBaseUrlForNetwork(networkType: EChainNetworkType): UriString { + return networkType === EChainNetworkType.Mainnet + ? IRIS_API_MAINNET + : IRIS_API_TESTNET; + } + + getContracts( + domain: ECircleDomainId, + networkType: EChainNetworkType, + ): ICctpContracts { + if (!SUPPORTED_DOMAINS.has(domain)) { + throw new Error(`No CCTP V2 contracts for Circle domain ${domain}`); + } + if (networkType === EChainNetworkType.Mainnet) { + return { + tokenMessengerV2: MAINNET_TOKEN_MESSENGER, + messageTransmitterV2: MAINNET_MESSAGE_TRANSMITTER, + }; + } + return { + tokenMessengerV2: TESTNET_TOKEN_MESSENGER, + messageTransmitterV2: TESTNET_MESSAGE_TRANSMITTER, + }; + } + + /** + * Circle CCTP forwarding fee math (Ethereum→Arc quickstart). + * `minimumFee` is a human rate; convert via `round(minimumFee * 100) / 1e6`. + */ + computeBurnFees( + amountAtoms: bigint, + fee: IIrisForwardingFee, + ): ICctpBurnFees { + const forwardFee = BigInt(fee.forwardFee.med); + const protocolFee = + (amountAtoms * BigInt(Math.round(fee.minimumFee * 100))) / 1_000_000n; + const maxFee = forwardFee + protocolFee; + return { + forwardFee, + protocolFee, + maxFee, + totalBurn: amountAtoms + maxFee, + }; + } + + pickFeeForThreshold( + fees: readonly IIrisForwardingFee[], + finalityThreshold: number, + ): IIrisForwardingFee { + const match = fees.find( + (row) => row.finalityThreshold === finalityThreshold, + ); + if (!match) { + throw new Error( + `No Iris forwarding fee for finalityThreshold ${finalityThreshold}`, + ); + } + return match; + } + + encodeUsdcApprove( + spender: EVMAccountAddressType, + amount: bigint, + ): HexStringType { + return HexString( + encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, amount], + }) as Hex, + ); + } + + encodeDepositForBurnWithHook( + params: IEncodeDepositForBurnWithHookParams, + ): HexStringType { + return HexString( + encodeFunctionData({ + abi: tokenMessengerV2Abi, + functionName: "depositForBurnWithHook", + args: [ + params.totalBurn, + params.destDomain, + pad(params.mintRecipient, { size: 32 }), + params.burnToken, + padHex("0x", { size: 32 }), + params.maxFee, + params.minFinalityThreshold, + FORWARD_HOOK_DATA, + ], + }) as Hex, + ); + } + + shouldSkipUsdcApprove(allowance: bigint, totalBurn: bigint): boolean { + return allowance >= totalBurn; + } + + /** Approve (if needed) + `depositForBurnWithHook` ExactCalldata work items. */ + buildRelayerWork(params: IBuildCctpRelayerWorkParams): ITransactionWork[] { + const burn: ITransactionWork = { + to: params.tokenMessenger, + data: params.burnData, + value: 0n, + }; + if (this.shouldSkipUsdcApprove(params.allowance, params.totalBurn)) { + return [burn]; + } + return [ + { + to: params.usdcAddress, + data: params.approveData, + value: 0n, + }, + burn, + ]; + } + + isValidDestination(source: SupportedChain, dest: SupportedChain): boolean { + if (!dest.cctpBridgeDestination) { + return false; + } + if (dest.networkType !== source.networkType) { + return false; + } + return ( + String(dest.chainId).toLowerCase() !== + String(source.chainId).toLowerCase() + ); + } + + listDestinations( + chains: readonly SupportedChain[], + source: SupportedChain, + ): SupportedChain[] { + return chains.filter((chain) => this.isValidDestination(source, chain)); + } +} + +/** Exported for encode tests that decode against the same ABI fragment. */ +export { tokenMessengerV2Abi }; diff --git a/src/lib/implementations/business/utils/TransactionUtils.ts b/src/lib/implementations/business/utils/TransactionUtils.ts index 8fd4ce8..af9be53 100644 --- a/src/lib/implementations/business/utils/TransactionUtils.ts +++ b/src/lib/implementations/business/utils/TransactionUtils.ts @@ -239,13 +239,17 @@ export class TransactionUtils implements ITransactionUtils { async sendViaRelayer(args: { chainId: EVMChainId; - work: ITransactionWork; + work: ITransactionWork | ITransactionWork[]; paymentToken: EVMAccountAddress; feeAtoms: bigint; authorizationList?: IRelayerAuthorizationEntry[]; relayerUrl: string; }): Promise { - const { chainId, work, paymentToken, relayerUrl } = args; + const { chainId, paymentToken, relayerUrl } = args; + const workItems = Array.isArray(args.work) ? args.work : [args.work]; + if (workItems.length === 0) { + throw new Error("sendViaRelayer requires at least one work item"); + } let feeAtoms = args.feeAtoms; let authorizationList = args.authorizationList; @@ -308,12 +312,11 @@ export class TransactionUtils implements ITransactionUtils { args: [capabilities.feeCollector, feeAtoms], }), ); - const workData = (work.data || "0x") as Hex; - const workValue = work.value ?? 0n; const approveCopy = approveTransactionCeremony(needsUpgrade); + const minCalls = (needsUpgrade ? 1 : 0) + 1 + workItems.length; - // One passkey: optional EIP-7702 auth + fee + work delegations. + // One passkey: optional EIP-7702 auth + fee + each work delegation. const signed = await withCeremonyUiReason( EPasskeyPromptReason.ApproveTransaction, () => @@ -321,7 +324,7 @@ export class TransactionUtils implements ITransactionUtils { signer, approveCopy, async () => { - const [authEntry, feeDelegation, workDelegation] = + const [authEntry, feeDelegation, ...workDelegations] = await Promise.all([ needsUpgrade ? this.signWalletUpgradeAuthorizationInner(chainId, { @@ -338,18 +341,20 @@ export class TransactionUtils implements ITransactionUtils { callData: feeCalldata, chainIdNumber, }), - this.createAndSignExactCalldataDelegation({ - smartAccount, - delegate: capabilities.targetAddress, - target: work.to, - value: workValue, - callData: workData, - chainIdNumber, - }), + ...workItems.map((item) => + this.createAndSignExactCalldataDelegation({ + smartAccount, + delegate: capabilities.targetAddress, + target: item.to, + value: item.value ?? 0n, + callData: (item.data || "0x") as Hex, + chainIdNumber, + }), + ), ]); - return { authEntry, feeDelegation, workDelegation }; + return { authEntry, feeDelegation, workDelegations }; }, - { minCalls: needsUpgrade ? 3 : 2 }, + { minCalls }, ), ); @@ -357,7 +362,7 @@ export class TransactionUtils implements ITransactionUtils { authorizationList = [signed.authEntry]; } let feeDelegation = signed.feeDelegation; - const workDelegation = signed.workDelegation; + const workDelegations = signed.workDelegations; const buildParams = ( feeSig: unknown, @@ -385,16 +390,19 @@ export class TransactionUtils implements ITransactionUtils { }, ], }, - { - permissionContext: [toRelayerJson(workDelegation)], - executions: [ - { - target: work.to, - value: workValue === 0n ? "0" : `0x${workValue.toString(16)}`, - data: workData as HexString, - }, - ], - }, + ...workItems.map((item, index) => { + const value = item.value ?? 0n; + return { + permissionContext: [toRelayerJson(workDelegations[index])], + executions: [ + { + target: item.to, + value: value === 0n ? "0" : `0x${value.toString(16)}`, + data: (item.data || "0x") as HexString, + }, + ], + }; + }), ], ...(authorizationList?.length ? { authorizationList } diff --git a/src/lib/implementations/data/CircleRepository.ts b/src/lib/implementations/data/CircleRepository.ts new file mode 100644 index 0000000..22d83fe --- /dev/null +++ b/src/lib/implementations/data/CircleRepository.ts @@ -0,0 +1,250 @@ +import { + EVMAccountAddress, + EVMChainId, + EVMTransactionHash, + type EVMAccountAddress as EVMAccountAddressType, + type EVMTransactionHash as EVMTransactionHashType, +} from "@1shotapi/ows-types"; +import type { ECircleDomainId } from "../../types/enum/ECircleDomainId"; +import type { + ICctpInFlightBurn, + ICircleRepository, + IIrisCctpMessage, +} from "../../interfaces/data/ICircleRepository"; +import type { IIrisForwardingFee } from "../../interfaces/data/ICircleRepository"; + +export const CCTP_IN_FLIGHT_KEY_PREFIX = "oneshot.cctpInFlight."; + +export function inFlightStorageKey(address: EVMAccountAddressType): string { + return `${CCTP_IN_FLIGHT_KEY_PREFIX}${String(address).toLowerCase()}`; +} + +type IStoredInFlight = { + burnTxHash: string; + sourceDomain: number; + sourceChainId: string; + destChainId: string; + amountAtoms: string; + address: string; +}; + +export function serializeInFlight(record: ICctpInFlightBurn): string { + const stored: IStoredInFlight = { + burnTxHash: record.burnTxHash, + sourceDomain: record.sourceDomain, + sourceChainId: record.sourceChainId, + destChainId: record.destChainId, + amountAtoms: record.amountAtoms.toString(10), + address: record.address, + }; + return JSON.stringify(stored); +} + +export function parseInFlight(raw: string): ICctpInFlightBurn | null { + let parsed: unknown; + try { + parsed = JSON.parse(raw) as unknown; + } catch { + return null; + } + if (!parsed || typeof parsed !== "object") { + return null; + } + const row = parsed as Partial; + if ( + typeof row.burnTxHash !== "string" || + !row.burnTxHash.startsWith("0x") || + typeof row.sourceDomain !== "number" || + typeof row.sourceChainId !== "string" || + !row.sourceChainId.startsWith("0x") || + typeof row.destChainId !== "string" || + !row.destChainId.startsWith("0x") || + typeof row.amountAtoms !== "string" || + typeof row.address !== "string" || + !row.address.startsWith("0x") + ) { + return null; + } + let amountAtoms: bigint; + try { + amountAtoms = BigInt(row.amountAtoms); + } catch { + return null; + } + return { + burnTxHash: EVMTransactionHash(row.burnTxHash as `0x${string}`), + sourceDomain: row.sourceDomain as ECircleDomainId, + sourceChainId: EVMChainId(row.sourceChainId as `0x${string}`), + destChainId: EVMChainId(row.destChainId as `0x${string}`), + amountAtoms, + address: EVMAccountAddress(row.address as `0x${string}`), + }; +} + +type IrisFeeAmount = string | number; + +type IrisFeeJson = { + finalityThreshold?: number; + minimumFee?: number; + forwardFee?: { low?: IrisFeeAmount; med?: IrisFeeAmount; high?: IrisFeeAmount }; +}; + +type IrisMessagesJson = { + messages?: Array<{ + status?: string; + forwardTxHash?: string | null; + message?: string; + attestation?: string | null; + }>; +}; + +/** + * Circle Iris CCTP V2 client. HTTP only — no BridgeKit. + */ +export class CircleRepository implements ICircleRepository { + async getForwardingFees( + irisBaseUrl: string, + sourceDomain: ECircleDomainId, + destDomain: ECircleDomainId, + ): Promise { + const url = `${trimSlash(irisBaseUrl)}/v2/burn/USDC/fees/${sourceDomain}/${destDomain}?forward=true`; + const json = await getJson(url); + const rows = Array.isArray(json) ? json : []; + const fees: IIrisForwardingFee[] = []; + for (const item of rows) { + const fee = parseFeeRow(item); + if (fee) { + fees.push(fee); + } + } + if (fees.length === 0) { + throw new Error("Iris returned no forwarding fees"); + } + return fees; + } + + async getMessageByBurnTx( + irisBaseUrl: string, + sourceDomain: ECircleDomainId, + txHash: EVMTransactionHashType, + ): Promise { + const url = `${trimSlash(irisBaseUrl)}/v2/messages/${sourceDomain}?transactionHash=${encodeURIComponent(String(txHash))}`; + try { + const json = await getJson(url); + const first = json.messages?.[0]; + if (!first) { + return null; + } + return { + status: first.status, + forwardTxHash: + typeof first.forwardTxHash === "string" && + first.forwardTxHash.startsWith("0x") + ? EVMTransactionHash(first.forwardTxHash as `0x${string}`) + : null, + message: first.message, + attestation: first.attestation, + }; + } catch (error: unknown) { + if (error instanceof IrisHttpError && (error.status === 404 || error.status === 429)) { + return null; + } + throw error; + } + } + + saveInFlight(record: ICctpInFlightBurn): void { + try { + localStorage.setItem( + inFlightStorageKey(record.address), + serializeInFlight(record), + ); + } catch { + // Quota / private mode — polling still works for this session. + } + } + + loadInFlight(address: EVMAccountAddressType): ICctpInFlightBurn | null { + try { + const raw = localStorage.getItem(inFlightStorageKey(address)); + if (!raw) { + return null; + } + return parseInFlight(raw); + } catch { + return null; + } + } + + clearInFlight(address: EVMAccountAddressType): void { + try { + localStorage.removeItem(inFlightStorageKey(address)); + } catch { + // ignore + } + } +} + +class IrisHttpError extends Error { + constructor( + public readonly status: number, + message: string, + ) { + super(message); + this.name = "IrisHttpError"; + } +} + +/** Exported for unit tests — Iris fee rows vary string vs number atom amounts. */ +export function parseFeeRow(item: unknown): IIrisForwardingFee | null { + if (!item || typeof item !== "object") { + return null; + } + const row = item as IrisFeeJson; + if ( + typeof row.finalityThreshold !== "number" || + typeof row.minimumFee !== "number" || + !row.forwardFee + ) { + return null; + } + const med = normalizeIrisAmount(row.forwardFee.med); + if (med === null) { + return null; + } + const low = normalizeIrisAmount(row.forwardFee.low) ?? med; + const high = normalizeIrisAmount(row.forwardFee.high) ?? med; + return { + finalityThreshold: row.finalityThreshold, + minimumFee: row.minimumFee, + forwardFee: { low, med, high }, + }; +} + +function normalizeIrisAmount(value: IrisFeeAmount | undefined): string | null { + if (typeof value === "string" && value.length > 0) { + return value; + } + if (typeof value === "number" && Number.isFinite(value)) { + return String(Math.trunc(value)); + } + return null; +} + +async function getJson(url: string): Promise { + const response = await fetch(url, { + method: "GET", + headers: { Accept: "application/json" }, + }); + if (!response.ok) { + throw new IrisHttpError( + response.status, + `Iris ${response.status} ${response.statusText}`, + ); + } + return (await response.json()) as T; +} + +function trimSlash(url: string): string { + return url.endsWith("/") ? url.slice(0, -1) : url; +} diff --git a/src/lib/implementations/data/HardcodedChainRepository.ts b/src/lib/implementations/data/HardcodedChainRepository.ts index c7f3fd3..623b4a8 100644 --- a/src/lib/implementations/data/HardcodedChainRepository.ts +++ b/src/lib/implementations/data/HardcodedChainRepository.ts @@ -1,10 +1,10 @@ import { - EVMChainId, type EVMAccountAddress, type EVMChainId as EVMChainIdType, } from "@1shotapi/ows-types"; import type { IChainRepository } from "../../interfaces/data/IChainRepository"; import { SupportedChain } from "../../types/domain/SupportedChain"; +import { EChain } from "../../types/enum/EChain"; import { EChainNetworkType } from "../../types/enum/EChainNetworkType"; import arcLogo from "../../../assets/images/chains/arc-logo.png"; @@ -33,7 +33,7 @@ const ALCHEMY_KEY = "jqLUTbHeN_cVsIX2W7tJk"; */ const CATALOG: readonly SupportedChain[] = [ new SupportedChain( - EVMChainId("0x13b2"), + EChain.Arc, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, false, @@ -42,9 +42,10 @@ const CATALOG: readonly SupportedChain[] = [ `https://arc-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, "Arc", "https://explorer.arc.io", + false, ), new SupportedChain( - EVMChainId("0x4cef52"), + EChain.ArcTestnet, EChainNetworkType.Testnet, DEVELOPMENT_RELAYER_URL, true, @@ -53,9 +54,10 @@ const CATALOG: readonly SupportedChain[] = [ `https://arc-testnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, "Arc Testnet", "https://testnet.arcscan.app", + true, ), new SupportedChain( - EVMChainId("0xaa36a7"), + EChain.Sepolia, EChainNetworkType.Testnet, DEVELOPMENT_RELAYER_URL, true, @@ -64,9 +66,10 @@ const CATALOG: readonly SupportedChain[] = [ `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}`, "Sepolia", "https://sepolia.etherscan.io", + true, ), new SupportedChain( - EVMChainId("0x14a34"), + EChain.BaseSepolia, EChainNetworkType.Testnet, DEVELOPMENT_RELAYER_URL, true, @@ -75,9 +78,10 @@ const CATALOG: readonly SupportedChain[] = [ `https://base-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}`, "Base Sepolia", "https://sepolia.basescan.org", + true, ), new SupportedChain( - EVMChainId("0x1"), + EChain.Ethereum, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -86,9 +90,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://ethereum.publicnode.com", "Ethereum", "https://etherscan.io", + true, ), new SupportedChain( - EVMChainId("0xe708"), + EChain.Linea, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -97,9 +102,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://rpc.linea.build", "Linea", "https://lineascan.build", + true, ), new SupportedChain( - EVMChainId("0xa4b1"), + EChain.Arbitrum, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -108,9 +114,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://arb1.arbitrum.io/rpc", "Arbitrum", "https://arbiscan.io", + true, ), new SupportedChain( - EVMChainId("0xa"), + EChain.Optimism, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -119,9 +126,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://mainnet.optimism.io", "Optimism", "https://optimistic.etherscan.io", + true, ), new SupportedChain( - EVMChainId("0x38"), + EChain.Bsc, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -130,9 +138,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://bsc-dataseed.binance.org", "BSC", "https://bscscan.com", + false, ), new SupportedChain( - EVMChainId("0x2105"), + EChain.Base, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -141,9 +150,10 @@ const CATALOG: readonly SupportedChain[] = [ `https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, "Base", "https://basescan.org", + true, ), new SupportedChain( - EVMChainId("0x89"), + EChain.Polygon, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -152,9 +162,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://polygon-rpc.com", "Polygon", "https://polygonscan.com", + true, ), new SupportedChain( - EVMChainId("0x92"), + EChain.Sonic, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -163,9 +174,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://rpc.soniclabs.com", "Sonic", "https://sonicscan.org", + true, ), new SupportedChain( - EVMChainId("0x82"), + EChain.Unichain, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -174,9 +186,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://mainnet.unichain.org", "Unichain", "https://uniscan.xyz", + true, ), new SupportedChain( - EVMChainId("0x8f"), + EChain.Monad, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -185,9 +198,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://rpc.monad.xyz", "Monad", "https://monadvision.com", + true, ), new SupportedChain( - EVMChainId("0xa4ec"), + EChain.Celo, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -196,9 +210,10 @@ const CATALOG: readonly SupportedChain[] = [ "https://forno.celo.org", "Celo", "https://celoscan.io", + false, ), new SupportedChain( - EVMChainId("0x1237"), + EChain.Robinhood, EChainNetworkType.Mainnet, PRODUCTION_RELAYER_URL, true, @@ -207,11 +222,12 @@ const CATALOG: readonly SupportedChain[] = [ `https://robinhood-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, "Robinhood", "https://robinhoodchain.blockscout.com", + false, ), ]; /** Default chain for a fresh session (Arc mainnet). */ -export const DEFAULT_CHAIN_ID = EVMChainId("0x13b2"); +export const DEFAULT_CHAIN_ID = EChain.Arc; export class HardcodedChainRepository implements IChainRepository { private allowedChains: Set | null = null; diff --git a/src/lib/implementations/data/HardcodedKnownAssetRepository.ts b/src/lib/implementations/data/HardcodedKnownAssetRepository.ts index 53c016f..9809993 100644 --- a/src/lib/implementations/data/HardcodedKnownAssetRepository.ts +++ b/src/lib/implementations/data/HardcodedKnownAssetRepository.ts @@ -9,7 +9,11 @@ import { KnownAsset } from "../../types/domain/KnownAsset"; import { NewTrackedAsset } from "../../types/domain/TrackedAsset"; import { EAssetType } from "../../types/enum/EAssetType"; import { makeTrackedAssetId } from "@/lib/types/primitives"; -import { RELAYER_KNOWN_ASSETS } from "./relayerKnownAssets"; +import { EChain } from "../../types/enum/EChain"; +import { + RELAYER_KNOWN_ASSETS, + getCctpBridgeAsset as lookupCctpBridgeAsset, +} from "./relayerKnownAssets"; const BY_KEY = new Map( RELAYER_KNOWN_ASSETS.map((asset) => [ @@ -20,11 +24,11 @@ const BY_KEY = new Map( /** Chain → pinned default stablecoin symbol (always shown, not removable). */ const DEFAULT_TRACKED_STABLE_BY_CHAIN = new Map([ - ["0x4cef52", "USDC"], - ["0xaa36a7", "USDC"], - ["0x14a34", "USDC"], - ["0x2105", "USDC"], - ["0x1237", "USDG"], + [String(EChain.ArcTestnet).toLowerCase(), "USDC"], + [String(EChain.Sepolia).toLowerCase(), "USDC"], + [String(EChain.BaseSepolia).toLowerCase(), "USDC"], + [String(EChain.Base).toLowerCase(), "USDC"], + [String(EChain.Robinhood).toLowerCase(), "USDG"], ]); /** @@ -62,6 +66,12 @@ export class HardcodedKnownAssetRepository implements IKnownAssetRepository { return BY_KEY.get(makeTrackedAssetId(chainId, address)) ?? null; } + async getCctpBridgeAsset( + chainId: EVMChainIdType, + ): Promise { + return lookupCctpBridgeAsset(chainId); + } + async resolveForTracking( chainId: EVMChainIdType, address: EVMAccountAddressType, diff --git a/src/lib/implementations/data/index.ts b/src/lib/implementations/data/index.ts index 4bacc80..8a46bd5 100644 --- a/src/lib/implementations/data/index.ts +++ b/src/lib/implementations/data/index.ts @@ -13,6 +13,13 @@ export { DEFAULT_CHAIN_ID, HardcodedChainRepository, } from "./HardcodedChainRepository"; +export { CircleRepository } from "./CircleRepository"; +export { + parseInFlight, + serializeInFlight, + inFlightStorageKey, +} from "./CircleRepository"; +export { getCctpBridgeAsset } from "./relayerKnownAssets"; export { RelayerCredentialsClient, RelayerCredentialsError, diff --git a/src/lib/implementations/data/relayerKnownAssets.ts b/src/lib/implementations/data/relayerKnownAssets.ts index 783aa3e..0ada8fa 100644 --- a/src/lib/implementations/data/relayerKnownAssets.ts +++ b/src/lib/implementations/data/relayerKnownAssets.ts @@ -1,327 +1,353 @@ -import { - EVMAccountAddress, - EVMChainId, - type EVMAccountAddress as EVMAccountAddressType, - type EVMChainId as EVMChainIdType, -} from "@1shotapi/ows-types"; -import { KnownAsset } from "../../types/domain/KnownAsset"; -import { EAssetType } from "../../types/enum/EAssetType"; -import { makeTrackedAssetId } from "../../types/primitives"; -import { - iconUrlForSymbol, - registerKnownAssetIconResolver, -} from "../../utils/tokenIcons"; - -type ISeedRow = { - chainId: EVMChainIdType; - address: EVMAccountAddressType; - symbol: string; - name: string; - decimals: number; -}; - -function seed(row: ISeedRow): KnownAsset { - return new KnownAsset( - row.chainId, - row.address, - EAssetType.Erc20, - row.name, - row.symbol, - row.decimals, - iconUrlForSymbol(row.symbol), - ); -} - -/** - * Static snapshot from `relayer_getCapabilities` (prod + dev), including - * Arc Testnet USDC and Robinhood USDG. - * @see https://www.1shotapi.com/docs/relayer/get-started/overview - */ -const SEED_ROWS: readonly ISeedRow[] = [ - // Arc Testnet (5042002) — native USDC - { - chainId: EVMChainId("0x4cef52"), - address: EVMAccountAddress( - "0x3600000000000000000000000000000000000000", - ), - symbol: "USDC", - name: "USDC", - decimals: 6, - }, - // Robinhood (4663) — official USDG (USDC is not deployed) - { - chainId: EVMChainId("0x1237"), - address: EVMAccountAddress( - "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", - ), - symbol: "USDG", - name: "Global Dollar", - decimals: 6, - }, - // Ethereum mainnet (1) - { - chainId: EVMChainId("0x1"), - address: EVMAccountAddress( - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0x1"), - address: EVMAccountAddress( - "0xdac17f958d2ee523a2206206994597c13d831ec7", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 6, - }, - { - chainId: EVMChainId("0x1"), - address: EVMAccountAddress( - "0xe343167631d89B6Ffc58B88d6b7fB0228795491D", - ), - symbol: "USDG", - name: "Global Dollar", - decimals: 6, - }, - { - chainId: EVMChainId("0x1"), - address: EVMAccountAddress( - "0xacA92E438df0B2401fF60dA7E4337B687a2435DA", - ), - symbol: "mUSD", - name: "mUSD", - decimals: 6, - }, - // Optimism (10) - { - chainId: EVMChainId("0xa"), - address: EVMAccountAddress( - "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0xa"), - address: EVMAccountAddress( - "0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 6, - }, - // BSC (56) - { - chainId: EVMChainId("0x38"), - address: EVMAccountAddress( - "0x8AC76a51cc950d9822D68b83fe1Ad97B32Cd580d", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 18, - }, - { - chainId: EVMChainId("0x38"), - address: EVMAccountAddress( - "0x55d398326f99059fF775485246999027B3197955", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 18, - }, - // Unichain (130) - { - chainId: EVMChainId("0x82"), - address: EVMAccountAddress( - "0x078D782b760474a361dDA0AF3839290b0EF57AD6", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0x82"), - address: EVMAccountAddress( - "0xfe97E85d13ABD9c1c33384E796F10B73905637cE", - ), - symbol: "USD₮0", - name: "Tether USD", - decimals: 6, - }, - // Polygon (137) - { - chainId: EVMChainId("0x89"), - address: EVMAccountAddress( - "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0x89"), - address: EVMAccountAddress( - "0xc2132D05D31c914a87C6611C10748AeB04B58e8F", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 6, - }, - // Sonic (146) - { - chainId: EVMChainId("0x92"), - address: EVMAccountAddress( - "0x29219dd400f2Bf60E5a23d13Be72B486D4038894", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - // Monad (143) - { - chainId: EVMChainId("0x8f"), - address: EVMAccountAddress( - "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0x8f"), - address: EVMAccountAddress( - "0xe7cd86e13AC4309349F30B3435a9d337750fC82D", - ), - symbol: "USDT0", - name: "Tether USD", - decimals: 6, - }, - // Base (8453) - { - chainId: EVMChainId("0x2105"), - address: EVMAccountAddress( - "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0x2105"), - address: EVMAccountAddress( - "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 6, - }, - // Arbitrum (42161) - { - chainId: EVMChainId("0xa4b1"), - address: EVMAccountAddress( - "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0xa4b1"), - address: EVMAccountAddress( - "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 6, - }, - // Celo (42220) - { - chainId: EVMChainId("0xa4ec"), - address: EVMAccountAddress( - "0xcebA9300f2b948710d2653dd7b07f33A8B32118C", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0xa4ec"), - address: EVMAccountAddress( - "0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 6, - }, - // Linea (59144) - { - chainId: EVMChainId("0xe708"), - address: EVMAccountAddress( - "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - { - chainId: EVMChainId("0xe708"), - address: EVMAccountAddress( - "0xA219439258ca9da29E9Cc4cE5596924745e12B93", - ), - symbol: "USDT", - name: "Tether USD", - decimals: 6, - }, - { - chainId: EVMChainId("0xe708"), - address: EVMAccountAddress( - "0xaca92e438df0b2401ff60da7e4337b687a2435da", - ), - symbol: "mUSD", - name: "mUSD", - decimals: 6, - }, - // Base Sepolia (84532) - { - chainId: EVMChainId("0x14a34"), - address: EVMAccountAddress( - "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, - // Sepolia (11155111) - { - chainId: EVMChainId("0xaa36a7"), - address: EVMAccountAddress( - "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", - ), - symbol: "USDC", - name: "USD Coin", - decimals: 6, - }, -]; - -export const RELAYER_KNOWN_ASSETS: readonly KnownAsset[] = - SEED_ROWS.map(seed); - -const BY_KEY = new Map( - RELAYER_KNOWN_ASSETS.map((asset) => [ - makeTrackedAssetId(asset.chainId, asset.address), - asset, - ]), -); - -export function getKnownAssetIconUrl( - chainId: EVMChainIdType, - address: EVMAccountAddressType, -): string | undefined { - return BY_KEY.get(makeTrackedAssetId(chainId, address))?.iconUrl; -} - -registerKnownAssetIconResolver(getKnownAssetIconUrl); +import { + EVMAccountAddress, + type EVMAccountAddress as EVMAccountAddressType, + type EVMChainId as EVMChainIdType, +} from "@1shotapi/ows-types"; +import { EChain } from "../../types/enum/EChain"; +import { KnownAsset } from "../../types/domain/KnownAsset"; +import { EAssetType } from "../../types/enum/EAssetType"; +import { makeTrackedAssetId } from "../../types/primitives"; +import { + iconUrlForSymbol, + registerKnownAssetIconResolver, +} from "../../utils/tokenIcons"; + +type ISeedRow = { + chainId: EVMChainIdType; + address: EVMAccountAddressType; + symbol: string; + name: string; + decimals: number; + useCCTPBridge?: boolean; +}; + +function seed(row: ISeedRow): KnownAsset { + return new KnownAsset( + row.chainId, + row.address, + EAssetType.Erc20, + row.name, + row.symbol, + row.decimals, + row.useCCTPBridge === true, + iconUrlForSymbol(row.symbol), + ); +} + +/** + * Static snapshot from `relayer_getCapabilities` (prod + dev), including + * Arc Testnet USDC and Robinhood USDG. + * @see https://www.1shotapi.com/docs/relayer/get-started/overview + */ +const SEED_ROWS: readonly ISeedRow[] = [ + // Arc Testnet (5042002) — native USDC + { + chainId: EChain.ArcTestnet, + address: EVMAccountAddress( + "0x3600000000000000000000000000000000000000", + ), + symbol: "USDC", + name: "USDC", + decimals: 6, + useCCTPBridge: true, + }, + // Robinhood (4663) — official USDG (USDC is not deployed) + { + chainId: EChain.Robinhood, + address: EVMAccountAddress( + "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", + ), + symbol: "USDG", + name: "Global Dollar", + decimals: 6, + }, + // Ethereum mainnet (1) + { + chainId: EChain.Ethereum, + address: EVMAccountAddress( + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Ethereum, + address: EVMAccountAddress( + "0xdac17f958d2ee523a2206206994597c13d831ec7", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 6, + }, + { + chainId: EChain.Ethereum, + address: EVMAccountAddress( + "0xe343167631d89B6Ffc58B88d6b7fB0228795491D", + ), + symbol: "USDG", + name: "Global Dollar", + decimals: 6, + }, + { + chainId: EChain.Ethereum, + address: EVMAccountAddress( + "0xacA92E438df0B2401fF60dA7E4337B687a2435DA", + ), + symbol: "mUSD", + name: "mUSD", + decimals: 6, + }, + // Optimism (10) + { + chainId: EChain.Optimism, + address: EVMAccountAddress( + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Optimism, + address: EVMAccountAddress( + "0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 6, + }, + // BSC (56) + { + chainId: EChain.Bsc, + address: EVMAccountAddress( + "0x8AC76a51cc950d9822D68b83fe1Ad97B32Cd580d", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 18, + }, + { + chainId: EChain.Bsc, + address: EVMAccountAddress( + "0x55d398326f99059fF775485246999027B3197955", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 18, + }, + // Unichain (130) + { + chainId: EChain.Unichain, + address: EVMAccountAddress( + "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Unichain, + address: EVMAccountAddress( + "0xfe97E85d13ABD9c1c33384E796F10B73905637cE", + ), + symbol: "USD₮0", + name: "Tether USD", + decimals: 6, + }, + // Polygon (137) + { + chainId: EChain.Polygon, + address: EVMAccountAddress( + "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Polygon, + address: EVMAccountAddress( + "0xc2132D05D31c914a87C6611C10748AeB04B58e8F", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 6, + }, + // Sonic (146) + { + chainId: EChain.Sonic, + address: EVMAccountAddress( + "0x29219dd400f2Bf60E5a23d13Be72B486D4038894", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + // Monad (143) + { + chainId: EChain.Monad, + address: EVMAccountAddress( + "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Monad, + address: EVMAccountAddress( + "0xe7cd86e13AC4309349F30B3435a9d337750fC82D", + ), + symbol: "USDT0", + name: "Tether USD", + decimals: 6, + }, + // Base (8453) + { + chainId: EChain.Base, + address: EVMAccountAddress( + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Base, + address: EVMAccountAddress( + "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 6, + }, + // Arbitrum (42161) + { + chainId: EChain.Arbitrum, + address: EVMAccountAddress( + "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Arbitrum, + address: EVMAccountAddress( + "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 6, + }, + // Celo (42220) + { + chainId: EChain.Celo, + address: EVMAccountAddress( + "0xcebA9300f2b948710d2653dd7b07f33A8B32118C", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + }, + { + chainId: EChain.Celo, + address: EVMAccountAddress( + "0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 6, + }, + // Linea (59144) + { + chainId: EChain.Linea, + address: EVMAccountAddress( + "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + { + chainId: EChain.Linea, + address: EVMAccountAddress( + "0xA219439258ca9da29E9Cc4cE5596924745e12B93", + ), + symbol: "USDT", + name: "Tether USD", + decimals: 6, + }, + { + chainId: EChain.Linea, + address: EVMAccountAddress( + "0xaca92e438df0b2401ff60da7e4337b687a2435da", + ), + symbol: "mUSD", + name: "mUSD", + decimals: 6, + }, + // Base Sepolia (84532) + { + chainId: EChain.BaseSepolia, + address: EVMAccountAddress( + "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, + // Sepolia (11155111) + { + chainId: EChain.Sepolia, + address: EVMAccountAddress( + "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + ), + symbol: "USDC", + name: "USD Coin", + decimals: 6, + useCCTPBridge: true, + }, +]; + +export const RELAYER_KNOWN_ASSETS: readonly KnownAsset[] = + SEED_ROWS.map(seed); + +const BY_KEY = new Map( + RELAYER_KNOWN_ASSETS.map((asset) => [ + makeTrackedAssetId(asset.chainId, asset.address), + asset, + ]), +); + +export function getKnownAssetIconUrl( + chainId: EVMChainIdType, + address: EVMAccountAddressType, +): string | undefined { + return BY_KEY.get(makeTrackedAssetId(chainId, address))?.iconUrl; +} + +export function getCctpBridgeAsset( + chainId: EVMChainIdType, +): KnownAsset | null { + const key = String(chainId).toLowerCase(); + return ( + RELAYER_KNOWN_ASSETS.find( + (asset) => + asset.useCCTPBridge && String(asset.chainId).toLowerCase() === key, + ) ?? null + ); +} + +registerKnownAssetIconResolver(getKnownAssetIconUrl); diff --git a/src/lib/interfaces/business/IBridgeService.ts b/src/lib/interfaces/business/IBridgeService.ts new file mode 100644 index 0000000..0299e40 --- /dev/null +++ b/src/lib/interfaces/business/IBridgeService.ts @@ -0,0 +1,77 @@ +import type { + EVMAccountAddress, + EVMChainId, + EVMTransactionHash, + HexString, +} from "@1shotapi/ows-types"; +import type { ECctpTransferSpeed } from "../../types/enum/ECctpTransferSpeed"; +import type { KnownAsset } from "../../types/domain/KnownAsset"; +import type { SupportedChain } from "../../types/domain/SupportedChain"; +import type { ICctpInFlightBurn } from "../data/ICircleRepository"; +import type { IPaymentQuote } from "./ITransactionService"; + +export interface ICctpQuoteParams { + sourceChainId: EVMChainId; + destChainId: EVMChainId; + amountAtoms: bigint; + speed: ECctpTransferSpeed; + owner: EVMAccountAddress; +} + +export interface ICctpBridgeQuote { + sourceChainId: EVMChainId; + destChainId: EVMChainId; + amountAtoms: bigint; + speed: ECctpTransferSpeed; + owner: EVMAccountAddress; + sourceUsdc: KnownAsset; + destChain: SupportedChain; + minFinalityThreshold: number; + forwardFee: bigint; + protocolFee: bigint; + maxFee: bigint; + totalBurn: bigint; + netReceivedAtoms: bigint; + paymentQuote: IPaymentQuote; + burnCalldata: HexString; +} + +export interface ICctpBridgePayment { + paymentToken: EVMAccountAddress; + feeAtoms: bigint; +} + +export interface ICctpBridgeResult { + burnTxHash: EVMTransactionHash; + forwardTxHash?: EVMTransactionHash; +} + +export interface ICctpPollProgress { + burnTxHash: EVMTransactionHash; + forwardTxHash?: EVMTransactionHash; +} + +/** + * Gasless CCTP V2 USDC bridge via native TokenMessengerV2 + Circle Forwarding + * Service, submitted through the 1Shot relayer. + */ +export interface IBridgeService { + listDestinations(sourceChainId: EVMChainId): Promise; + + quote(params: ICctpQuoteParams): Promise; + + execute( + quote: ICctpBridgeQuote, + payment: ICctpBridgePayment, + onProgress?: (progress: ICctpPollProgress) => void, + ): Promise; + + resume(owner: EVMAccountAddress): Promise; + + pollUntilForwarded( + inFlight: ICctpInFlightBurn, + onProgress?: (progress: ICctpPollProgress) => void, + ): Promise; +} + +export const IBridgeServiceType = Symbol.for("IBridgeService"); diff --git a/src/lib/interfaces/business/ITransactionService.ts b/src/lib/interfaces/business/ITransactionService.ts index 52da246..42bfe65 100644 --- a/src/lib/interfaces/business/ITransactionService.ts +++ b/src/lib/interfaces/business/ITransactionService.ts @@ -34,7 +34,7 @@ export interface ITransactionWork { export interface ISendViaRelayerParams { chainId: EVMChainId; - work: ITransactionWork; + work: ITransactionWork | ITransactionWork[]; paymentToken: EVMAccountAddress; /** Fee atoms from the confirm UI quote; may be adjusted after estimate. */ feeAtoms: bigint; diff --git a/src/lib/interfaces/business/index.ts b/src/lib/interfaces/business/index.ts index b72d1cb..e9de502 100644 --- a/src/lib/interfaces/business/index.ts +++ b/src/lib/interfaces/business/index.ts @@ -6,6 +6,15 @@ export type { ITransactionWork, } from "./ITransactionService"; export { ITransactionServiceType } from "./ITransactionService"; +export type { + IBridgeService, + ICctpBridgePayment, + ICctpBridgeQuote, + ICctpBridgeResult, + ICctpPollProgress, + ICctpQuoteParams, +} from "./IBridgeService"; +export { IBridgeServiceType } from "./IBridgeService"; export type { ICancelDelegationParams, ICancelDelegationResult, @@ -18,3 +27,12 @@ export { } from "./IDelegationService"; export type { ITransactionUtils as IBusinessTransactionUtils } from "./utils/ITransactionUtils"; export { ITransactionUtilsType as IBusinessTransactionUtilsType } from "./utils/ITransactionUtils"; +export type { + IBuildCctpRelayerWorkParams, + ICctpBurnFees, + ICctpContracts, + ICctpRoute, + ICCTPUtils, + IEncodeDepositForBurnWithHookParams, +} from "./utils/ICCTPUtils"; +export { ICCTPUtilsType } from "./utils/ICCTPUtils"; diff --git a/src/lib/interfaces/business/utils/ICCTPUtils.ts b/src/lib/interfaces/business/utils/ICCTPUtils.ts new file mode 100644 index 0000000..feaf465 --- /dev/null +++ b/src/lib/interfaces/business/utils/ICCTPUtils.ts @@ -0,0 +1,104 @@ +import type { + EVMAccountAddress, + EVMChainId, + HexString, + UriString, +} from "@1shotapi/ows-types"; +import type { SupportedChain } from "../../../types/domain/SupportedChain"; +import type { ECctpTransferSpeed } from "../../../types/enum/ECctpTransferSpeed"; +import type { EChainNetworkType } from "../../../types/enum/EChainNetworkType"; +import type { ECircleDomainId } from "../../../types/enum/ECircleDomainId"; +import type { IIrisForwardingFee } from "../../data/ICircleRepository"; +import type { ITransactionWork } from "../ITransactionService"; + +export interface ICctpContracts { + tokenMessengerV2: EVMAccountAddress; + messageTransmitterV2: EVMAccountAddress; +} + +export interface ICctpRoute { + chainId: EVMChainId; + domain: ECircleDomainId; + networkType: EChainNetworkType; + irisBaseUrl: UriString; +} + +export interface ICctpBurnFees { + forwardFee: bigint; + protocolFee: bigint; + maxFee: bigint; + totalBurn: bigint; +} + +export interface IEncodeDepositForBurnWithHookParams { + totalBurn: bigint; + destDomain: ECircleDomainId; + mintRecipient: EVMAccountAddress; + burnToken: EVMAccountAddress; + maxFee: bigint; + minFinalityThreshold: number; +} + +export interface IBuildCctpRelayerWorkParams { + allowance: bigint; + totalBurn: bigint; + usdcAddress: EVMAccountAddress; + tokenMessenger: EVMAccountAddress; + approveData: HexString; + burnData: HexString; +} + +/** + * CCTP V2 helpers: routes, Iris fee math, contract addresses, burn calldata, + * and destination filtering. Stateless — safe as a singleton. + */ +export interface ICCTPUtils { + readonly irisApiMainnet: string; + readonly irisApiTestnet: string; + readonly fastFinalityThreshold: number; + readonly slowFinalityThreshold: number; + /** 32-byte `cctp-forward` hook (Circle Forwarding Service). */ + readonly forwardHookData: HexString; + + getRoute(chainId: EVMChainId): ICctpRoute | null; + + requireRoute(chainId: EVMChainId): ICctpRoute; + + finalityThresholdForSpeed(speed: ECctpTransferSpeed): number; + + irisBaseUrlForNetwork(networkType: EChainNetworkType): UriString; + + getContracts( + domain: ECircleDomainId, + networkType: EChainNetworkType, + ): ICctpContracts; + + computeBurnFees( + amountAtoms: bigint, + fee: IIrisForwardingFee, + ): ICctpBurnFees; + + pickFeeForThreshold( + fees: readonly IIrisForwardingFee[], + finalityThreshold: number, + ): IIrisForwardingFee; + + encodeUsdcApprove(spender: EVMAccountAddress, amount: bigint): HexString; + + encodeDepositForBurnWithHook( + params: IEncodeDepositForBurnWithHookParams, + ): HexString; + + shouldSkipUsdcApprove(allowance: bigint, totalBurn: bigint): boolean; + + buildRelayerWork(params: IBuildCctpRelayerWorkParams): ITransactionWork[]; + + isValidDestination(source: SupportedChain, dest: SupportedChain): boolean; + + listDestinations( + chains: readonly SupportedChain[], + source: SupportedChain, + ): SupportedChain[]; +} + +export const ICCTPUtilsType = Symbol.for("business.ICCTPUtils"); diff --git a/src/lib/interfaces/business/utils/ITransactionUtils.ts b/src/lib/interfaces/business/utils/ITransactionUtils.ts index cc1c605..4ec6e21 100644 --- a/src/lib/interfaces/business/utils/ITransactionUtils.ts +++ b/src/lib/interfaces/business/utils/ITransactionUtils.ts @@ -40,11 +40,12 @@ export interface ITransactionUtils { /** * Public-relayer ExactCalldata fee + work path: optional EIP-7702 upgrade, - * estimate, send, poll. + * estimate, send, poll. `work` may be one item (Send) or several + * (e.g. USDC approve + CCTP burn) — still one fee and one passkey ceremony. */ sendViaRelayer(args: { chainId: EVMChainId; - work: ITransactionWork; + work: ITransactionWork | ITransactionWork[]; paymentToken: EVMAccountAddress; feeAtoms: bigint; authorizationList?: IRelayerAuthorizationEntry[]; diff --git a/src/lib/interfaces/business/utils/index.ts b/src/lib/interfaces/business/utils/index.ts index 7587def..07d8f2c 100644 --- a/src/lib/interfaces/business/utils/index.ts +++ b/src/lib/interfaces/business/utils/index.ts @@ -1,2 +1,11 @@ export type { ITransactionUtils } from "./ITransactionUtils"; export { ITransactionUtilsType } from "./ITransactionUtils"; +export type { + IBuildCctpRelayerWorkParams, + ICctpBurnFees, + ICctpContracts, + ICctpRoute, + ICCTPUtils, + IEncodeDepositForBurnWithHookParams, +} from "./ICCTPUtils"; +export { ICCTPUtilsType } from "./ICCTPUtils"; diff --git a/src/lib/interfaces/data/ICircleRepository.ts b/src/lib/interfaces/data/ICircleRepository.ts new file mode 100644 index 0000000..9667366 --- /dev/null +++ b/src/lib/interfaces/data/ICircleRepository.ts @@ -0,0 +1,60 @@ +import type { + EVMAccountAddress, + EVMChainId, + EVMTransactionHash, + UriString, +} from "@1shotapi/ows-types"; +import type { ECircleDomainId } from "../../types/enum/ECircleDomainId"; + +/** Iris `forward=true` fee row used to size `depositForBurnWithHook`. */ +export interface IIrisForwardingFee { + finalityThreshold: number; + minimumFee: number; + forwardFee: { + low: string; + med: string; + high: string; + }; +} + +export interface ICctpInFlightBurn { + burnTxHash: EVMTransactionHash; + sourceDomain: ECircleDomainId; + sourceChainId: EVMChainId; + destChainId: EVMChainId; + amountAtoms: bigint; + address: EVMAccountAddress; +} + +export interface IIrisCctpMessage { + status?: string; + forwardTxHash?: EVMTransactionHash | null; + message?: string; + attestation?: string | null; +} + +/** + * Circle Iris HTTP client for CCTP V2 forwarding fees and burn-message polling. + * No BridgeKit; persist in-flight burns for resume after reload. + */ +export interface ICircleRepository { + getForwardingFees( + irisBaseUrl: UriString, + sourceDomain: ECircleDomainId, + destDomain: ECircleDomainId, + ): Promise; + + getMessageByBurnTx( + irisBaseUrl: UriString, + sourceDomain: ECircleDomainId, + txHash: EVMTransactionHash, + ): Promise; + + saveInFlight(record: ICctpInFlightBurn): void; + + loadInFlight(address: EVMAccountAddress): ICctpInFlightBurn | null; + + clearInFlight(address: EVMAccountAddress): void; +} + +export const ICircleRepositoryType = Symbol.for("ICircleRepository"); diff --git a/src/lib/interfaces/data/IKnownAssetRepository.ts b/src/lib/interfaces/data/IKnownAssetRepository.ts index 80ea258..8f219a9 100644 --- a/src/lib/interfaces/data/IKnownAssetRepository.ts +++ b/src/lib/interfaces/data/IKnownAssetRepository.ts @@ -7,6 +7,9 @@ export interface IKnownAssetRepository { address: EVMAccountAddress, ): Promise; + /** Native Circle USDC on `chainId` when the catalog marks `useCCTPBridge`. */ + getCctpBridgeAsset(chainId: EVMChainId): Promise; + /** * Catalog hit or on-chain ERC-20 probe → NewTrackedAsset. * Throws if the address is not a contract or not ERC-20. diff --git a/src/lib/interfaces/data/index.ts b/src/lib/interfaces/data/index.ts index 6a837ad..ba94fa6 100644 --- a/src/lib/interfaces/data/index.ts +++ b/src/lib/interfaces/data/index.ts @@ -25,6 +25,13 @@ export type { export { IOneshotRelayerRepositoryType } from "./IOneshotRelayerRepository"; export type { IChainRepository } from "./IChainRepository"; export { IChainRepositoryType } from "./IChainRepository"; +export type { + ICctpInFlightBurn, + ICircleRepository, + IIrisCctpMessage, + IIrisForwardingFee, +} from "./ICircleRepository"; +export { ICircleRepositoryType } from "./ICircleRepository"; export type { IDelegationRepository } from "./IDelegationRepository"; export { IDelegationRepositoryType } from "./IDelegationRepository"; export type { IRelayerCredentialsClient } from "./IRelayerCredentialsClient"; diff --git a/src/lib/types/domain/KnownAsset.ts b/src/lib/types/domain/KnownAsset.ts index a4778aa..325960e 100644 --- a/src/lib/types/domain/KnownAsset.ts +++ b/src/lib/types/domain/KnownAsset.ts @@ -10,6 +10,7 @@ export class KnownAsset { public readonly name: string, public readonly symbol: string, public readonly decimals: number, + public readonly useCCTPBridge: boolean, public readonly iconUrl?: string, ) {} } diff --git a/src/lib/types/domain/SupportedChain.ts b/src/lib/types/domain/SupportedChain.ts index 733fb99..0fe0e81 100644 --- a/src/lib/types/domain/SupportedChain.ts +++ b/src/lib/types/domain/SupportedChain.ts @@ -20,6 +20,7 @@ export class SupportedChain { public readonly label: string, /** Base URL for tx/address explorer links (no trailing slash). */ public readonly blockExplorerUrl: string, + public readonly cctpBridgeDestination: boolean, ) {} public txExplorerUrl(transactionHash: string): string { diff --git a/src/lib/types/enum/ECctpTransferSpeed.ts b/src/lib/types/enum/ECctpTransferSpeed.ts new file mode 100644 index 0000000..24f76c3 --- /dev/null +++ b/src/lib/types/enum/ECctpTransferSpeed.ts @@ -0,0 +1,5 @@ +/** CCTP V2 finality: Fast ≈ 1000, Slow ≈ 2000 (Iris `finalityThreshold`). */ +export enum ECctpTransferSpeed { + Fast = "fast", + Slow = "slow", +} diff --git a/src/lib/types/enum/EChain.ts b/src/lib/types/enum/EChain.ts new file mode 100644 index 0000000..4d69d48 --- /dev/null +++ b/src/lib/types/enum/EChain.ts @@ -0,0 +1,25 @@ +import { EVMChainId } from "@1shotapi/ows-types"; + +/** + * Catalog EVM chain ids (hex). Prefer these over inline `EVMChainId("0x…")`. + */ +export const EChain = { + Arc: EVMChainId("0x13b2"), + ArcTestnet: EVMChainId("0x4cef52"), + Sepolia: EVMChainId("0xaa36a7"), + BaseSepolia: EVMChainId("0x14a34"), + Ethereum: EVMChainId("0x1"), + Linea: EVMChainId("0xe708"), + Arbitrum: EVMChainId("0xa4b1"), + Optimism: EVMChainId("0xa"), + Bsc: EVMChainId("0x38"), + Base: EVMChainId("0x2105"), + Polygon: EVMChainId("0x89"), + Sonic: EVMChainId("0x92"), + Unichain: EVMChainId("0x82"), + Monad: EVMChainId("0x8f"), + Celo: EVMChainId("0xa4ec"), + Robinhood: EVMChainId("0x1237"), +} as const; + +export type EChain = (typeof EChain)[keyof typeof EChain]; diff --git a/src/lib/types/enum/ECircleDomainId.ts b/src/lib/types/enum/ECircleDomainId.ts new file mode 100644 index 0000000..c6a70f9 --- /dev/null +++ b/src/lib/types/enum/ECircleDomainId.ts @@ -0,0 +1,30 @@ +export enum ECircleDomainId { + Ethereum = 0, + Avalanche = 1, + Optimism = 2, + Arbitrum = 3, + Solana = 5, + Base = 6, + Polygon = 7, + Unichain = 10, + Linea = 11, + Codex = 12, + Sonic = 13, + World = 14, + Monad = 15, + Sei = 16, + Binance = 17, + XDC = 18, + HyperEVM = 19, + Ink = 21, + Plume = 22, + Starknet = 25, + Arc = 26, + Stellar = 27, + EDGE = 28, + Injective = 29, + Morph = 30, + Pharos = 31, + Cronos = 32, + XLayer = 37, +} \ No newline at end of file diff --git a/src/lib/types/enum/index.ts b/src/lib/types/enum/index.ts index e5ec64b..72f3dea 100644 --- a/src/lib/types/enum/index.ts +++ b/src/lib/types/enum/index.ts @@ -1,6 +1,10 @@ export { EAssetActivityKind } from "./EAssetActivityKind"; export { EAssetActivityStatus } from "./EAssetActivityStatus"; export { EAssetType } from "./EAssetType"; +export { ECctpTransferSpeed } from "./ECctpTransferSpeed"; +export { EChain } from "./EChain"; export { EChainNetworkType } from "./EChainNetworkType"; +export { ECircleDomainId } from "./ECircleDomainId"; export { EPasskeyPromptReason } from "./EPasskeyPromptReason"; export { EWalletEventKind } from "./EWalletEventKind"; + diff --git a/src/style/applyStyle.ts b/src/style/applyStyle.ts index 8d1323e..a4a1f99 100644 --- a/src/style/applyStyle.ts +++ b/src/style/applyStyle.ts @@ -54,6 +54,10 @@ export function mergeStyle( ...current.copy.transferTokens, ...patch.copy?.transferTokens, }, + cctpBridge: { + ...current.copy.cctpBridge, + ...patch.copy?.cctpBridge, + }, grantExecutionPermission: { ...current.copy.grantExecutionPermission, ...patch.copy?.grantExecutionPermission, @@ -194,6 +198,7 @@ function cloneDefaultStyle(): IResolvedStyle { sendTransaction: { ...DEFAULT_STYLE.copy.sendTransaction }, confirmTransfer: { ...DEFAULT_STYLE.copy.confirmTransfer }, transferTokens: { ...DEFAULT_STYLE.copy.transferTokens }, + cctpBridge: { ...DEFAULT_STYLE.copy.cctpBridge }, grantExecutionPermission: { ...DEFAULT_STYLE.copy.grantExecutionPermission, }, diff --git a/src/style/defaults.ts b/src/style/defaults.ts index e4b29db..584d1b9 100644 --- a/src/style/defaults.ts +++ b/src/style/defaults.ts @@ -142,6 +142,47 @@ export const DEFAULT_STYLE: IResolvedStyle = { viewOnExplorerLabel: "View on explorer", doneLabel: "Done", }, + cctpBridge: { + title: "Bridge USDC", + body: "Send USDC to another network. Circle mints on the destination — you never pay native gas.", + amountLabel: "Amount", + amountPlaceholder: "0.0", + destinationLabel: "Destination", + destinationPlaceholder: "Select network", + speedLabel: "Speed", + speedFastLabel: "Fast", + speedSlowLabel: "Standard", + recipientLabel: "Recipient", + recipientHint: "Always your wallet on the destination network.", + getQuoteLabel: "Get quote", + confirmLabel: "Confirm bridge", + cancelLabel: "Cancel", + retryLabel: "Retry", + transferAmountLabel: "Transfer", + forwardFeeLabel: "Forwarding fee", + protocolFeeLabel: "CCTP protocol fee", + cctpFeeLabel: "CCTP fees", + relayerFeeLabel: "Relayer fee", + totalBurnLabel: "Total burned", + netReceivedLabel: "You receive", + quotingLabel: "Fetching quote…", + submittingLabel: "Submitting burn…", + pollingLabel: "Waiting for destination mint…", + sourceHashLabel: "Source transaction", + destHashLabel: "Destination mint", + successTitle: "Bridge complete", + successBody: "USDC has been minted on the destination network.", + quoteFailedError: "Could not fetch a bridge quote.", + submitFailedError: "Bridge failed.", + timeoutError: + "Destination mint is taking longer than expected. Retry to keep waiting.", + insufficientBalanceError: + "Insufficient USDC for amount, CCTP fees, and relayer fee.", + invalidAmountError: "Enter a valid amount.", + noDestinationError: "Select a destination network.", + viewOnExplorerLabel: "View on explorer", + doneLabel: "Done", + }, grantExecutionPermission: { title: "Grant spending permission", body: "{domain} wants a periodic ERC-20 spending permission for {to} on {chainName}.", @@ -326,6 +367,7 @@ export const DEFAULT_STYLE: IResolvedStyle = { receiveCopyFailedLabel: "Copy failed", receiveCloseLabel: "Close", sendLabel: "Send", + bridgeLabel: "Bridge", }, exportPrivateKey: { title: "Export private key", diff --git a/src/style/index.ts b/src/style/index.ts index ae9194e..82de677 100644 --- a/src/style/index.ts +++ b/src/style/index.ts @@ -12,6 +12,7 @@ export type { IStyleCopyTypedData, IStyleCopySendTransaction, IStyleCopyTransferTokens, + IStyleCopyCctpBridge, IStyleCopyGrantExecutionPermission, IStyleCopyCancelDelegation, IStyleCopyPasskeyPrompt, diff --git a/src/style/registerConfigure.ts b/src/style/registerConfigure.ts index 7175874..c2686d6 100644 --- a/src/style/registerConfigure.ts +++ b/src/style/registerConfigure.ts @@ -142,16 +142,46 @@ const transferTokensCopySchema = z.strictObject({ scanQrLabel: z.string().optional(), cancelLabel: z.string().optional(), sendLabel: z.string().optional(), - invalidAmountError: z.string().optional(), + doneLabel: z.string().optional(), + }) + .optional(); + +const cctpBridgeCopySchema = z.strictObject({ + title: z.string().optional(), + body: z.string().optional(), + amountLabel: z.string().optional(), + amountPlaceholder: z.string().optional(), + destinationLabel: z.string().optional(), + destinationPlaceholder: z.string().optional(), + speedLabel: z.string().optional(), + speedFastLabel: z.string().optional(), + speedSlowLabel: z.string().optional(), + recipientLabel: z.string().optional(), + recipientHint: z.string().optional(), + getQuoteLabel: z.string().optional(), + confirmLabel: z.string().optional(), + cancelLabel: z.string().optional(), + retryLabel: z.string().optional(), + transferAmountLabel: z.string().optional(), + forwardFeeLabel: z.string().optional(), + protocolFeeLabel: z.string().optional(), + cctpFeeLabel: z.string().optional(), + relayerFeeLabel: z.string().optional(), + totalBurnLabel: z.string().optional(), + netReceivedLabel: z.string().optional(), + quotingLabel: z.string().optional(), + submittingLabel: z.string().optional(), + pollingLabel: z.string().optional(), + sourceHashLabel: z.string().optional(), + destHashLabel: z.string().optional(), + successTitle: z.string().optional(), + successBody: z.string().optional(), + quoteFailedError: z.string().optional(), + submitFailedError: z.string().optional(), + timeoutError: z.string().optional(), insufficientBalanceError: z.string().optional(), - invalidAddressError: z.string().optional(), - sendFailedError: z.string().optional(), - sentTitle: z.string().optional(), - sentBody: z.string().optional(), - hashLabel: z.string().optional(), - copyHashLabel: z.string().optional(), - hashCopiedLabel: z.string().optional(), - hashCopyFailedLabel: z.string().optional(), + invalidAmountError: z.string().optional(), + noDestinationError: z.string().optional(), viewOnExplorerLabel: z.string().optional(), doneLabel: z.string().optional(), }) @@ -319,6 +349,7 @@ const balancesCopySchema = z.strictObject({ receiveCopyFailedLabel: z.string().optional(), receiveCloseLabel: z.string().optional(), sendLabel: z.string().optional(), + bridgeLabel: z.string().optional(), }) .optional(); @@ -373,6 +404,7 @@ const copySchema = z.strictObject({ sendTransaction: sendTransactionCopySchema, confirmTransfer: confirmTransferCopySchema, transferTokens: transferTokensCopySchema, + cctpBridge: cctpBridgeCopySchema, grantExecutionPermission: grantExecutionPermissionCopySchema, cancelDelegation: cancelDelegationCopySchema, passkeyPrompt: passkeyPromptCopySchema, diff --git a/src/style/types.ts b/src/style/types.ts index a5f9d39..adeb8f3 100644 --- a/src/style/types.ts +++ b/src/style/types.ts @@ -158,6 +158,49 @@ export interface IStyleCopyTransferTokens { doneLabel: string; } +/** + * In-wallet CCTP USDC bridge modal (quote + confirm + Iris progress). + */ +export interface IStyleCopyCctpBridge { + title: string; + body: string; + amountLabel: string; + amountPlaceholder: string; + destinationLabel: string; + destinationPlaceholder: string; + speedLabel: string; + speedFastLabel: string; + speedSlowLabel: string; + recipientLabel: string; + recipientHint: string; + getQuoteLabel: string; + confirmLabel: string; + cancelLabel: string; + retryLabel: string; + transferAmountLabel: string; + forwardFeeLabel: string; + protocolFeeLabel: string; + cctpFeeLabel: string; + relayerFeeLabel: string; + totalBurnLabel: string; + netReceivedLabel: string; + quotingLabel: string; + submittingLabel: string; + pollingLabel: string; + sourceHashLabel: string; + destHashLabel: string; + successTitle: string; + successBody: string; + quoteFailedError: string; + submitFailedError: string; + timeoutError: string; + insufficientBalanceError: string; + invalidAmountError: string; + noDestinationError: string; + viewOnExplorerLabel: string; + doneLabel: string; +} + /** * EIP-7715 grant consent form (`wallet_requestExecutionPermissions`). * `body` supports `{domain}`, `{to}`, `{chainName}`, `{permissionType}`. @@ -352,6 +395,7 @@ export interface IStyleCopyBalances { receiveCopyFailedLabel: string; receiveCloseLabel: string; sendLabel: string; + bridgeLabel: string; } /** @@ -435,6 +479,8 @@ export interface IStyleCopyOptions { confirmTransfer?: Partial; /** Partial patch for the in-wallet transfer tokens modal */ transferTokens?: Partial; + /** Partial patch for the in-wallet CCTP USDC bridge modal */ + cctpBridge?: Partial; /** Partial patch for EIP-7715 grant consent */ grantExecutionPermission?: Partial; /** Partial patch for delegation cancel / revoke confirm */ @@ -486,6 +532,7 @@ export interface IResolvedCopy { sendTransaction: IStyleCopySendTransaction; confirmTransfer: IStyleCopyConfirmTransfer; transferTokens: IStyleCopyTransferTokens; + cctpBridge: IStyleCopyCctpBridge; grantExecutionPermission: IStyleCopyGrantExecutionPermission; cancelDelegation: IStyleCopyCancelDelegation; passkeyPrompt: IStyleCopyPasskeyPrompt; diff --git a/src/wallet/WalletProvider.tsx b/src/wallet/WalletProvider.tsx index 24840ed..bd539b6 100644 --- a/src/wallet/WalletProvider.tsx +++ b/src/wallet/WalletProvider.tsx @@ -30,12 +30,15 @@ import { CachedRelayerVaultRepository } from "../lib/implementations/data/Cached import type { AccountConnectStorage } from "../ows/registerAccountConnect"; import { RelayerCredentialsClient } from "../lib/implementations/data/utils/RelayerCredentialsClient"; import { HardcodedChainRepository } from "../lib/implementations/data/HardcodedChainRepository"; +import { CircleRepository } from "../lib/implementations/data/CircleRepository"; import { HardcodedKnownAssetRepository } from "../lib/implementations/data/HardcodedKnownAssetRepository"; import { LocalStorageTrackedAssetRepository } from "../lib/implementations/data/LocalStorageTrackedAssetRepository"; import { BlockscoutAssetActivityRepository } from "../lib/implementations/data/BlockscoutAssetActivityRepository"; import { OneshotRelayerRepository } from "../lib/implementations/data/OneshotRelayerRepository"; import { + BridgeService, BusinessTransactionUtils, + CCTPUtils, DelegationService, TransactionService, } from "../lib/implementations/business"; @@ -57,15 +60,18 @@ import { import type { IAssetActivityRepository, IChainRepository, + ICircleRepository, IKnownAssetRepository, IOneshotRelayerRepository, IRecordSentActivityParams, ITrackedAssetRepository, } from "../lib/interfaces/data"; import type { + IBridgeService, IDelegationService, ITransactionService, } from "../lib/interfaces/business"; +import type { ICCTPUtils } from "../lib/interfaces/business/utils/ICCTPUtils"; import type { ICircleProvider, IConfigProvider, @@ -99,6 +105,7 @@ import { useWalletAssets } from "./useWalletAssets"; import { useWalletBoot } from "./useWalletBoot"; import { useWalletSessionStore } from "./sessionStore"; import { CircleContextProvider } from "../circle/CircleContext"; +import { openCctpBridge } from "../circle/openCctpBridge"; /** Filled once the Signing Layer iframe finishes loading / wallet handshake. */ const configProvider: IConfigProvider = new ConfigProvider(); const circleProvider: ICircleProvider = new CircleProvider(configProvider); @@ -130,6 +137,7 @@ const oneshotRelayerRepository: IOneshotRelayerRepository = blockchain: blockchainProvider, owsProvider, }); +const circleRepository: ICircleRepository = new CircleRepository(); const businessTransactionUtils = new BusinessTransactionUtils({ chainRepository, @@ -139,12 +147,23 @@ const businessTransactionUtils = new BusinessTransactionUtils({ owsProvider, }); +const cctpUtils: ICCTPUtils = new CCTPUtils(); + const transactionService: ITransactionService = new TransactionService({ chainRepository, relayerRepository: oneshotRelayerRepository, transactionUtils: businessTransactionUtils, }); +const bridgeService: IBridgeService = new BridgeService( + chainRepository, + knownAssetRepository, + circleRepository, + businessTransactionUtils, + cctpUtils, + blockchainProvider, +); + const relayerCredentialsClient = new RelayerCredentialsClient({ configProvider, owsProvider, @@ -193,6 +212,7 @@ export type WalletContextValue = { assetActivityRepository: IAssetActivityRepository; oneshotRelayerRepository: IOneshotRelayerRepository; transactionService: ITransactionService; + bridgeService: IBridgeService; delegationService: IDelegationService; eventBus: IEventBus; @@ -336,6 +356,28 @@ export function WalletProvider({ children }: { children: ReactNode }) { }); }, [refreshAllowedChains]); + const evmAddress = useWalletSessionStore((state) => state.evmAddress); + const unlocked = useWalletSessionStore((state) => state.unlocked); + useEffect(() => { + if (!unlocked || !evmAddress || String(evmAddress).toLowerCase() === "0x0") { + return; + } + let cancelled = false; + void bridgeService.resume(evmAddress).then((inFlight) => { + if (cancelled || !inFlight) return; + void openCctpBridge({ + sourceChainId: inFlight.sourceChainId, + ownerAddress: evmAddress, + resume: inFlight, + }).catch(() => { + /* user closed resume modal */ + }); + }); + return () => { + cancelled = true; + }; + }, [evmAddress, unlocked]); + const { setUnlocked, refreshAddresses, @@ -422,6 +464,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { transactionService, delegationService, transactionUtils, + cctpUtils, credentialRepository, walletStorage, eventBus, @@ -658,6 +701,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { assetActivityRepository, oneshotRelayerRepository, transactionService, + bridgeService, delegationService, eventBus, chains, diff --git a/src/wallet/modalTypes.ts b/src/wallet/modalTypes.ts index 967f5a3..e1b9c10 100644 --- a/src/wallet/modalTypes.ts +++ b/src/wallet/modalTypes.ts @@ -16,6 +16,10 @@ import type { import type { ISiweFields } from "../lib/types/domain/SiweFields"; import type { IAddAssetApprovalRequest } from "./registerAddAsset"; import type { IOnrampOpenRequest } from "../circle/onrampTypes"; +import type { + ICctpBridgeModalResult, + ICctpBridgeOpenRequest, +} from "../circle/cctpBridgeTypes"; export type WalletSetupChoice = "login" | "create" | "import" | "cancel"; @@ -190,6 +194,13 @@ export type ModalRequest = request: IOnrampOpenRequest; resolve: () => void; reject: (error: unknown) => void; + } + | { + id: string; + kind: "cctpBridge"; + request: ICctpBridgeOpenRequest; + resolve: (result: ICctpBridgeModalResult) => void; + reject: (error: unknown) => void; }; export type ActiveModal = ModalRequest; diff --git a/src/wallet/registerBridge.ts b/src/wallet/registerBridge.ts new file mode 100644 index 0000000..f5e9911 --- /dev/null +++ b/src/wallet/registerBridge.ts @@ -0,0 +1,145 @@ +import { z } from "zod"; +import type { OWSWallet } from "@1shotapi/ows-wallet-utils"; +import { + EVMChainId, + OwsInvalidParamsError, + OwsUserRejectedError, + type EVMAccountAddress, + type EVMChainId as EVMChainIdType, +} from "@1shotapi/ows-types"; +import { parseUnits } from "viem"; +import { openCctpBridge } from "../circle/openCctpBridge"; +import type { IChainRepository } from "../lib/interfaces/data/IChainRepository"; +import type { IKnownAssetRepository } from "../lib/interfaces/data/IKnownAssetRepository"; +import type { ICCTPUtils } from "../lib/interfaces/business/utils/ICCTPUtils"; + +/** Custom RPC — host: `await proxy.rpc("bridge", { amount?, sourceChainId?, destinationChainId? })`. */ +export const BRIDGE_RPC_METHOD = "bridge"; + +const bridgeParamsSchema = z + .strictObject({ + amount: z.string().min(1).optional(), + sourceChainId: z.number().int().positive().optional(), + destinationChainId: z.number().int().positive().optional(), + }) + .default({}); + +export type IBridgeParams = z.infer; + +export type RegisterBridgeOptions = { + getOwnerAddress: () => EVMAccountAddress | null; + getSessionChainId: () => EVMChainIdType; + chainRepository: IChainRepository; + knownAssetRepository: IKnownAssetRepository; + cctpUtils: ICCTPUtils; +}; + +export function evmChainIdFromDecimal(decimal: number): EVMChainIdType { + return EVMChainId(`0x${decimal.toString(16)}`); +} + +/** Host `sourceChainId` is decimal; omit → the current session chain. */ +export function resolveBridgeSourceChainId( + sourceChainIdDecimal: number | undefined, + sessionChainId: EVMChainIdType, +): EVMChainIdType { + if (sourceChainIdDecimal === undefined) { + return sessionChainId; + } + return evmChainIdFromDecimal(sourceChainIdDecimal); +} + +/** + * Register host `bridge` RPC — opens the CCTP USDC bridge for the unlocked + * EVM address on a relayer CCTP source chain. + */ +export function registerBridgeRpc( + wallet: OWSWallet, + options: RegisterBridgeOptions, +): void { + wallet.registerRpc( + BRIDGE_RPC_METHOD, + async (params) => { + const { amount, sourceChainId, destinationChainId } = + params as IBridgeParams; + const owner = options.getOwnerAddress(); + if (!owner) { + throw new Error("Wallet is locked — unlock before bridge"); + } + + const sourceId = resolveBridgeSourceChainId( + sourceChainId, + options.getSessionChainId(), + ); + const sourceUsdc = + await options.knownAssetRepository.getCctpBridgeAsset(sourceId); + if (!sourceUsdc) { + throw new OwsInvalidParamsError( + `No CCTP USDC on source chain ${sourceId}`, + ); + } + + const sourceChain = await options.chainRepository.get(sourceId); + if (!sourceChain?.useRelayer) { + throw new OwsInvalidParamsError( + `Source chain ${sourceId} is not a relayer CCTP source`, + ); + } + + let destId: EVMChainIdType | undefined; + let amountAtoms: bigint | undefined; + if (destinationChainId !== undefined) { + destId = evmChainIdFromDecimal(destinationChainId); + const destChain = await options.chainRepository.get(destId); + if ( + !destChain || + !options.cctpUtils.isValidDestination(sourceChain, destChain) + ) { + throw new OwsInvalidParamsError( + "destinationChainId must be a same-network CCTP destination", + ); + } + } + if (amount !== undefined) { + try { + amountAtoms = parseUnits(amount, sourceUsdc.decimals); + } catch { + throw new OwsInvalidParamsError("amount must be a valid USDC amount"); + } + if (amountAtoms <= 0n) { + throw new OwsInvalidParamsError("amount must be greater than zero"); + } + } + + const display = await wallet.requestDisplay(); + try { + const result = await openCctpBridge({ + sourceChainId: sourceId, + ownerAddress: owner, + ...(amountAtoms !== undefined ? { amountAtoms } : {}), + ...(destId ? { destinationChainId: destId } : {}), + }); + return { + ok: true as const, + burnTxHash: result.burnTxHash, + ...(result.forwardTxHash + ? { forwardTxHash: result.forwardTxHash } + : {}), + }; + } catch (error: unknown) { + if ( + error instanceof OwsUserRejectedError || + (error instanceof Error && /reject/i.test(error.message)) + ) { + throw error instanceof OwsUserRejectedError + ? error + : new OwsUserRejectedError("User closed bridge"); + } + throw error; + } finally { + await display.hide(); + } + }, + bridgeParamsSchema, + ); +} diff --git a/src/wallet/useWalletBoot.ts b/src/wallet/useWalletBoot.ts index 70253e2..aa489e1 100644 --- a/src/wallet/useWalletBoot.ts +++ b/src/wallet/useWalletBoot.ts @@ -53,6 +53,7 @@ import type { ISIWEUtils, ITransactionUtils, } from "../lib/interfaces/utils"; +import type { ICCTPUtils } from "../lib/interfaces/business/utils/ICCTPUtils"; import { SIWEUtils } from "../lib/implementations/utils/SIWEUtils"; import type { SupportedChain } from "../lib/types/domain"; import { @@ -77,6 +78,7 @@ import { registerCreateAccountRpc } from "./registerCreateAccount"; import type { IPasskeyRegistrationResult } from "./registerCreateAccount"; import { registerFocusModeRpc } from "./registerFocusMode"; import { registerOnrampRpc } from "./registerOnramp"; +import { registerBridgeRpc } from "./registerBridge"; import { loadCachedEvmAddress, loadCredentialId } from "../storage"; import { pushModal } from "./pushModal"; import type { @@ -184,6 +186,7 @@ export interface IUseWalletBootParams { transactionService: ITransactionService; delegationService: IDelegationService; transactionUtils: ITransactionUtils; + cctpUtils: ICCTPUtils; credentialRepository: CachedRelayerVaultRepository; walletStorage: AccountConnectStorage; eventBus: IEventBus; @@ -213,6 +216,7 @@ export function useWalletBoot({ transactionService, delegationService, transactionUtils, + cctpUtils, credentialRepository, walletStorage, eventBus, @@ -475,6 +479,20 @@ export function useWalletBoot({ }, }); + registerBridgeRpc(wallet, { + getOwnerAddress: () => { + const address = useWalletSessionStore.getState().evmAddress; + if (!address || String(address).toLowerCase() === "0x0") { + return null; + } + return address; + }, + getSessionChainId: () => useWalletSessionStore.getState().chainId, + chainRepository, + knownAssetRepository, + cctpUtils, + }); + registerAddAssetRpc(wallet, { knownAssetRepository, trackedAssetRepository, diff --git a/test/lib/implementations/business/utils/CCTPUtils.test.ts b/test/lib/implementations/business/utils/CCTPUtils.test.ts new file mode 100644 index 0000000..4569737 --- /dev/null +++ b/test/lib/implementations/business/utils/CCTPUtils.test.ts @@ -0,0 +1,223 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { decodeFunctionData, erc20Abi, pad, padHex } from "viem"; +import { EVMAccountAddress, type EVMChainId } from "@1shotapi/ows-types"; +import { + CCTPUtils, + tokenMessengerV2Abi, +} from "@/lib/implementations/business/utils/CCTPUtils.ts"; +import { SupportedChain } from "@/lib/types/domain/SupportedChain.ts"; +import { EChain } from "@/lib/types/enum/EChain.ts"; +import { EChainNetworkType } from "@/lib/types/enum/EChainNetworkType.ts"; +import { ECircleDomainId } from "@/lib/types/enum/ECircleDomainId.ts"; +import { ECctpTransferSpeed } from "@/lib/types/enum/ECctpTransferSpeed.ts"; + +const cctp = new CCTPUtils(); + +const owner = EVMAccountAddress("0x1111111111111111111111111111111111111111"); +const usdc = EVMAccountAddress("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); +const messenger = EVMAccountAddress( + "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", +); + +function chain( + id: EVMChainId, + network: EChainNetworkType, + dest: boolean, + useRelayer = true, +): SupportedChain { + return new SupportedChain( + id, + network, + "https://relayer.example", + useRelayer, + "/logo.png", + true, + "https://rpc.example", + String(id), + "https://explorer.example", + dest, + ); +} + +describe("CCTPUtils", () => { + describe("routes", () => { + it("maps Base mainnet to Circle domain 6 and mainnet Iris", () => { + const route = cctp.getRoute(EChain.Base); + assert.ok(route); + assert.equal(route.domain, ECircleDomainId.Base); + assert.equal(route.networkType, EChainNetworkType.Mainnet); + assert.equal(route.irisBaseUrl, cctp.irisApiMainnet); + }); + + it("maps Sepolia to Ethereum domain 0 and sandbox Iris", () => { + const route = cctp.getRoute(EChain.Sepolia); + assert.ok(route); + assert.equal(route.domain, ECircleDomainId.Ethereum); + assert.equal(route.networkType, EChainNetworkType.Testnet); + assert.equal(route.irisBaseUrl, cctp.irisApiTestnet); + }); + + it("maps Arc Testnet to Arc domain 26", () => { + const route = cctp.requireRoute(EChain.ArcTestnet); + assert.equal(route.domain, ECircleDomainId.Arc); + assert.equal(route.networkType, EChainNetworkType.Testnet); + }); + + it("returns null for Arc mainnet (not a CCTP wallet source)", () => { + assert.equal(cctp.getRoute(EChain.Arc), null); + }); + + it("maps Fast to finality 1000 and Slow to 2000", () => { + assert.equal( + cctp.finalityThresholdForSpeed(ECctpTransferSpeed.Fast), + cctp.fastFinalityThreshold, + ); + assert.equal( + cctp.finalityThresholdForSpeed(ECctpTransferSpeed.Slow), + cctp.slowFinalityThreshold, + ); + }); + }); + + describe("fee math", () => { + it("adds forwardFee.med and protocol fee into totalBurn", () => { + const amount = 10_000_000n; // 10 USDC + const fees = cctp.computeBurnFees(amount, { + finalityThreshold: 1000, + minimumFee: 1, + forwardFee: { low: "1000", med: "8000", high: "12000" }, + }); + // protocolFee = amount * round(1 * 100) / 1_000_000 = 10_000_000 * 100 / 1e6 = 1000 + assert.equal(fees.forwardFee, 8000n); + assert.equal(fees.protocolFee, 1000n); + assert.equal(fees.maxFee, 9000n); + assert.equal(fees.totalBurn, 10_009_000n); + }); + + it("uses zero protocol fee when minimumFee is 0", () => { + const fees = cctp.computeBurnFees(5_000_000n, { + finalityThreshold: 2000, + minimumFee: 0, + forwardFee: { low: "0", med: "2500", high: "4000" }, + }); + assert.equal(fees.protocolFee, 0n); + assert.equal(fees.maxFee, 2500n); + assert.equal(fees.totalBurn, 5_002_500n); + }); + }); + + describe("encoding", () => { + it("encodes depositForBurnWithHook with padded recipient and cctp-forward hook", () => { + const data = cctp.encodeDepositForBurnWithHook({ + totalBurn: 10_008_000n, + destDomain: ECircleDomainId.Arc, + mintRecipient: owner, + burnToken: usdc, + maxFee: 8000n, + minFinalityThreshold: 1000, + }); + const decoded = decodeFunctionData({ + abi: tokenMessengerV2Abi, + data, + }); + assert.equal(decoded.functionName, "depositForBurnWithHook"); + assert.deepEqual(decoded.args, [ + 10_008_000n, + ECircleDomainId.Arc, + pad(owner, { size: 32 }), + usdc, + padHex("0x", { size: 32 }), + 8000n, + 1000, + cctp.forwardHookData, + ]); + assert.equal( + cctp.forwardHookData, + "0x636374702d666f72776172640000000000000000000000000000000000000000", + ); + }); + + it("skips the approve work item when allowance covers totalBurn", () => { + const approveData = cctp.encodeUsdcApprove(messenger, 100n); + const burnData = cctp.encodeDepositForBurnWithHook({ + totalBurn: 100n, + destDomain: ECircleDomainId.Base, + mintRecipient: owner, + burnToken: usdc, + maxFee: 1n, + minFinalityThreshold: 1000, + }); + assert.equal(cctp.shouldSkipUsdcApprove(100n, 100n), true); + const skipped = cctp.buildRelayerWork({ + allowance: 100n, + totalBurn: 100n, + usdcAddress: usdc, + tokenMessenger: messenger, + approveData, + burnData, + }); + assert.equal(skipped.length, 1); + assert.equal(skipped[0]?.to, messenger); + + const needed = cctp.buildRelayerWork({ + allowance: 99n, + totalBurn: 100n, + usdcAddress: usdc, + tokenMessenger: messenger, + approveData, + burnData, + }); + assert.equal(needed.length, 2); + assert.equal(needed[0]?.to, usdc); + const approveDecoded = decodeFunctionData({ + abi: erc20Abi, + data: needed[0]?.data ?? "0x", + }); + assert.equal(approveDecoded.functionName, "approve"); + }); + }); + + describe("destinations", () => { + const base = chain(EChain.Base, EChainNetworkType.Mainnet, true); + const ethereum = chain(EChain.Ethereum, EChainNetworkType.Mainnet, true); + const sepolia = chain(EChain.Sepolia, EChainNetworkType.Testnet, true); + const bsc = chain(EChain.Bsc, EChainNetworkType.Mainnet, false); + const arcMainnet = chain(EChain.Arc, EChainNetworkType.Mainnet, false, false); + + it("allows a same-network CCTP destination other than source", () => { + assert.equal(cctp.isValidDestination(base, ethereum), true); + }); + + it("rejects the source chain, other networks, and non-CCTP dests", () => { + assert.equal(cctp.isValidDestination(base, base), false); + assert.equal(cctp.isValidDestination(base, sepolia), false); + assert.equal(cctp.isValidDestination(base, bsc), false); + assert.equal(cctp.isValidDestination(base, arcMainnet), false); + }); + + it("filters the catalog to valid destinations", () => { + const dests = cctp.listDestinations( + [base, ethereum, sepolia, bsc, arcMainnet], + base, + ); + assert.deepEqual( + dests.map((entry) => String(entry.chainId)), + [String(EChain.Ethereum)], + ); + }); + }); + + describe("contracts", () => { + it("returns shared mainnet TokenMessengerV2 for Base", () => { + const contracts = cctp.getContracts( + ECircleDomainId.Base, + EChainNetworkType.Mainnet, + ); + assert.equal( + String(contracts.tokenMessengerV2).toLowerCase(), + "0x28b5a0e9c621a5badaa536219b3a228c8168cf5d", + ); + }); + }); +}); diff --git a/test/lib/implementations/data/CircleRepository.test.ts b/test/lib/implementations/data/CircleRepository.test.ts new file mode 100644 index 0000000..5bdfb05 --- /dev/null +++ b/test/lib/implementations/data/CircleRepository.test.ts @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { parseFeeRow } from "@/lib/implementations/data/CircleRepository.ts"; + +describe("CircleRepository Iris fee parsing", () => { + it("accepts numeric forwardFee atoms from Iris sandbox", () => { + const fee = parseFeeRow({ + finalityThreshold: 1000, + minimumFee: 0, + forwardFee: { low: 53982, med: 53982, high: 54294 }, + }); + assert.ok(fee); + assert.equal(fee.finalityThreshold, 1000); + assert.equal(fee.minimumFee, 0); + assert.equal(fee.forwardFee.med, "53982"); + assert.equal(fee.forwardFee.low, "53982"); + assert.equal(fee.forwardFee.high, "54294"); + }); + + it("accepts string forwardFee atoms", () => { + const fee = parseFeeRow({ + finalityThreshold: 2000, + minimumFee: 0.001, + forwardFee: { low: "100", med: "200", high: "300" }, + }); + assert.ok(fee); + assert.equal(fee.forwardFee.med, "200"); + }); + + it("returns null when med is missing", () => { + assert.equal( + parseFeeRow({ + finalityThreshold: 1000, + minimumFee: 0, + forwardFee: { low: 1, high: 2 }, + }), + null, + ); + }); +}); diff --git a/test/lib/implementations/data/cctpPersist.test.ts b/test/lib/implementations/data/cctpPersist.test.ts new file mode 100644 index 0000000..0b46bca --- /dev/null +++ b/test/lib/implementations/data/cctpPersist.test.ts @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + EVMAccountAddress, + EVMTransactionHash, +} from "@1shotapi/ows-types"; +import { EChain } from "@/lib/types/enum/EChain.ts"; +import { ECircleDomainId } from "@/lib/types/enum/ECircleDomainId.ts"; +import { + parseInFlight, + serializeInFlight, +} from "@/lib/implementations/data/CircleRepository.ts"; +import type { ICctpInFlightBurn } from "@/lib/interfaces/data/ICircleRepository.ts"; + +describe("CCTP in-flight persist/resume", () => { + const record: ICctpInFlightBurn = { + burnTxHash: EVMTransactionHash( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ), + sourceDomain: ECircleDomainId.Base, + sourceChainId: EChain.Base, + destChainId: EChain.Ethereum, + amountAtoms: 10_000_000n, + address: EVMAccountAddress("0x1111111111111111111111111111111111111111"), + }; + + it("round-trips burn hash, domains, and amount atoms", () => { + const restored = parseInFlight(serializeInFlight(record)); + assert.ok(restored); + assert.equal(restored.burnTxHash, record.burnTxHash); + assert.equal(restored.sourceDomain, ECircleDomainId.Base); + assert.equal(restored.sourceChainId, EChain.Base); + assert.equal(restored.destChainId, EChain.Ethereum); + assert.equal(restored.amountAtoms, 10_000_000n); + assert.equal(String(restored.address).toLowerCase(), String(record.address)); + }); + + it("returns null for malformed JSON", () => { + assert.equal(parseInFlight("{"), null); + assert.equal(parseInFlight("{}"), null); + assert.equal(parseInFlight(JSON.stringify({ burnTxHash: "nope" })), null); + }); +}); diff --git a/test/wallet/registerBridge.test.ts b/test/wallet/registerBridge.test.ts new file mode 100644 index 0000000..ee7c7ee --- /dev/null +++ b/test/wallet/registerBridge.test.ts @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + evmChainIdFromDecimal, + resolveBridgeSourceChainId, +} from "@/wallet/registerBridge.ts"; +import { EChain } from "@/lib/types/enum/EChain.ts"; + +describe("resolveBridgeSourceChainId", () => { + const session = EChain.Base; + + it("defaults to the session chain when sourceChainId is omitted", () => { + assert.equal(resolveBridgeSourceChainId(undefined, session), session); + }); + + it("converts a decimal host chain id to branded hex", () => { + assert.equal(evmChainIdFromDecimal(8453), EChain.Base); + assert.equal(resolveBridgeSourceChainId(1, session), EChain.Ethereum); + }); +}); From 8a95c98304172810d8f198fc869e8aeab407b3c1 Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Thu, 27 Aug 2026 18:27:46 -0700 Subject: [PATCH 06/11] Add feature toggle for Arc mainnet integration - Introduced a new module `features.ts` to manage the `EnableArcMainnet` flag. - Updated various components to conditionally render UI elements based on the `EnableArcMainnet` state, including chain options, wallet actions, and asset details. - Refactored `HardcodedChainRepository` to set the default chain based on the feature flag. - Ensured that the integration is in sync with the wallet package for future updates. --- host/src/components/WalletActions.tsx | 33 ++++++++---- host/src/components/hostChains.ts | 52 ++++++++++++------- host/src/features.ts | 6 +++ host/src/styleForm.ts | 4 +- src/components/AssetDetails.tsx | 19 ++++--- src/lib/features.ts | 5 ++ .../data/HardcodedChainRepository.ts | 9 ++-- 7 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 host/src/features.ts create mode 100644 src/lib/features.ts diff --git a/host/src/components/WalletActions.tsx b/host/src/components/WalletActions.tsx index 35e651c..8609b67 100644 --- a/host/src/components/WalletActions.tsx +++ b/host/src/components/WalletActions.tsx @@ -12,6 +12,7 @@ import { import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import type { SignMode } from "@/constants/signDemo"; +import { EnableArcMainnet } from "@/features"; import { HOST_CHAINS, hostChainMeta, @@ -410,20 +411,30 @@ export function WalletActions({
- +

- Open Circle fiat onramp via onramp, or gasless CCTP USDC - bridge via bridge. + {EnableArcMainnet ? ( + <> + Open Circle fiat onramp via onramp, or gasless CCTP + USDC bridge via bridge. + + ) : ( + <> + Open gasless CCTP USDC bridge via bridge. + + )}

- + {EnableArcMainnet ? ( + + ) : null}