diff --git a/.agents/skills/oneshot-embedded-wallet/SKILL.md b/.agents/skills/oneshot-embedded-wallet/SKILL.md index a64796e..fb6f735 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 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: @@ -16,6 +16,8 @@ metadata: Teach an agent how to embed the **1Shot Wallet** Branding Layer from a Host Layer app using `@1shotapi/ows-provider`. +> Use of 1Shot-hosted public infrastructure is subject to the [Public Infrastructure Terms](https://1shotapi.com/legal/public-infrastructure-terms) and [Acceptable Use Policy](https://1shotapi.com/legal/acceptable-use-policy). By accessing or using those hosted services, you agree to those terms. + ``` Host (your dapp) @1shotapi/ows-provider → OWSProxy └── Branding iframe https://wallet.1shotapi.com/ @@ -84,7 +86,7 @@ await proxy.rpc("configure", options); | `features.disableCredentials` | boolean | Hide Credentials tab; default `false`. Host credential flows still work | | `features.disableDelegations` | boolean | Hide Delegations tab; default `false`. Host delegation flows still work | | `features.allowedChains` | `string[]` (hex `0x…` chain ids) | Restrict Network dropdown to these catalog chains; omit or `[]` ⇒ all enabled | -| `destinationUrl` | string \| null | URL to receive transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/relayer/get-started/overview) (≤256 chars). `null` or `""` clears | +| `destinationUrl` | string \| null | URL to receive transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/embedded-wallet/webhooks) (≤256 chars). `null` or `""` clears | | `copy.productName` | string | titles / chrome | | `copy.tagline` | string | supporting line | | `copy.connect.title` | string | connect modal title | @@ -195,7 +197,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(); @@ -220,7 +222,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(); @@ -236,7 +238,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"); @@ -250,6 +252,43 @@ 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. + +## 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 | @@ -257,6 +296,8 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" }); | `proxy.ethereum.request(...)` | EIP-1193 (accounts, sign, chain, …) | | `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`, `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`, …) | @@ -308,3 +349,5 @@ include a live Analytics panel fed by `proxy.analytics.on` (filter by `name`). - Theme with `configure`; 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. +- Use `bridge` (or the in-wallet Bridge button) for gasless CCTP USDC; do not require native gas or dest-chain `receiveMessage`. diff --git a/.github/workflows/Deploy Dev.yaml b/.github/workflows/Deploy Dev.yaml index c227225..0da3bff 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 cd084b4..f5e10c6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +.DS_Store # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json @@ -154,3 +155,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 71873a1..33d9b4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,11 +20,15 @@ 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`). Browser extension: `extension/` (`npm run dev:extension`) — see [`extension/README.md`](extension/README.md). Branding / host config via Host RPC `configure`, 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). + +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. @@ -48,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/Dockerfile b/Dockerfile index e6d675a..78f54aa 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/LICENSE b/LICENSE index 261eeb9..37ec93a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,190 +1,180 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. Copyright [yyyy] [name of copyright owner] @@ -192,7 +182,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..967c737 --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +1Shot +Copyright 2026 1Shot API, Inc. + +This product includes software developed by 1Shot API, Inc. diff --git a/README.md b/README.md index a465c0a..1b5a36b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This repo is the **OWS Branding Layer** for the 1Shot Wallet — a frontend-only static app (React + Vite + Tailwind + shadcn/ui) hosted at `wallet.1shotapi.com`. +> Use of 1Shot-hosted public infrastructure is subject to the [Public Infrastructure Terms](https://1shotapi.com/legal/public-infrastructure-terms) and [Acceptable Use Policy](https://1shotapi.com/legal/acceptable-use-policy). By accessing or using those hosted services, you agree to those terms. + ``` Host Layer (integrator dapp) └── This app / Branding Layer (@1shotapi/ows-wallet-utils) @@ -27,11 +29,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/extension/README.md b/extension/README.md index 1c5c39f..98b249f 100644 --- a/extension/README.md +++ b/extension/README.md @@ -4,6 +4,8 @@ MetaMask-style host for the 1Shot Branding Layer: a thin MAIN-world EIP-1193 / E This avoids page `frame-src` CSP, hostile host CSS, and per-dApp third-party storage partitions. +> Use of 1Shot-hosted public infrastructure is subject to the [Public Infrastructure Terms](https://1shotapi.com/legal/public-infrastructure-terms) and [Acceptable Use Policy](https://1shotapi.com/legal/acceptable-use-policy). By accessing or using those hosted services, you agree to those terms. + ## Requirements - Node.js 22+ diff --git a/host/src/components/WalletActions.tsx b/host/src/components/WalletActions.tsx index 0fe7936..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, @@ -57,6 +58,8 @@ export interface IWalletActionsProps { onUnfocusWallet: () => void; onAddUsdcArc: () => void; onAddUsdtBase: () => void; + onOnramp: () => void; + onBridge: () => void; /** In-memory grants from this session (`wallet_requestExecutionPermissions`). */ sessionGrants: ReadonlyArray<{ id: string; @@ -106,6 +109,8 @@ export function WalletActions({ onUnfocusWallet, onAddUsdcArc, onAddUsdtBase, + onOnramp, + onBridge, sessionGrants, delegationsOutput, onRequestDelegation, @@ -405,6 +410,42 @@ export function WalletActions({ +
+ +

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

+
+ {EnableArcMainnet ? ( + + ) : null} + +
+
+
diff --git a/host/src/components/WalletConfigurator.tsx b/host/src/components/WalletConfigurator.tsx index ee7a06a..8e41b7e 100644 --- a/host/src/components/WalletConfigurator.tsx +++ b/host/src/components/WalletConfigurator.tsx @@ -97,7 +97,7 @@ export function WalletConfigurator({

URL to receive transaction status update webhooks from the{" "} 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 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 handleBridge = () => { + 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; @@ -842,6 +886,8 @@ export function useHostTestActions({ onUnfocusWallet: handleUnfocusWallet, 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 7532092..a8463e7 100644 --- a/host/src/styleForm.ts +++ b/host/src/styleForm.ts @@ -1,3 +1,5 @@ +import { EnableArcMainnet } from "./features"; + /** Flat form state for the host style playground. */ export interface IStyleFormState { // Basic @@ -32,7 +34,7 @@ export interface IStyleFormState { /** * Status webhook URL for the 1Shot Relayer (`destinationUrl` on configure). * Empty ⇒ omit / clear when applying a full features payload. - * @see https://1shotapi.com/docs/relayer/get-started/overview + * @see https://1shotapi.com/docs/embedded-wallet/webhooks */ destinationUrl: string; @@ -143,6 +145,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; @@ -200,6 +209,7 @@ export const CATALOG_CHAIN_OPTIONS: ReadonlyArray<{ chainId: string; label: string; }> = [ + ...(EnableArcMainnet ? [{ chainId: "0x13b2", label: "Arc" }] : []), { chainId: "0x4cef52", label: "Arc Testnet" }, { chainId: "0xaa36a7", label: "Sepolia" }, { chainId: "0x14a34", label: "Base Sepolia" }, @@ -326,6 +336,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.", @@ -480,6 +498,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.", @@ -752,8 +778,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/package-lock.json b/package-lock.json index 6aea50e..eb43397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@1shotapi/ows-signer-utils": "^0.5.1", "@1shotapi/ows-types": "^0.5.1", "@1shotapi/ows-wallet-utils": "^0.4.1", + "@crcl-main/app-kit": "^1.10.0-canary-feature-onramp-kit-sdk.1784835391", "@fontsource-variable/geist": "^5.2.9", "@metamask/smart-accounts-kit": "^2.0.0", "@reown/walletkit": "^1.5.6", @@ -888,6 +889,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", @@ -998,18 +1008,688 @@ "linux" ] }, - "node_modules/@cbor-extract/cbor-extract-win32-x64": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.2.tgz", - "integrity": "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==", - "cpu": [ - "x64" - ], + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.2.tgz", + "integrity": "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "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/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", + "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/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", + "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/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", + "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/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", + "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/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", + "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/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", + "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", - "optional": true, - "os": [ - "win32" - ] + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } }, "node_modules/@dotenvx/dotenvx": { "version": "1.75.1", @@ -1780,70 +2460,483 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", - "dev": true, - "license": "Apache-2.0", - "peer": true, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@ethereumjs/common": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", + "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", + "license": "MIT", + "dependencies": { + "@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": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "@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/@ethereumjs/common": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", - "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", + "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": { - "@ethereumjs/util": "^8.1.0", - "crc-32": "^1.2.0" + "@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": { @@ -3649,6 +4742,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", @@ -5722,6 +6821,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", @@ -6115,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", @@ -6186,7 +7415,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" @@ -6222,12 +7450,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", @@ -6393,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", @@ -6827,6 +8085,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", @@ -7014,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", @@ -7052,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", @@ -7103,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", @@ -7127,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", @@ -7146,27 +8463,83 @@ "url": "https://opencollective.com/browserslist" }, { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "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": "github", - "url": "https://github.com/sponsors/ai" + "type": "consulting", + "url": "https://feross.org/support" } ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" + "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 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=6.14.2" } }, "node_modules/bundle-name": { @@ -7778,6 +9151,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", @@ -8001,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", @@ -8222,6 +9616,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", @@ -8356,6 +9771,21 @@ "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", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -8959,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", @@ -8997,6 +10435,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.5", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", @@ -9388,6 +10832,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", @@ -9417,10 +10871,21 @@ "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.13.1", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", - "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", + "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" @@ -9574,6 +11039,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", @@ -9596,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", @@ -10089,6 +11583,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", @@ -10104,6 +11607,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", @@ -10123,6 +11710,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", @@ -10197,6 +11790,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", @@ -10818,6 +12417,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", @@ -10907,9 +12518,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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", @@ -10951,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", @@ -12511,6 +14154,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", @@ -12546,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", @@ -12945,6 +14653,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", @@ -13098,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", @@ -13191,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", @@ -13261,6 +14995,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", @@ -13391,7 +15137,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", @@ -13455,7 +15200,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": { @@ -13810,6 +15554,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", @@ -14008,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", @@ -14015,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", diff --git a/package.json b/package.json index 4d579c4..bed1510 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint": "tsc -p tsconfig.json --noEmit", "test": "tsx --tsconfig tsconfig.test.json --test \"test/**/*.test.ts\"", "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": { @@ -30,6 +30,7 @@ "@1shotapi/ows-signer-utils": "^0.5.1", "@1shotapi/ows-types": "^0.5.1", "@1shotapi/ows-wallet-utils": "^0.4.1", + "@crcl-main/app-kit": "^1.10.0-canary-feature-onramp-kit-sdk.1784835391", "@fontsource-variable/geist": "^5.2.9", "@metamask/smart-accounts-kit": "^2.0.0", "@reown/walletkit": "^1.5.6", diff --git a/skills/oneshot-embedded-wallet/SKILL.md b/skills/oneshot-embedded-wallet/SKILL.md index bd2692f..5a47763 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 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: @@ -16,6 +16,8 @@ metadata: Teach an agent how to embed the **1Shot Wallet** Branding Layer from a Host Layer app using `@1shotapi/ows-provider`. +> Use of 1Shot-hosted public infrastructure is subject to the [Public Infrastructure Terms](https://1shotapi.com/legal/public-infrastructure-terms) and [Acceptable Use Policy](https://1shotapi.com/legal/acceptable-use-policy). By accessing or using those hosted services, you agree to those terms. + ``` Host (your dapp) @1shotapi/ows-provider → OWSProxy └── Branding iframe https://wallet.1shotapi.com/ @@ -84,7 +86,7 @@ await proxy.rpc("configure", options); | `features.disableCredentials` | boolean | Hide Credentials tab; default `false`. Host credential flows still work | | `features.disableDelegations` | boolean | Hide Delegations tab; default `false`. Host delegation flows still work | | `features.allowedChains` | `string[]` (hex `0x…` chain ids) | Restrict Network dropdown to these catalog chains; omit or `[]` ⇒ all enabled | -| `destinationUrl` | string \| null | URL to receive transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/relayer/get-started/overview) (≤256 chars). `null` or `""` clears | +| `destinationUrl` | string \| null | URL to receive transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/embedded-wallet/webhooks) (≤256 chars). `null` or `""` clears | | `copy.productName` | string | titles / chrome | | `copy.tagline` | string | supporting line | | `copy.connect.title` | string | connect modal title | @@ -195,7 +197,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(); @@ -220,7 +222,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(); @@ -236,7 +238,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"); @@ -250,15 +252,64 @@ 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. + +## 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 | |-----|-----| | `proxy.ethereum.request(...)` | EIP-1193 (accounts, sign, chain, …) | +| `proxy.ethereum.on` / `removeListener` | Branding→Host EIP-1193 notifications (`chainChanged`, `accountsChanged` via `ows:eip1193`) | | `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`, …) | +| `proxy.rpc(method, params)` | Custom Branding RPC (`configure`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, `bridge`, …) | + +Subscribe so in-wallet chain/account changes update host UI without polling: + +```typescript +proxy.ethereum.on("chainChanged", (chainId) => { + // hex chain id string +}); +proxy.ethereum.on("accountsChanged", (accounts) => { + // EVM address array +}); +``` ## Analytics (`proxy.analytics`) @@ -289,6 +340,17 @@ The same rich payload is POSTed fire-and-forget to `POST /wallet/product-events` 1Shot relayer. The local Host (`host/`) and marketing [wallet playground](https://www.1shotapi.com/playground) include a live Analytics panel fed by `proxy.analytics.on` (filter by `name`). +## Relayer integration (when the host submits txs) + +- **Default sends:** `eth_sendTransaction` through OWSProxy — the wallet signs delegations and calls `relayer_*` internally. The host does **not** implement a relayer JSON-RPC client. +- **Delegated execution (Path B):** when the host or backend will **redeem** a user grant via public relayer JSON-RPC, also install the **`public-relayer`** skill. + - **B1 direct:** grant **`to: relayer targetAddress`** → Example 0b in **`public-relayer/references/examples.md`**. + - **B2 session key (recommended):** grant **`to: host session account`**, redelegate **`to: targetAddress`**, submit delegation chain → Example 0c. + - See **`public-relayer/SKILL.md`** (Integration paths with `1shot-wallet`). +- **Status webhooks:** optional `configure.destinationUrl` — the wallet forwards it to the relayer on send. Still no direct relayer client in the host. + +EIP-7715 host RPCs: `wallet_requestExecutionPermissions`, `wallet_revokeExecutionPermission` (grant consent and on-chain revoke are wallet-driven). + ## Hard rules - Never embed the Signing Layer iframe from the Host — always Host → Branding → Signing. @@ -296,3 +358,5 @@ include a live Analytics panel fed by `proxy.analytics.on` (filter by `name`). - Theme with `configure`; 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. +- 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/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/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/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/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/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 65ba4d6..7e5518b 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, @@ -13,11 +14,13 @@ 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 { openCctpBridge } from "../circle/openCctpBridge"; +import { EnableArcMainnet } from "../lib/features"; import { AssetIdentityMark } from "./AssetIdentityMark"; 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 { @@ -31,7 +34,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, @@ -40,7 +43,9 @@ 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 [bridgeBusy, setBridgeBusy] = useState(false); + const [canBridge, setCanBridge] = useState(false); const { balance, decimals } = useLiveTrackedBalance( assetProp.id, @@ -65,6 +70,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({ @@ -73,12 +90,55 @@ 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]); + + 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 (

@@ -156,9 +229,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 8d1d9c3..ea25f74 100644 --- a/src/components/ModalHost.tsx +++ b/src/components/ModalHost.tsx @@ -20,6 +20,8 @@ import { ImportPrivateKeyModal } from "./modals/ImportPrivateKeyModal"; 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"; @@ -144,6 +146,24 @@ export function ModalHost() { onResolve={activeModal.resolve} /> ); + case "onramp": + return ( + activeModal.resolve()} + /> + ); + case "cctpBridge": + return ( + + ); 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/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/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/features.ts b/src/lib/features.ts new file mode 100644 index 0000000..87d8bbf --- /dev/null +++ b/src/lib/features.ts @@ -0,0 +1,5 @@ +/** + * Flip to true when Circle clears Arc mainnet + Onramp for production. + * Then delete this module and all call sites. + */ +export const EnableArcMainnet = false; 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 e428620..4cfc49c 100644 --- a/src/lib/implementations/data/HardcodedChainRepository.ts +++ b/src/lib/implementations/data/HardcodedChainRepository.ts @@ -1,10 +1,11 @@ import { - EVMChainId, type EVMAccountAddress, type EVMChainId as EVMChainIdType, } from "@1shotapi/ows-types"; +import { EnableArcMainnet } from "../../features"; 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 +34,19 @@ const ALCHEMY_KEY = "jqLUTbHeN_cVsIX2W7tJk"; */ const CATALOG: readonly SupportedChain[] = [ new SupportedChain( - EVMChainId("0x4cef52"), + EChain.Arc, + EChainNetworkType.Mainnet, + PRODUCTION_RELAYER_URL, + false, + arcLogo, + EnableArcMainnet, + `https://arc-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, + "Arc", + "https://explorer.arc.io", + false, + ), + new SupportedChain( + EChain.ArcTestnet, EChainNetworkType.Testnet, DEVELOPMENT_RELAYER_URL, true, @@ -42,9 +55,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, @@ -53,9 +67,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, @@ -64,9 +79,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, @@ -75,9 +91,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, @@ -86,9 +103,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, @@ -97,9 +115,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, @@ -108,9 +127,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, @@ -119,9 +139,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, @@ -130,9 +151,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, @@ -141,9 +163,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, @@ -152,9 +175,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, @@ -163,9 +187,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, @@ -174,9 +199,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, @@ -185,9 +211,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, @@ -196,11 +223,14 @@ 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 Testnet). */ -export const DEFAULT_CHAIN_ID = EVMChainId("0x4cef52"); +/** Default chain for a fresh session. */ +export const DEFAULT_CHAIN_ID = EnableArcMainnet + ? EChain.Arc + : EChain.ArcTestnet; 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/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 ab05634..a7049ea 100644 --- a/src/lib/implementations/utils/ConfigProvider.ts +++ b/src/lib/implementations/utils/ConfigProvider.ts @@ -11,6 +11,8 @@ const DEFAULT_TRACKED_ASSETS_STORAGE_KEY = "ows.tracked-assets.v2"; const DEFAULT_VAULT_STORAGE_KEY = "ows.vault.v1"; 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. @@ -32,6 +34,7 @@ export class ConfigProvider implements IConfigProvider { DEFAULT_VAULT_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 58676ab..aeb4a08 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/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/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 85f4e9e..0b3a70d 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/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/domain/WalletConfig.ts b/src/lib/types/domain/WalletConfig.ts index 19d936a..36aeee9 100644 --- a/src/lib/types/domain/WalletConfig.ts +++ b/src/lib/types/domain/WalletConfig.ts @@ -26,5 +26,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/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 215cd1d..bd539b6 100644 --- a/src/wallet/WalletProvider.tsx +++ b/src/wallet/WalletProvider.tsx @@ -30,17 +30,21 @@ 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"; import { ConfigProvider, + CircleProvider, OWSProvider, SupportedChainsBlockchainProvider, EventBus, @@ -56,16 +60,20 @@ 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, IEventBus, IOWSProvider, @@ -96,8 +104,11 @@ import { useWalletAuth } from "./useWalletAuth"; 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); const owsProvider: IOWSProvider = new OWSProvider(); const chainRepository: IChainRepository = new HardcodedChainRepository(); const blockchainProvider: IBlockchainProvider = @@ -126,6 +137,7 @@ const oneshotRelayerRepository: IOneshotRelayerRepository = blockchain: blockchainProvider, owsProvider, }); +const circleRepository: ICircleRepository = new CircleRepository(); const businessTransactionUtils = new BusinessTransactionUtils({ chainRepository, @@ -135,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, @@ -189,6 +212,7 @@ export type WalletContextValue = { assetActivityRepository: IAssetActivityRepository; oneshotRelayerRepository: IOneshotRelayerRepository; transactionService: ITransactionService; + bridgeService: IBridgeService; delegationService: IDelegationService; eventBus: IEventBus; @@ -332,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, @@ -418,6 +464,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { transactionService, delegationService, transactionUtils, + cctpUtils, credentialRepository, walletStorage, eventBus, @@ -654,6 +701,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { assetActivityRepository, oneshotRelayerRepository, transactionService, + bridgeService, delegationService, eventBus, chains, @@ -725,6 +773,8 @@ export function WalletProvider({ children }: { children: ReactNode }) { ); return ( - {children} + + {children} + ); } diff --git a/src/wallet/modalTypes.ts b/src/wallet/modalTypes.ts index d859ffa..e1b9c10 100644 --- a/src/wallet/modalTypes.ts +++ b/src/wallet/modalTypes.ts @@ -15,6 +15,11 @@ import type { } from "@1shotapi/ows-types"; 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"; @@ -182,6 +187,20 @@ 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; + } + | { + 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/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 4484247..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 { @@ -76,6 +77,8 @@ import { registerAddAssetRpc } from "./registerAddAsset"; 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 { @@ -183,6 +186,7 @@ export interface IUseWalletBootParams { transactionService: ITransactionService; delegationService: IDelegationService; transactionUtils: ITransactionUtils; + cctpUtils: ICCTPUtils; credentialRepository: CachedRelayerVaultRepository; walletStorage: AccountConnectStorage; eventBus: IEventBus; @@ -212,6 +216,7 @@ export function useWalletBoot({ transactionService, delegationService, transactionUtils, + cctpUtils, credentialRepository, walletStorage, eventBus, @@ -464,6 +469,30 @@ export function useWalletBoot({ registerFocusModeRpc(wallet, rpcHelper); + registerOnrampRpc(wallet, { + getOwnerAddress: () => { + const address = useWalletSessionStore.getState().evmAddress; + if (!address || String(address).toLowerCase() === "0x0") { + return null; + } + return address; + }, + }); + + 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); + }); +});