diff --git a/.changeset/networks87.md b/.changeset/networks87.md new file mode 100644 index 00000000..44d721b7 --- /dev/null +++ b/.changeset/networks87.md @@ -0,0 +1,22 @@ +--- +"@clip-wallet/core": minor +"@clip-wallet/config": minor +"@clip-wallet/vault": minor +"@clip-wallet/1mask": minor +"@clip-wallet/engine": minor +"@clip-wallet/extension-kit": minor +"@clip-wallet/ui": minor +"@clip-wallet/chains-evm": minor +"@clip-wallet/chains-substrate": minor +"@clip-wallet/chains-cosmos": minor +"@clip-wallet/chains-tron": minor +"@clip-wallet/chains-xrpl": minor +"@clip-wallet/chains-antelope": minor +"@clip-wallet/chains-multiversx": minor +"@clip-wallet/chains-icp": minor +"@clip-wallet/chains-stacks": minor +"@clip-wallet/chains-fuel": minor +"@clip-wallet/chains-bitcoincash": minor +--- + +Every CLPR network with a self-custodial key model: twelve new families (Cosmos SDK chains Osmosis, dYdX, ZIGChain, Provenance, THORChain and Initia; TRON; XRP Ledger; Vaulta, Telos and XPR Network; MultiversX; Internet Computer; Stacks; Fuel; Bitcoin Cash), Chainflip in chains-substrate, STRATO and Arc mainnet in chains-evm, and their dapp providers where a standard lets the wallet appear as itself. diff --git a/.gitleaksignore b/.gitleaksignore index da21e04e..b2250f6c 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -102,3 +102,28 @@ services/backup/test/oidc-fixtures.ts:jwt:20 services/backup/test/oidc-fixtures.ts:jwt:21 services/backup/test/oidc-fixtures.ts:jwt:22 services/backup/test/oidc-fixtures.ts:jwt:23 + +# packages/chains-antelope/test/fixtures.ts: an Antelope public key (PUB_K1) in a test fixture +e5aaffa83cc45b83045fa3ab2b1c4514f8aa83f8:packages/chains-antelope/test/fixtures.ts:generic-api-key:53 +packages/chains-antelope/test/fixtures.ts:generic-api-key:53 +# packages/chains-antelope/test/serializer.test.ts: an Antelope public key (PUB_K1) in a serializer test +e5aaffa83cc45b83045fa3ab2b1c4514f8aa83f8:packages/chains-antelope/test/serializer.test.ts:generic-api-key:44 +packages/chains-antelope/test/serializer.test.ts:generic-api-key:44 +# packages/chains-xrpl/test/codec.test.ts: XRPL NFToken and offer ids (public ledger object ids) in codec tests +1fb0d0f6ce12beeec2764035960fdba226889570:packages/chains-xrpl/test/codec.test.ts:generic-api-key:27 +1fb0d0f6ce12beeec2764035960fdba226889570:packages/chains-xrpl/test/codec.test.ts:generic-api-key:28 +packages/chains-xrpl/test/codec.test.ts:generic-api-key:27 +packages/chains-xrpl/test/codec.test.ts:generic-api-key:28 +# packages/chains-xrpl/test/xrpl.test.ts: an XRPL NFToken offer id (a public ledger object id) +1fb0d0f6ce12beeec2764035960fdba226889570:packages/chains-xrpl/test/xrpl.test.ts:generic-api-key:329 +packages/chains-xrpl/test/xrpl.test.ts:generic-api-key:329 +# packages/1mask/src/shared/cosmos.ts: Initia chain feature names (eth key sign, eth secp256k1 initia) +b96d0e3f552b0471e9f435f9c7d4daa424d978dd:packages/1mask/src/shared/cosmos.ts:generic-api-key:102 +b96d0e3f552b0471e9f435f9c7d4daa424d978dd:packages/1mask/src/shared/cosmos.ts:generic-api-key:103 +packages/1mask/src/shared/cosmos.ts:generic-api-key:102 +packages/1mask/src/shared/cosmos.ts:generic-api-key:103 +# packages/chains-cosmos/test/helpers.ts: a secp256k1 public key (base64) in a test helper +b96d0e3f552b0471e9f435f9c7d4daa424d978dd:packages/chains-cosmos/test/helpers.ts:generic-api-key:92 +packages/chains-cosmos/test/helpers.ts:generic-api-key:92 +# .gitleaksignore itself: the comment above quoted those feature names in the commit that added it +ffcd993f6685732a06c3f4d751d75f77f8c1a2dc:.gitleaksignore:generic-api-key:120 diff --git a/.harness/spec.md b/.harness/spec.md index 678eca08..356b5001 100644 --- a/.harness/spec.md +++ b/.harness/spec.md @@ -1,7 +1,7 @@ # Clip Wallet: product spec Clip Wallet is a **non-custodial** wallet for every CLPR network. One recovery phrase, one set of accounts across -14 network families, and money that moves to wherever an app needs it. It is also a **kit**: anyone can ship a wallet +26 network families (85 of the 87 CLPR networks), and money that moves to wherever an app needs it. It is also a **kit**: anyone can ship a wallet of their own on the same packages. Pre-release: **test networks only**. ## Principles diff --git a/AGENTS.md b/AGENTS.md index f13c98ce..74083ef3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md: Clip Wallet -Clip Wallet is a non-custodial wallet for every CLPR network: 14 network families, 1Mask for every dapp, decoded +Clip Wallet is a non-custodial wallet for the CLPR networks (85 of 87; Canton and Mixin have no self-custodial model, docs/r1/networks87.md): 26 network families, 1Mask for every dapp, decoded approvals, a security floor, features (staking, swaps, on-ramps, Secure Trade), social (contacts, Clip handles, notifications, Discover), sandboxed Clip Plugins, and route-and-fund / settle-on-Hedera on CLPRouter. It is also a kit: the `@clip-wallet/*` packages, `@clip-wallet/extension-kit`, `@clip-wallet/desktop-kit`, `@clip-wallet/mobile-kit` @@ -32,8 +32,9 @@ Release work (Docker for the last one): `pnpm --filter @clip-wallet/extension pa ``` packages/core shared types (the contract); additive changes only packages/config clip.config.ts schema (zod): identity, theme, networks, languages, app ids, scheme, route, services, mainnet; /node loader -packages/vault phrase, derivation for 14 families, encryption, approval-bound signing, passkey unlock +packages/vault phrase, derivation for 26 families, encryption, approval-bound signing, passkey unlock packages/chains-* evm hedera solana bitcoin sui aptos cardano substrate starknet ton near stellar tezos algorand + cosmos (+provenance thorchain initia) tron xrpl antelope multiversx icp stacks fuel bitcoincash packages/1mask dapp connectors for all families + WalletConnect; announces the wallet's identity packages/engine environment-free orchestration (approvals, portfolio, catalog, wiring) shared by extension and mobile packages/extension-kit the browser extension as a library: background, pages, WXT config (clipWallet()), security floor diff --git a/README.md b/README.md index 5701a83a..fca0f46e 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,16 @@

One wallet for every network, and the networks stay out of your way.

- A non-custodial wallet for 14 network families on one recovery phrase. It works with every dapp as it is, and it - decodes every request into plain words before you sign. It is also an open kit for launching your own wallet. + A non-custodial wallet for 85 of the 87 CLPR networks, 26 network families on one recovery phrase. It works with + dapps as they are, and it decodes every request into plain words before you sign. It is also an open kit for + launching your own wallet.

License: Apache-2.0 CI Tests: 2,300+ passing - Networks: 14 families + Networks: 26 families, 93 mainnets Status: testnet preview

@@ -59,13 +60,16 @@ mistake loses money. One example is an address that can receive on several netwo -- **One seed, 14 families.** EVM (Ethereum, Base, Arbitrum, Optimism, Hedera EVM and any EVM chain by id), Hedera, - Solana, Bitcoin, Sui, Aptos, Cardano, Polkadot SDK, Starknet, TON, NEAR, Stellar, Tezos and Algorand, all from one - BIP-39 phrase. -- **Works with every dapp, with no changes to the dapp.** 1Mask speaks each ecosystem's own standard: EIP-1193 with - EIP-6963, the Wallet Standard (Solana, Sui, Aptos, Bitcoin), CIP-30, `injectedWeb3`, TON Connect, NEAR Wallet - Selector, SEP-43, Beacon, ARC-1, and WalletConnect v2. It is tested against stock wallet pickers and live testnet - dapps. +- **One seed, 26 families.** EVM (Ethereum, Base, Arbitrum, Optimism, Hedera EVM and any EVM chain by id), Hedera, + Solana, Bitcoin, Sui, Aptos, Cardano, Polkadot SDK (with Chainflip), Starknet, TON, NEAR, Stellar, Tezos, Algorand, + the Cosmos SDK chains (Osmosis, dYdX, ZIGChain, Provenance, THORChain, Initia), TRON, XRP Ledger, Stacks, Fuel, + Bitcoin Cash, MultiversX, Internet Computer and Antelope (Vaulta, Telos, XPR Network), all from one BIP-39 phrase: + 85 of the 87 CLPR networks and 93 mainnets ([the list](https://coldai.org/clip/docs/reference/networks.html)). +- **Works with dapps as they are.** 1Mask speaks each ecosystem's own standard: EIP-1193 with EIP-6963, the Wallet + Standard (Solana, Sui, Aptos, Bitcoin, XRPL's XLS-72d), CIP-30, `injectedWeb3`, TON Connect, NEAR Wallet Selector, + SEP-43, Beacon, ARC-1, a Keplr-compatible Cosmos API, TIP-1193 with TIP-6963, SIP-030 with WBIP-004, the + FuelConnector, and WalletConnect v2 (including Bitcoin Cash's wc2-bch-bcr). It is tested against stock wallet pickers + and live testnet dapps. - **Approval screens that decode what you sign.** Every request becomes a plain-words `DecodedRequest` that shows the title, balance changes, fee, time and warnings. Unlimited approvals, permits, look-alike tokens and unknown contract calls are called out. Requests the wallet can't read are **blocked by default**: no blind signing. @@ -141,7 +145,9 @@ site gets its own session. ## Networks and dapp standards -All 14 families are tested end to end on their public testnets with the real build, in two suites. The +Clip supports 85 of the 87 CLPR networks: every one except Canton and Mixin, which have no model in which a +self-custodial key wallet can hold funds ([why, and the full list](https://coldai.org/clip/docs/reference/networks.html)). +The families are tested on their public testnets with the real build, in two suites. The [dapp matrix](docs/r1/dapp-matrix.md) drives each ecosystem's own dapp library (**connect**, **sign** and verify, **send** and confirm on chain, **approval** decoded). The [picker matrix](docs/r1/picker-matrix.md) drives the wallet pickers dapps actually ship, plus 8 hosted testnet dapps. @@ -162,6 +168,15 @@ dapps actually ship, plus 8 hosted testnet dapps. | Stellar | Testnet | Stellar Wallets Kit module (SEP-43), SEP-53 | connect · sign · send · approval | Stellar Wallets Kit + module | | Tezos | Shadownet | Beacon (TZIP-10) | connect · sign · send · approval | Beacon lists Clip; connecting from its modal is a known gap | | Algorand | TestNet | use-wallet adapter (ARC-1, ARC-6) | connect · send · approval | use-wallet UI + `@clip-wallet/kit-modules/algorand` | +| Cosmos SDK | osmo-test-5 | Keplr-compatible API at `window.clipwallet.cosmos` | connect · sign ¹ | cosmjs, with Clip's object in place of `window.keplr` | +| TRON | Nile | TIP-1193 + TIP-6963 | connect · sign ¹ | TronWeb; the tronwallet adapters don't take TIP-6963 wallets yet | +| XRP Ledger | Testnet | XLS-72d (Wallet Standard) | connect · send · approval (no message signing in XLS-72d) | Wallet Standard discovery | +| Stacks | Testnet | SIP-030 + WBIP-004 | connect · sign · send · approval | `@stacks/connect` 8 | +| Fuel | Testnet | FuelConnector | connect · sign ¹ | fuels-ts `Fuel` | +| Chainflip (Polkadot SDK) | Perseverance | `injectedWeb3` | connect · sign · approval ¹ | `@polkadot/extension-dapp`, as lp.chainflip.io | +| MultiversX | Devnet | best effort: sdk-dapp's undocumented custom-provider hook | connect · sign ¹ | sdk-dapp 5, where the dapp keeps `window.multiversx` | +| Bitcoin Cash | Chipnet | WalletConnect (wc2-bch-bcr) | not run (needs a WalletConnect project id) | Cashonize-style WalletConnect dapps | +| Internet Computer, Antelope | `icp:test`, Jungle4 | none: send and receive only | n/a | n/a | ¹ The remaining steps wait for testnet funds in the matrix wallet. Each approval says plainly why it can't go ahead, for example "You don't have enough APT to pay the network fee". Bitcoin passed send and approval in an earlier @@ -358,7 +373,7 @@ screens come from the fixture build. Details: [`packages/route`](packages/route) | Path | What | | --- | --- | | [`packages/core`](packages/core) | The contract: `ChainModule`, `DecodedRequest`, assets, errors | -| [`packages/vault`](packages/vault) | Phrase, derivation for 14 families, encryption, approval-bound signing, passkeys. The only package that touches keys | +| [`packages/vault`](packages/vault) | Phrase, derivation for 26 families, encryption, approval-bound signing, passkeys. The only package that touches keys | | [`packages/chains-*`](packages) | One `ChainModule` per family | | [`packages/1mask`](packages/1mask) | Dapp connectors for every family, the router, WalletConnect | | [`packages/engine`](packages/engine) | Orchestration shared by the extension, desktop and mobile | @@ -389,10 +404,11 @@ screens come from the fixture build. Details: [`packages/route`](packages/route) - **Optional services see ciphertext and hashes only.** The threat model is in [`services/backup`](services/backup/README.md). -**Audit status.** An internal review in October 2026 covered the vault, the approval path for all 14 families, 1Mask, -WalletConnect, plugins, hardware signing, the services and the supply chain. It found 47 issues (2 critical, 5 high, -18 medium, 22 low) and all of them are fixed, each with a regression test: there are no open findings -([report](docs/audit/internal-audit-2026-10.md)). The **external audit comes before any mainnet build**. +**Audit status.** An internal review in October 2026 covered the vault, the approval path for the first 14 families, +1Mask, WalletConnect, plugins, hardware signing, the services and the supply chain. It found 47 issues (2 critical, +5 high, 18 medium, 22 low) and all of them are fixed, each with a regression test: there are no open findings +([report](docs/audit/internal-audit-2026-10.md)). The 12 newer families came after that review. The **external +audit comes before any mainnet build**. Report vulnerabilities privately: [SECURITY.md](SECURITY.md) (GitHub private reporting, or [shayan@coldai.org](mailto:shayan@coldai.org)). @@ -405,7 +421,7 @@ Report vulnerabilities privately: [SECURITY.md](SECURITY.md) (GitHub private rep | Extension (Firefox) | Builds as MV3 and passes `addons-linter`. No Firefox e2e yet, and Clip Plugins are left out | | Desktop (macOS, Windows, Linux) | Electron app with a built-in dapp browser. e2e on all three in CI. Unsigned unless signing secrets are set | | Mobile (iOS, Android) | Screens and engine tested (vitest and jest-expo). No store builds yet | -| Networks | 14 families on testnets. **Mainnet is off**, behind a build flag and a checklist | +| Networks | 26 families (85 of the 87 CLPR networks) on testnets. **Mainnet is off**, behind a build flag and a checklist | | Settle on Hedera | Testnet contracts live. The test Connector runs locally | | Store listings | Packages, copy, screenshots and permission justifications are ready. **Nothing has been submitted yet** | | Security | Internal review done. **External audit pending** | diff --git a/apps/desktop/clip.config.ts b/apps/desktop/clip.config.ts index d9da2179..60946c4f 100644 --- a/apps/desktop/clip.config.ts +++ b/apps/desktop/clip.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ icon: "./icon.svg", theme: { accent: "#FF3C00", accentText: "#FFFFFF", font: "Inter", radius: 14 }, // All 14 families, testnets only (same as the extension). - networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand"], + networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", "cosmos", "provenance", "thorchain", "initia", "tron", "xrpl", "antelope", "multiversx", "icp", "stacks", "fuel", "bitcoincash"], passkeys: { enabled: true }, walletConnect: {}, services: MAINNET ? {} : TESTNET_SERVICES, diff --git a/apps/docs/.vitepress/sidebar.ts b/apps/docs/.vitepress/sidebar.ts index 8c62d808..6112f9d8 100644 --- a/apps/docs/.vitepress/sidebar.ts +++ b/apps/docs/.vitepress/sidebar.ts @@ -74,6 +74,13 @@ const dapps: Item[] = [ { text: "Stellar", link: "/dapps/stellar" }, { text: "Algorand", link: "/dapps/algorand" }, { text: "Tezos (Beacon)", link: "/dapps/tezos" }, + { text: "Cosmos SDK (Keplr API)", link: "/dapps/cosmos" }, + { text: "TRON (TIP-1193)", link: "/dapps/tron" }, + { text: "XRP Ledger (XLS-72d)", link: "/dapps/xrpl" }, + { text: "Stacks (SIP-030)", link: "/dapps/stacks" }, + { text: "Fuel", link: "/dapps/fuel" }, + { text: "Bitcoin Cash", link: "/dapps/bitcoincash" }, + { text: "MultiversX (best effort)", link: "/dapps/multiversx" }, { text: "WalletConnect", link: "/dapps/walletconnect" }, ], }, @@ -174,6 +181,7 @@ const reference = (): Item[] => [ text: "Reference", items: [ { text: "Overview", link: "/reference/" }, + { text: "Networks", link: "/reference/networks" }, { text: "clip.config.ts schema", link: "/reference/config" }, { text: "Error codes", link: "/reference/errors" }, { text: "Warning codes", link: "/reference/warnings" }, diff --git a/apps/docs/package.json b/apps/docs/package.json index b6074a69..a5a16b42 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -31,19 +31,28 @@ "@clip-wallet/1mask": "workspace:*", "@clip-wallet/backup-client": "workspace:*", "@clip-wallet/chains-algorand": "workspace:*", + "@clip-wallet/chains-antelope": "workspace:*", "@clip-wallet/chains-aptos": "workspace:*", "@clip-wallet/chains-bitcoin": "workspace:*", + "@clip-wallet/chains-bitcoincash": "workspace:*", "@clip-wallet/chains-cardano": "workspace:*", + "@clip-wallet/chains-cosmos": "workspace:*", "@clip-wallet/chains-evm": "workspace:*", + "@clip-wallet/chains-fuel": "workspace:*", "@clip-wallet/chains-hedera": "workspace:*", + "@clip-wallet/chains-icp": "workspace:*", + "@clip-wallet/chains-multiversx": "workspace:*", "@clip-wallet/chains-near": "workspace:*", "@clip-wallet/chains-solana": "workspace:*", + "@clip-wallet/chains-stacks": "workspace:*", "@clip-wallet/chains-starknet": "workspace:*", "@clip-wallet/chains-stellar": "workspace:*", "@clip-wallet/chains-substrate": "workspace:*", "@clip-wallet/chains-sui": "workspace:*", "@clip-wallet/chains-tezos": "workspace:*", "@clip-wallet/chains-ton": "workspace:*", + "@clip-wallet/chains-tron": "workspace:*", + "@clip-wallet/chains-xrpl": "workspace:*", "@clip-wallet/config": "workspace:*", "@clip-wallet/connect": "workspace:*", "@clip-wallet/core": "workspace:*", diff --git a/apps/docs/src/architecture/chain-modules.md b/apps/docs/src/architecture/chain-modules.md index b613e9e8..5f5ff7fc 100644 --- a/apps/docs/src/architecture/chain-modules.md +++ b/apps/docs/src/architecture/chain-modules.md @@ -25,7 +25,10 @@ the change-address helpers. A module that can't decode something throws a `ClipE How to write one, step by step with a complete example: [Write a chain module](../extend/chain-module.md). -## The fourteen modules +## The modules + +One package per family; the Cosmos SDK package serves four families (cosmos, provenance, thorchain, initia) and +the Antelope package three chains. Every network and its CLPR status: [Networks](../reference/networks.md). | Family | Package | Factory | Test networks | Dapp standard | | --- | --- | --- | --- | --- | @@ -43,6 +46,15 @@ How to write one, step by step with a complete example: [Write a chain module](. | Stellar | `@clip-wallet/chains-stellar` | `createStellarModule()` | Testnet | SEP-43 | | Tezos | `@clip-wallet/chains-tezos` | `createTezosModule()` | Shadownet | Beacon | | Algorand | `@clip-wallet/chains-algorand` | `createAlgorandModule()` | TestNet | ARC-1, use-wallet | +| Cosmos SDK | `@clip-wallet/chains-cosmos` | `createCosmosModule()` | osmo-test-5, dydx-testnet-4, zig-test-2, pio-testnet-1, initiation-2 (THORChain: none) | Keplr-compatible API | +| TRON | `@clip-wallet/chains-tron` | `createTronModule()` | Nile, Shasta | TIP-1193 + TIP-6963 | +| XRP Ledger | `@clip-wallet/chains-xrpl` | `createXrplModule()` | Testnet, Devnet | XLS-72d (Wallet Standard) | +| Stacks | `@clip-wallet/chains-stacks` | `createStacksModule()` | Testnet | SIP-030 + WBIP-004 | +| Fuel | `@clip-wallet/chains-fuel` | `createFuelModule()` | Testnet | FuelConnector | +| Bitcoin Cash | `@clip-wallet/chains-bitcoincash` | `createBitcoinCashModule()` | Chipnet, Testnet4 | WalletConnect (wc2-bch-bcr) | +| MultiversX | `@clip-wallet/chains-multiversx` | `createMultiversXModule()` | Devnet, Testnet | best effort (sdk-dapp hook) | +| Internet Computer | `@clip-wallet/chains-icp` | `createIcpModule()` | `icp:test` (DFINITY test ledgers) | none: send and receive | +| Antelope | `@clip-wallet/chains-antelope` | `createAntelopeModule()` | Jungle4, Telos testnet, XPR testnet | none: send and receive | Each package also exports its networks (most at `@clip-wallet/chains-/networks`) and its token tables. The catalogue in [`packages/engine/src/catalog.ts`](repo:packages/engine/src/catalog.ts) assembles them for a wallet, diff --git a/apps/docs/src/architecture/extension-kit.md b/apps/docs/src/architecture/extension-kit.md index fcd8ba97..851b4378 100644 --- a/apps/docs/src/architecture/extension-kit.md +++ b/apps/docs/src/architecture/extension-kit.md @@ -1,7 +1,7 @@ # Extension kit `@clip-wallet/extension-kit` is the whole browser extension as a library for [WXT](https://wxt.dev): the background -(vault, approvals, security checks), the pages, 1Mask for all fourteen families and the build wiring. A wallet project, +(vault, approvals, security checks), the pages, 1Mask for every family and the build wiring. A wallet project, Clip Wallet's own `apps/extension` included, keeps only its identity, its `wxt.config.ts` and one-line entrypoints. ```mermaid diff --git a/apps/docs/src/architecture/signing-flow.md b/apps/docs/src/architecture/signing-flow.md index 8a7590d0..cc58c957 100644 --- a/apps/docs/src/architecture/signing-flow.md +++ b/apps/docs/src/architecture/signing-flow.md @@ -1,6 +1,6 @@ # The signing flow -Every request takes the same path, whichever dapp library sent it and whichever of the fourteen families it is for: +Every request takes the same path, whichever dapp library sent it and whichever family it is for: **request → decode → sanitise → approval → single-use signing**. This page follows one request end to end and points at the code for each step. diff --git a/apps/docs/src/architecture/vault.md b/apps/docs/src/architecture/vault.md index 4f1f9bad..fa317ce1 100644 --- a/apps/docs/src/architecture/vault.md +++ b/apps/docs/src/architecture/vault.md @@ -1,7 +1,7 @@ # Vault `@clip-wallet/vault` is the only package that touches recovery phrases and private keys. It implements the `Vault` -interface from `@clip-wallet/core` as `ClipVault`: create or import a phrase, derive accounts for fourteen families, +interface from `@clip-wallet/core` as `ClipVault`: create or import a phrase, derive accounts for 26 families, encrypt everything at rest, and sign only what the person approved. ```mermaid @@ -52,6 +52,18 @@ the same addresses: | stellar | ed25519 | `m/44'/148'/i'` (SEP-0005) | Stellar wallets | | tezos | ed25519 | `m/44'/1729'/i'/0'` | Temple, Kukai | | algorand | BIP32-Ed25519 (ARC-52) | `m/44'/283'/i'/0/0` | Pera Universal Wallet | +| cosmos | secp256k1 | `m/44'/118'/0'/0/i` | Keplr, Leap, Cosmostation | +| provenance | secp256k1 | `m/44'/505'/0'/0/i` | Keplr, Leap | +| thorchain | secp256k1 | `m/44'/931'/0'/0/i` | Keplr, Ctrl (XDEFI), Vultisig | +| initia | ethsecp256k1 | `m/44'/60'/0'/0/i` (the EVM key) | Initia Wallet, Keplr | +| tron | secp256k1 | `m/44'/195'/0'/0/i` | TronLink, TronWeb `fromMnemonic` | +| xrpl | secp256k1 | `m/44'/144'/i'/0/0` | xrpl.js `Wallet.fromMnemonic`, Ledger Live | +| antelope | secp256k1 (K1, canonical signatures) | `m/44'/194'/0'/0/i` | TokenPocket; one key for Vaulta, Telos and XPR Network | +| multiversx | ed25519 | `m/44'/508'/0'/0'/i'` | xPortal, DeFi Wallet | +| icp | secp256k1 | `m/44'/223'/0'/0/i` | Plug, dfx `identity import` | +| stacks | secp256k1 | `m/44'/5757'/0'/0/i` | Leather, Xverse | +| fuel | secp256k1 | `m/44'/1179993420'/i'/0/0` | Fuel Wallet, fuels-ts | +| bitcoincash | secp256k1 | `m/44'/145'/0'/0/i` | Electron Cash, Paytaca, Bitcoin.com Wallet | The [vault README](repo:packages/vault/README.md) gives the source for every row and the cases where wallets disagree (Hedera key types, Algorand's three schemes, TON's native mnemonics, Starknet's account schemes). diff --git a/apps/docs/src/dapps/bitcoincash.md b/apps/docs/src/dapps/bitcoincash.md new file mode 100644 index 00000000..65e3f8b1 --- /dev/null +++ b/apps/docs/src/dapps/bitcoincash.md @@ -0,0 +1,21 @@ +# Bitcoin Cash (WalletConnect) + +Bitcoin Cash has no injected-provider standard. Dapps reach wallets over **WalletConnect v2** with the community +specification **wc2-bch-bcr** (used by Cashonize, Paytaca and Zapit, and apps such as TapSwap), and that is what Clip +Wallet answers. It needs a build with a WalletConnect project id (`CLIP_WALLETCONNECT_PROJECT_ID`). + +| | | +| --- | --- | +| Namespace | `bch` | +| Chains | `bch:bitcoincash` (mainnet), `bch:bchtest` (chipnet, where BCH upgrades are tested) | +| Methods | `bch_getAddresses`, `bch_signTransaction`, `bch_signMessage` | +| Events | `addressesChanged` | + +Pair from the dapp's WalletConnect QR code or link as with any other chain: see [WalletConnect](./walletconnect.md). +Every transaction is decoded into plain words before approval. + +## Tested + +- Unit tests run the wc2-bch-bcr session and requests against the wallet's WalletConnect host + ([`packages/1mask/test/walletconnect-bch.test.ts`](repo:packages/1mask/test/walletconnect-bch.test.ts)). There is no + dapp matrix row: it needs a WalletConnect project id and testnet BCH. diff --git a/apps/docs/src/dapps/cosmos.md b/apps/docs/src/dapps/cosmos.md new file mode 100644 index 00000000..cf6a2daf --- /dev/null +++ b/apps/docs/src/dapps/cosmos.md @@ -0,0 +1,30 @@ +# Cosmos SDK chains (Keplr-compatible) + +Clip Wallet answers the **Keplr API** at `window.clipwallet.cosmos` (a kit-built wallet uses its own global key). It +never sets `window.keplr`: a dapp written for Keplr works by passing this object where it used `window.keplr`, and +lists Clip under its own name. Method names, arguments and results follow Keplr's `Keplr` interface +(`@keplr-wallet/types` 0.13), including the cosmjs offline signers. + +<<< @/snippets/dapps/cosmos.ts + +| Supported | | +| --- | --- | +| `enable(chainIds)`, `disable()` | the connect approval, per chain | +| `getKey(chainId)` | `bech32Address`, `pubKey`, `algo` (`secp256k1`, or `ethsecp256k1` on Initia) | +| `signDirect`, `signAmino` | transactions, decoded into plain words before approval | +| `signArbitrary`, `verifyArbitrary` | ADR-36 messages | +| `sendTx(chainId, tx, mode)` | broadcast through the wallet's node | +| `getOfflineSigner`, `getOfflineSignerOnlyAmino`, `getOfflineSignerAuto` | cosmjs signers (`SigningStargateClient.connectWithSigner`) | +| `getChainInfosWithoutEndpoints`, `getChainInfoWithoutEndpoints` | the chains Clip ships | +| `on("keystorechange")` | account changes; also the window event `clipwallet_keystorechange` | +| Chains | Osmosis, dYdX, ZIGChain, Provenance, THORChain, Initia, and their testnets where one exists | + +`experimentalSuggestChain` resolves for a chain Clip already ships and refuses any other: a website can't add chains +to the wallet. Each chain id maps to one wallet family (`cosmos`, `provenance`, `thorchain`, `initia`), so permissions +and accounts are per family. See [Networks](../reference/networks.md) for coin types and network ids. + +## Tested + +- **Dapp matrix**, osmo-test-5: cosmjs `SigningStargateClient` over `window.clipwallet.cosmos` connects and an ADR-36 + signature verifies with `@cosmjs/crypto`. The send and approval levels wait for testnet OSMO. + [Results](../testing/results/dapp-matrix.md) · [test page](repo:apps/extension/e2e/matrix/dapps/cosmos.ts) diff --git a/apps/docs/src/dapps/evm.md b/apps/docs/src/dapps/evm.md index 8519350c..2cc57751 100644 --- a/apps/docs/src/dapps/evm.md +++ b/apps/docs/src/dapps/evm.md @@ -9,7 +9,7 @@ MetaMask. | EIP-6963 name | `Clip Wallet` | | EIP-6963 rdns | `org.coldai.clipwallet` (kit-built wallets announce their own) | | `window.ethereum` | left alone | -| Networks | the EVM test networks the wallet ships with: Sepolia, Base Sepolia, Arbitrum Sepolia, OP Sepolia, Arc, Hedera EVM and more | +| Networks | the EVM test networks the wallet ships with: Sepolia, Base Sepolia, Arbitrum Sepolia, OP Sepolia, Arc, STRATO Helium, Hedera EVM and more (every mainnet: [Networks](../reference/networks.md)) | ## Without a library @@ -58,3 +58,9 @@ Send standard, readable requests: EIP-712 typed data instead of raw hashes, real - **Picker matrix**: RainbowKit, ConnectKit and Reown AppKit list Clip under "Installed" with its icon, connect, reconnect and restore after a reload; Reown AppKit Lab on Sepolia signs a message. [Results](../testing/results/picker-matrix.md) + +## STRATO + +STRATO's nodes take signed legacy (EIP-155) Ethereum transactions, so EIP-1193 dapps work unchanged: gas is unpriced, +and the flat per-transaction fee (0.01 USDST, or a voucher) shows on the approval as its own line. Contract calls are +matched to SolidVM functions by selector; STRATO's EIP-712 "function call" transactions aren't supported. diff --git a/apps/docs/src/dapps/fuel.md b/apps/docs/src/dapps/fuel.md new file mode 100644 index 00000000..972b297d --- /dev/null +++ b/apps/docs/src/dapps/fuel.md @@ -0,0 +1,25 @@ +# Fuel (FuelConnector) + +Clip Wallet announces a **FuelConnector** with the `FuelConnector` window event, the way Fuel Wallet does, so every +fuels-ts `Fuel` instance on the page adds it to its connectors under Clip's own name and icon. Because a `Fuel` created +later misses an early event, Clip announces on install, on `DOMContentLoaded` and on `load`, and is also at +`window.clipwallet.fuel` for `new Fuel({ connectors: [window.clipwallet.fuel] })`. + +<<< @/snippets/dapps/fuel.ts + +| Supported | | +| --- | --- | +| `connect`, `disconnect`, `isConnected`, `accounts`, `currentAccount` | the connect approval and accounts | +| `sendTransaction(address, request)` | a fuels-ts `TransactionRequest`, decoded before approval; returns the transaction id | +| `signTransaction`, `signMessage` | signing without sending | +| `currentNetwork`, `networks`, `selectNetwork` | Fuel Ignition and the testnet | +| Not supported | `addNetwork`, `addAsset(s)`: a website can't add networks or assets to the wallet | + +The connector is dependency-free: fuels-ts never runs inside the wallet's page script, and transactions cross to the +wallet as the same JSON Fuel Wallet's connector sends. + +## Tested + +- **Dapp matrix**, testnet: fuels-ts `Fuel` lists Clip's connector and connects, and a signed message recovers the + account with `Signer.recoverAddress(hashMessage(…))`. The send and approval levels wait for testnet ETH. + [Results](../testing/results/dapp-matrix.md) · [test page](repo:apps/extension/e2e/matrix/dapps/fuel.ts) diff --git a/apps/docs/src/dapps/index.md b/apps/docs/src/dapps/index.md index f236b409..44a60dba 100644 --- a/apps/docs/src/dapps/index.md +++ b/apps/docs/src/dapps/index.md @@ -28,8 +28,20 @@ libraries against every build to prove it. See [Compatibility promise](../connec | Stellar | SEP-43 | Stellar Wallets Kit with `@clip-wallet/kit-modules/stellar` | [Stellar](./stellar.md) | | Algorand | ARC-1 | use-wallet with `@clip-wallet/kit-modules/algorand` | [Algorand](./algorand.md) | | Tezos | Beacon (TZIP-10) | Beacon finds Clip; its modal needs a listing to connect | [Tezos](./tezos.md) | +| Cosmos SDK (Osmosis, dYdX, ZIGChain, Provenance, THORChain, Initia) | Keplr-compatible API at `window.clipwallet.cosmos` | cosmjs and Keplr-style dapps, passing Clip's object instead of `window.keplr` | [Cosmos](./cosmos.md) | +| TRON | TIP-1193 + TIP-6963 | TIP-6963 discovery; TronWeb for building transactions | [TRON](./tron.md) | +| XRP Ledger | XLS-72d (Wallet Standard) | `@wallet-standard/app` with the XRPL feature filter | [XRP Ledger](./xrpl.md) | +| Stacks | SIP-030 + WBIP-004 | `@stacks/connect` 8 | [Stacks](./stacks.md) | +| Fuel | FuelConnector | fuels-ts `Fuel` | [Fuel](./fuel.md) | +| Bitcoin Cash | WalletConnect (wc2-bch-bcr) | Cashonize-style WalletConnect dapps | [Bitcoin Cash](./bitcoincash.md) | +| MultiversX | best effort: sdk-dapp's custom-provider hook | `@multiversx/sdk-dapp` 5, where the dapp keeps `window.multiversx` | [MultiversX](./multiversx.md) | | Any (phones, desktop apps) | WalletConnect v2 | Any WalletConnect dapp | [WalletConnect](./walletconnect.md) | +Antelope (Vaulta, Telos, XPR Network) and the Internet Computer have no wallet standard Clip can answer as itself +(WharfKit's plugins live in the dapp, Anchor Link and Scatter would mean posing as those wallets, and ICRC-94 is still a +draft), so on those networks Clip sends and receives but doesn't connect to dapps. Every network, with how dapps reach +it: [Networks](../reference/networks.md). + Some ecosystems' pickers show only the wallets in their own registry. For those, the guide shows the one line a dapp adds (a module, an adapter, a list entry) until Clip is listed. Listing submissions are drafted and wait for the public release. diff --git a/apps/docs/src/dapps/multiversx.md b/apps/docs/src/dapps/multiversx.md new file mode 100644 index 00000000..f8ac8dc9 --- /dev/null +++ b/apps/docs/src/dapps/multiversx.md @@ -0,0 +1,26 @@ +# MultiversX (best effort) + +::: warning Best effort, through an undocumented hook +MultiversX has no wallet-discovery standard for browser extensions. Clip Wallet adds itself to +`window.multiversx.providers`, which `@multiversx/sdk-dapp` 5 merges into its custom providers in `initApp`. sdk-dapp +documents that list as something the dapp sets, not as a channel for wallets, so it can change without notice, and it +works only on sdk-dapp dapps that don't replace `window.multiversx` (MultiversX's own template dapp does replace it). +::: + +When it works, Clip appears in the dapp's login options under its own name and icon (type `clipwallet`), never as +"extension" or the MultiversX DeFi Wallet. Clip doesn't answer the DeFi Wallet's message protocol. + +| Supported | | +| --- | --- | +| `login({ token })` | connect; with a native-auth token, also signs `address + token` (checked to be for this site) | +| `signTransaction(s)` | sdk-core `Transaction` objects, decoded before approval (EGLD, ESDT, staking calls) | +| `signMessage` | MultiversX messages, verifiable with sdk-dapp's `verifyMessage` | +| Networks | mainnet (`mvx:1`), devnet, testnet | + +There is no MultiversX WalletConnect namespace in Clip yet, so the hook is the only way a dapp reaches it. + +## Tested + +- **Dapp matrix**, devnet: `@multiversx/sdk-dapp` 5.7.3 `initApp` + `ProviderFactory` finds Clip through the hook and + connects, and a signed message verifies with `verifyMessage`. The send and approval levels wait for devnet EGLD. + [Results](../testing/results/dapp-matrix.md) · [test page](repo:apps/extension/e2e/matrix/dapps/multiversx.ts) diff --git a/apps/docs/src/dapps/polkadot.md b/apps/docs/src/dapps/polkadot.md index dc409f45..3d611eed 100644 --- a/apps/docs/src/dapps/polkadot.md +++ b/apps/docs/src/dapps/polkadot.md @@ -11,13 +11,19 @@ Accounts are sr25519, shown with the generic SS58 prefix 42; dapps re-encode for | `accounts.get()`, `accounts.subscribe()` | the accounts the person shared with the site | | `signer.signPayload` | extrinsics, decoded with the chain's metadata before approval | | `signer.signRaw` | messages (`type: "bytes"`, wrapped in `` as polkadot.js does) | -| Networks | Westend, Paseo and their Asset Hubs | +| Networks | Westend, Paseo and their Asset Hubs; Chainflip's State Chain (Perseverance testnet), with `cF…` addresses | ::: info Metadata Clip reads chain metadata from the chain itself and doesn't accept metadata from dapps (`metadata.provide()` refuses it), so polkadot.js apps keeps showing "1 extension that needs to be updated". It's harmless. ::: +::: info Chainflip +lp.chainflip.io and other Chainflip dapps use `@polkadot/extension-dapp`, so they find Clip the same way. FLIP shows +from the State Chain's `Flip.Account`; there is no FLIP transfer between State Chain accounts (FLIP is redeemed to +Ethereum), and liquidity-provider order calls are shown as the raw call with a caution. +::: + `window.injectedWeb3` is writable, like every other extension's, so `@polkadot/extension-dapp` can add to it. ## Pickers @@ -33,3 +39,6 @@ listed, add an entry to `walletList`. [Results](../testing/results/dapp-matrix.md) · [test page](repo:apps/extension/e2e/matrix/dapps/substrate.ts) - **Picker and hosted dapps**: polkadot.js apps on Westend connects and signs (sr25519 verified); DOT Connect connects; Talisman Connect connects with a list entry. [Results](../testing/results/picker-matrix.md) +- **Dapp matrix**, Chainflip Perseverance: `@polkadot/extension-dapp` + `@polkadot/api`, as lp.chainflip.io uses them, + connect with a `cF…` address, `signRaw` verifies, and the approval reads "Register as a Chainflip liquidity + provider". [test page](repo:apps/extension/e2e/matrix/dapps/chainflip.ts) diff --git a/apps/docs/src/dapps/stacks.md b/apps/docs/src/dapps/stacks.md new file mode 100644 index 00000000..b2512b9f --- /dev/null +++ b/apps/docs/src/dapps/stacks.md @@ -0,0 +1,26 @@ +# Stacks (SIP-030 and WBIP-004) + +Clip Wallet's Stacks provider implements **SIP-030** at `window.clipwallet.stacks` and registers on +`window.wbip_providers` (**WBIP-004**), which is where `@stacks/connect` 8 finds wallets for its picker. As SIP-030 +asks, it never sets a shared global (`window.StacksProvider`, `window.LeatherProvider` or `window.XverseProviders`). + +<<< @/snippets/dapps/stacks.ts + +With `@stacks/connect`, nothing extra is needed: its `request()` lists Clip under its own name and resolves the +provider from its WBIP-004 id (`clipwallet.stacks`). + +| Supported | | +| --- | --- | +| `stx_getAddresses` (and `getAddresses`) | the connect approval, then `{ addresses }` | +| `stx_getNetworks` | the networks Clip ships | +| `stx_transferStx`, `stx_transferSip10Ft`, `stx_transferSip9Nft` | transfers | +| `stx_callContract`, `stx_deployContract`, `stx_signTransaction` | contract calls, deployments, signing a built transaction | +| `stx_signMessage`, `stx_signStructuredMessage` | messages (SIP-018 for structured data) | +| Errors | SIP-030 JSON-RPC codes: `-32000` user rejection, `-32002` access denied, `-32601`, `-32602` | +| Networks | `"mainnet"`, `"testnet"` or the CAIP-2 id in the `network` param | + +## Tested + +- **Dapp matrix**, testnet: `@stacks/connect` 8 `request()` through WBIP-004 connects, a signed message verifies with + `verifyMessageSignatureRsv`, an STX transfer confirms on the testnet, and the approval reads "Send 0.000001 STX to …". + [Results](../testing/results/dapp-matrix.md) · [test page](repo:apps/extension/e2e/matrix/dapps/stacks.ts) diff --git a/apps/docs/src/dapps/tron.md b/apps/docs/src/dapps/tron.md new file mode 100644 index 00000000..1d97e3b1 --- /dev/null +++ b/apps/docs/src/dapps/tron.md @@ -0,0 +1,31 @@ +# TRON (TIP-1193 and TIP-6963) + +Clip Wallet's TRON provider follows **TIP-1193** and is announced with **TIP-6963** under the wallet's own name, icon +and rdns. It never sets `isTronLink` and never touches `window.tronLink` or `window.tronWeb`. It is also at +`window.clipwallet.tron`. + +<<< @/snippets/dapps/tron.ts + +| Supported | | +| --- | --- | +| `eth_requestAccounts` | TIP-1102: the connect approval, then `[address]` | +| `eth_chainId`, `wallet_switchEthereumChain` | TRON chain ids (`0x2b6653dc` mainnet, `0xcd8690dc` Nile, `0x94a9059e` Shasta); TIP-3326 | +| `tronWeb.trx.sign(transaction)` | transactions your TronWeb built, decoded before approval | +| `tronWeb.trx.signMessageV2(message)` | messages | +| Events | `accountsChanged`, `chainChanged`, `connect`, `disconnect` (TIP-1193) | + +TIP-1193 defines no signing methods: it says a provider carries a TronWeb as `tron.tronWeb`. Clip doesn't ship TronWeb +into the page; `tronWeb` is only the subset dapps sign through (`defaultAddress`, `trx.sign`, `trx.signMessageV2`). +Build transactions and read the chain with your own TronWeb. Legacy `trx.sign(hexString)` message signing, +`multiSign` and `_signTypedData` are refused with `4200`. + +## Pickers + +The TRON wallet adapters (`@tronweb3/tronwallet-adapters`) list a fixed set of wallets and don't yet accept a +third-party TIP-6963 wallet, so find Clip with TIP-6963 discovery as above. + +## Tested + +- **Dapp matrix**, Nile: raw TIP-6963 discovery and TIP-1193 connect, and `signMessageV2` verifies with TronWeb's + `Trx.verifyMessageV2`. The send and approval levels wait for testnet TRX. + [Results](../testing/results/dapp-matrix.md) · [test page](repo:apps/extension/e2e/matrix/dapps/tron.ts) diff --git a/apps/docs/src/dapps/walletconnect.md b/apps/docs/src/dapps/walletconnect.md index bf358383..18c8a9dc 100644 --- a/apps/docs/src/dapps/walletconnect.md +++ b/apps/docs/src/dapps/walletconnect.md @@ -10,6 +10,7 @@ with no Clip-specific code. | `solana` | devnet | `solana_signTransaction`, `solana_signAndSendTransaction`, `solana_signMessage` | | `bip122` | testnet4 | `signPsbt`, `signMessage`, `sendTransfer` | | `hedera` | testnet | `hedera_signMessage`, `hedera_signTransaction`, `hedera_signAndExecuteTransaction`, … | +| `bch` | `bch:bchtest` (chipnet) | `bch_getAddresses`, `bch_signTransaction`, `bch_signMessage` (wc2-bch-bcr; see [Bitcoin Cash](./bitcoincash.md)) | - **Proposals.** Required chains or methods Clip can't serve reject the proposal (`5100`, `5101`); optional ones are dropped and listed on the approval screen. diff --git a/apps/docs/src/dapps/xrpl.md b/apps/docs/src/dapps/xrpl.md new file mode 100644 index 00000000..80f188db --- /dev/null +++ b/apps/docs/src/dapps/xrpl.md @@ -0,0 +1,21 @@ +# XRP Ledger (XLS-72d) + +Clip Wallet registers an **XLS-72d** wallet on the Wallet Standard, under its own name and icon (it never claims to be +GemWallet, Crossmark or Xaman). Dapps that discover XRPL wallets through `@wallet-standard/app` with the +`@xrpl-wallet-standard/app` feature filter list it like any other. + +| Supported | | +| --- | --- | +| `standard:connect`, `standard:disconnect`, `standard:events` | connect approval, accounts, account changes | +| `xrpl:signTransaction` | sign a transaction the dapp built; returns the signed blob | +| `xrpl:signAndSubmitTransaction` | sign and submit; returns the transaction hash | +| Chains | `xrpl:0` (mainnet), `xrpl:1` (testnet), `xrpl:2` (devnet) | + +XLS-72d has no message signing, so there is none to call. Every transaction is decoded into plain words before +approval ("Send 10 XRP to r…", "Change your account settings"), with memos shown. + +## Tested + +- **Dapp matrix**, testnet: Wallet Standard discovery with the XLS-72d feature filter connects, and a signed and + submitted AccountSet with a memo validates on the testnet (rippled refuses a payment to yourself); the approval names + it. [Results](../testing/results/dapp-matrix.md) · [test page](repo:apps/extension/e2e/matrix/dapps/xrpl.ts) diff --git a/apps/docs/src/guide/index.md b/apps/docs/src/guide/index.md index 2ca6f865..6246f3c5 100644 --- a/apps/docs/src/guide/index.md +++ b/apps/docs/src/guide/index.md @@ -1,8 +1,8 @@ # What Clip is -Clip Wallet is a non-custodial wallet for every CLPR network: one recovery phrase for fourteen network families, -one balance per asset, and plain words before anything is signed. It runs as a browser extension (Chrome, Edge, -Firefox), an iOS and Android app, and a desktop app. +Clip Wallet is a non-custodial wallet for the CLPR networks (85 of the 87; see [Networks](../reference/networks.md)): +one recovery phrase for 26 network families, one balance per asset, and plain words before anything is signed. It +runs as a browser extension (Chrome, Edge, Firefox), an iOS and Android app, and a desktop app. It is also three things for developers: diff --git a/apps/docs/src/guide/repo-layout.md b/apps/docs/src/guide/repo-layout.md index a7257d2d..866f0480 100644 --- a/apps/docs/src/guide/repo-layout.md +++ b/apps/docs/src/guide/repo-layout.md @@ -9,7 +9,7 @@ published to npm as `@clip-wallet/` (plus `create-clip-wallet`), all at on | --- | --- | | `packages/core` | The contract: `Family`, `Network`, `AssetRef`, `ChainModule`, `DappRequest`, `DecodedRequest`, `SignablePayload`, `ClipError`, warning and error codes. Additive changes only. | | `packages/config` | The `clip.config.ts` schema (zod): identity, theme, networks, routing, services, the mainnet checklist. | -| `packages/vault` | Recovery phrase, key derivation for 14 families, encryption at rest, approval-bound signing, passkey unlock. **The only package that touches keys.** | +| `packages/vault` | Recovery phrase, key derivation for 26 families, encryption at rest, approval-bound signing, passkey unlock. **The only package that touches keys.** | | `packages/chains-*` | One `ChainModule` per family: `evm`, `hedera`, `solana`, `bitcoin`, `sui`, `aptos`, `cardano`, `substrate`, `starknet`, `ton`, `near`, `stellar`, `tezos`, `algorand`. | | `packages/1mask` | Dapp connectors for every family, plus WalletConnect. Announces the wallet's identity. | | `packages/engine` | Environment-free orchestration (approvals, permissions, portfolio, catalog) shared by mobile and desktop. | diff --git a/apps/docs/src/index.md b/apps/docs/src/index.md index c604ee8c..8dd2b643 100644 --- a/apps/docs/src/index.md +++ b/apps/docs/src/index.md @@ -31,7 +31,7 @@ features: link: /connect/ linkText: The SDK - title: A wallet of your own - details: create-clip-wallet gives you a branded extension for 14 network families, with its own name, icon and extension id, and a Scaffold-HBAR dapp next to it. + details: create-clip-wallet gives you a branded wallet for 26 network families, with its own name, icon and extension id, and a Scaffold-HBAR dapp next to it. link: /kit/ linkText: The kit - title: Extend it diff --git a/apps/docs/src/kit/index.md b/apps/docs/src/kit/index.md index 41656444..fd1c8799 100644 --- a/apps/docs/src/kit/index.md +++ b/apps/docs/src/kit/index.md @@ -1,6 +1,6 @@ # Launch your own wallet -The Clip Wallet kit lets you ship a wallet under your own name on the same code Clip Wallet runs: fourteen network +The Clip Wallet kit lets you ship a wallet under your own name on the same code Clip Wallet runs: 26 network families, 1Mask for every dapp, decoded approvals, the security floor, features, plugins and route-and-fund. One project gives you the wallet **on every platform**: a browser extension, a desktop app for macOS, Windows and Linux, and a phone app for iOS and Android, all described by one `clip.config.ts`. You change identity and configuration; the diff --git a/apps/docs/src/reference/index.md b/apps/docs/src/reference/index.md index 0772eaeb..ea48b948 100644 --- a/apps/docs/src/reference/index.md +++ b/apps/docs/src/reference/index.md @@ -1,6 +1,8 @@ # Reference -Everything on these pages is generated from the source on every build, so it can't fall behind the code. +Everything on these pages is generated from the source on every build, so it can't fall behind the code. The +[Networks](./networks.md) page lists every network the wallet ships, which CLPR network each one is, and how dapps +reach it; its coverage is checked by a test over the wallet's own catalogue. | Page | Generated from | | --- | --- | diff --git a/apps/docs/src/reference/networks.md b/apps/docs/src/reference/networks.md new file mode 100644 index 00000000..91109a6e --- /dev/null +++ b/apps/docs/src/reference/networks.md @@ -0,0 +1,194 @@ +# Networks + +Clip Wallet supports **85 of the 87 CLPR networks** on mainnet, and **93 mainnets** in total, across 26 network +families. The two CLPR networks it doesn't support, Canton and Mixin, have no model in which a self-custodial key +wallet can hold and move funds on its own: see [Not supported](#not-supported). + +::: warning Test networks first +Clip Wallet is pre-release and runs on test networks unless a build opts in to mainnet behind a checklist. "Supported +on mainnet" means the wallet's catalogue has the network and a build can be switched to it; see +[Mainnet](../kit/build-and-ship.md#mainnet). +::: + +The coverage isn't written by hand. [`packages/engine/test/networks87.test.ts`](repo:packages/engine/test/networks87.test.ts) +maps each CLPR network to the mainnet(s) in the wallet's own catalogue (`walletNetworks()` with every family on), fails +if anything but Canton and Mixin is missing, and prints the table: + +```sh +pnpm --filter @clip-wallet/engine exec vitest run test/networks87.test.ts +``` + +The working notes behind this page are in [`docs/r1/networks87.md`](repo:docs/r1/networks87.md). + +## How the count works + +- **85 of 87 CLPR networks** have at least one mainnet in the catalogue. +- **93 mainnets** counts every mainnet the wallet ships, CLPR or not: 60 EVM networks (without "Hedera (EVM)"), one + each for Hedera, Solana, Bitcoin, Sui, Aptos, Cardano, Starknet, TON, NEAR, Stellar, Tezos and Algorand, five + Polkadot SDK networks (Polkadot, Kusama, both Asset Hubs, Chainflip), six Cosmos SDK networks (Osmosis, dYdX, + ZIGChain, Provenance, THORChain, Initia), TRON, XRP Ledger, MultiversX, Internet Computer, Stacks, Fuel, Bitcoin Cash, + and Vaulta, Telos (native) and XPR Network. Polkadot, Kusama and the Asset Hubs aren't CLPR networks; Telos counts + once as an EVM network and once natively. + +## Supported CLPR networks + +Network ids are CAIP-2 where a namespace exists (each chain package's README cites its sources). Dapp connectivity is +per family; every family also sends and receives in the wallet itself. "(new)" marks the networks added with the +twelve newer families and the Chainflip, STRATO and Arc additions. + +| Network | Module and mainnet network id | Dapp connectivity | Notes | +| --- | --- | --- | --- | +| Abstract | `chains-evm` `eip155:2741` | EIP-6963 / EIP-1193 | | +| Algorand | `chains-algorand` `algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k` | existing provider | | +| Anubis | `chains-evm` `eip155:6714` | EIP-6963 / EIP-1193 | | +| Arbitrum Nova | `chains-evm` `eip155:42170` | EIP-6963 / EIP-1193 | | +| Arbitrum One | `chains-evm` `eip155:42161` | EIP-6963 / EIP-1193 | | +| Arc (new) | `chains-evm` `eip155:5042` | EIP-6963 / EIP-1193 | Arc mainnet (5042) went live 2026-09-16; before, only the testnet was in the list | +| Aurora | `chains-evm` `eip155:1313161554` | EIP-6963 / EIP-1193 | | +| Avalanche C-Chain | `chains-evm` `eip155:43114` | EIP-6963 / EIP-1193 | | +| Base | `chains-evm` `eip155:8453` | EIP-6963 / EIP-1193 | | +| Bifrost Network | `chains-evm` `eip155:3068` | EIP-6963 / EIP-1193 | EVM layer | +| Bitcoin | `chains-bitcoin` `bip122:000000000019d6689c085ae165831e93` | existing provider | | +| Bitcoin Cash (new) | `chains-bitcoincash` `bip122:000000000000000000651ef99cb9fcbe` | WalletConnect wc2-bch-bcr (no injected standard) | | +| Bittensor | `chains-evm` `eip155:964` | EIP-6963 / EIP-1193 | EVM layer | +| Blast | `chains-evm` `eip155:81457` | EIP-6963 / EIP-1193 | | +| BNB Smart Chain | `chains-evm` `eip155:56` | EIP-6963 / EIP-1193 | | +| BOB | `chains-evm` `eip155:60808` | EIP-6963 / EIP-1193 | | +| BOT Chain | `chains-evm` `eip155:677` | EIP-6963 / EIP-1193 | | +| Cardano | `chains-cardano` `cip34:1-764824073` | existing provider | | +| Celo | `chains-evm` `eip155:42220` | EIP-6963 / EIP-1193 | | +| Chainflip (new) | `chains-substrate` `polkadot:8b8c140b0af9db70686583e3f6bf2a59` | window.injectedWeb3 (polkadot extension-dapp) | State Chain FLIP from Flip.Account; no FLIP transfer exists (redeem to Ethereum); LP-portal calls decoded | +| Conflux | `chains-evm` `eip155:1030` | EIP-6963 / EIP-1193 | Conflux eSpace (EVM); Core Space not supported | +| Core | `chains-evm` `eip155:1116` | EIP-6963 / EIP-1193 | | +| Cronos | `chains-evm` `eip155:25` | EIP-6963 / EIP-1193 | EVM layer | +| dYdX (new) | `chains-cosmos` `cosmos:dydx-mainnet-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 118 | +| Ethereum | `chains-evm` `eip155:1` | EIP-6963 / EIP-1193 | | +| Etherlink | `chains-evm` `eip155:42793` | EIP-6963 / EIP-1193 | EVM layer of Tezos smart rollups | +| Flare | `chains-evm` `eip155:14` | EIP-6963 / EIP-1193 | | +| Fraxtal | `chains-evm` `eip155:252` | EIP-6963 / EIP-1193 | | +| Fuel (new) | `chains-fuel` `fuel:9889` | FuelConnector event | | +| Gnosis Chain | `chains-evm` `eip155:100` | EIP-6963 / EIP-1193 | | +| GRX Chain | `chains-evm` `eip155:1110` | EIP-6963 / EIP-1193 | | +| Hydration | `chains-evm` `eip155:222222` | EIP-6963 / EIP-1193 | EVM layer | +| Hyperliquid | `chains-evm` `eip155:999` | EIP-6963 / EIP-1193 | HyperEVM | +| ICP (new) | `chains-icp` `icp:737ba355e855bd4b61279056603e0550` | none: send/receive only (ICRC-94 discovery still a draft) | no public transfer testnet: icp:test = mainnet limited to DFINITY test ledgers; replies not certificate-verified | +| Immutable zkEVM | `chains-evm` `eip155:13371` | EIP-6963 / EIP-1193 | | +| Initia (new) | `chains-cosmos` `cosmos:interwoven-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 60, ethsecp256k1: the EVM key | +| Injective | `chains-evm` `eip155:1776` | EIP-6963 / EIP-1193 | EVM layer | +| Ink | `chains-evm` `eip155:57073` | EIP-6963 / EIP-1193 | | +| Kaia | `chains-evm` `eip155:8217` | EIP-6963 / EIP-1193 | Kaia is EVM (8217) | +| Katana | `chains-evm` `eip155:747474` | EIP-6963 / EIP-1193 | | +| Kava | `chains-evm` `eip155:2222` | EIP-6963 / EIP-1193 | EVM layer | +| KUB Chain | `chains-evm` `eip155:96` | EIP-6963 / EIP-1193 | | +| Linea | `chains-evm` `eip155:59144` | EIP-6963 / EIP-1193 | | +| Mantle | `chains-evm` `eip155:5000` | EIP-6963 / EIP-1193 | | +| MANTRA | `chains-evm` `eip155:5888` | EIP-6963 / EIP-1193 | EVM layer | +| MegaETH | `chains-evm` `eip155:4326` | EIP-6963 / EIP-1193 | | +| Mezo | `chains-evm` `eip155:31612` | EIP-6963 / EIP-1193 | EVM layer | +| Monad | `chains-evm` `eip155:143` | EIP-6963 / EIP-1193 | | +| Morph | `chains-evm` `eip155:2818` | EIP-6963 / EIP-1193 | | +| MultiversX (new) | `chains-multiversx` `mvx:1` | best effort: sdk-dapp's undocumented window.multiversx.providers hook | | +| NEAR | `chains-near` `near:mainnet` | existing provider | | +| OP Mainnet | `chains-evm` `eip155:10` | EIP-6963 / EIP-1193 | | +| Osmosis (new) | `chains-cosmos` `cosmos:osmosis-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 118 | +| Plasma | `chains-evm` `eip155:9745` | EIP-6963 / EIP-1193 | | +| Plume | `chains-evm` `eip155:98866` | EIP-6963 / EIP-1193 | | +| Polygon PoS | `chains-evm` `eip155:137` | EIP-6963 / EIP-1193 | | +| Provenance (new) | `chains-cosmos` `cosmos:pio-mainnet-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 505 (Keplr's testnet uses 1: different key there) | +| PulseChain | `chains-evm` `eip155:369` | EIP-6963 / EIP-1193 | | +| Reya | `chains-evm` `eip155:1729` | EIP-6963 / EIP-1193 | | +| RISE | `chains-evm` `eip155:4153` | EIP-6963 / EIP-1193 | | +| Robinhood Chain | `chains-evm` `eip155:4663` | EIP-6963 / EIP-1193 | | +| Ronin | `chains-evm` `eip155:2020` | EIP-6963 / EIP-1193 | | +| Rootstock | `chains-evm` `eip155:30` | EIP-6963 / EIP-1193 | | +| Scroll | `chains-evm` `eip155:534352` | EIP-6963 / EIP-1193 | | +| Sei | `chains-evm` `eip155:1329` | EIP-6963 / EIP-1193 | EVM layer | +| Solana | `chains-solana` `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | existing provider | | +| Soneium | `chains-evm` `eip155:1868` | EIP-6963 / EIP-1193 | | +| Stable | `chains-evm` `eip155:988` | EIP-6963 / EIP-1193 | EVM layer | +| Stacks (new) | `chains-stacks` `stacks:1` | SIP-030 + WBIP-004 | | +| Starknet | `chains-starknet` `starknet:SN_MAIN` | existing provider | | +| Stellar | `chains-stellar` `stellar:pubnet` | existing provider | | +| STRATO (new) | `chains-evm` `eip155:123354377739506` | EIP-6963 / EIP-1193 | SolidVM, but nodes take signed legacy Ethereum transactions; flat 0.01 USDST fee per transaction | +| Telos | `chains-evm` `eip155:40` | EIP-6963 / EIP-1193 | Telos EVM (and Telos native in chains-antelope) | +| Tezos | `chains-tezos` `tezos:NetXdQprcVkpaWU` | existing provider | | +| THORChain (new) | `chains-cosmos` `cosmos:thorchain-1` | Keplr-compatible API at window.clipwallet.cosmos | mainnet only: stagenet runs on real funds | +| TON | `chains-ton` `ton:-239` | existing provider | | +| TRON (new) | `chains-tron` `tron:0x2b6653dc` | TIP-1193 + TIP-6963 | | +| Unichain | `chains-evm` `eip155:130` | EIP-6963 / EIP-1193 | | +| Vaulta (new) | `chains-antelope` `antelope:aca376f206b8fc25a6ed44dbdc66547c` | none: send/receive only | accounts are names created on chain; receive needs one for this key | +| World Chain | `chains-evm` `eip155:480` | EIP-6963 / EIP-1193 | | +| X Layer | `chains-evm` `eip155:196` | EIP-6963 / EIP-1193 | | +| XPR Network (new) | `chains-antelope` `antelope:384da888112027f0321850a169f737c3` | none: send/receive only | accounts are names created on chain | +| XRP Ledger (new) | `chains-xrpl` `xrpl:0` | XLS-72d Wallet Standard (no message signing) | | +| ZIGChain (new) | `chains-cosmos` `cosmos:zigchain-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 118 | +| ZKsync Era | `chains-evm` `eip155:324` | EIP-6963 / EIP-1193 | | + +## Families and derivation paths + +Every derivation is in `@clip-wallet/vault`, the only package that touches keys, and is cross-checked for the public +"abandon … about" test phrase against each ecosystem's own SDK +([`packages/vault/test/families87.test.ts`](repo:packages/vault/test/families87.test.ts)). Each chain module serializes +transactions itself and is checked byte for byte against the official SDK, which is a development dependency only; a +module's runtime dependencies are `@clip-wallet/core`, `@noble/*` and `@scure/base`. + +| Family (vault) | Path (account `i`) | Package | Mainnets / testnets | Dapp standard in 1Mask | +| --- | --- | --- | --- | --- | +| `cosmos` | `m/44'/118'/0'/0/i` | `chains-cosmos` | Osmosis, dYdX, ZIGChain / osmo-test-5, dydx-testnet-4, zig-test-2 | Keplr-compatible API (`enable`, `getKey`, `signDirect`, `signAmino`, `signArbitrary`, `verifyArbitrary`, `sendTx`, offline signers) at `window.clipwallet.cosmos`; never `window.keplr` | +| `provenance` | `m/44'/505'/0'/0/i` | `chains-cosmos` | Provenance / pio-testnet-1 | same | +| `thorchain` | `m/44'/931'/0'/0/i` | `chains-cosmos` | THORChain / none (stagenet uses real funds) | same | +| `initia` | `m/44'/60'/0'/0/i` (ethsecp256k1) | `chains-cosmos` | Initia / initiation-2 | same | +| `tron` | `m/44'/195'/0'/0/i` | `chains-tron` | TRON / Nile, Shasta | TIP-1193 provider announced with TIP-6963; TronWeb-shaped signing subset (`trx.sign`, `signMessageV2`) | +| `xrpl` | `m/44'/144'/i'/0/0` | `chains-xrpl` | XRP Ledger / testnet, devnet | XLS-72d (Wallet Standard `xrpl:signTransaction`, `xrpl:signAndSubmitTransaction`); no message signing in the standard | +| `antelope` | `m/44'/194'/0'/0/i` (K1, canonical signatures) | `chains-antelope` | Vaulta, Telos, XPR Network / Jungle4, Telos testnet, XPR testnet | none: send/receive only (WharfKit wallet plugins live in the dapp; Anchor Link/ESR and Scatter injection would mean posing as Anchor or Scatter) | +| `multiversx` | `m/44'/508'/0'/0'/i'` | `chains-multiversx` | MultiversX / devnet, testnet | best effort: an entry on sdk-dapp's `window.multiversx.providers` custom-provider hook under Clip's own name. **Undocumented and subject to change**; it only works on dapps that don't reset `window.multiversx` (MultiversX's own template dapp does). WalletConnect `mvx_*` methods are handled by the module | +| `icp` | `m/44'/223'/0'/0/i` | `chains-icp` | Internet Computer / `icp:test` (mainnet limited to DFINITY test ledgers) | none: ICRC-94 (extension discovery) is still a draft; Plug's `window.ic.plug` is proprietary | +| `stacks` | `m/44'/5757'/0'/0/i` | `chains-stacks` | Stacks / testnet | SIP-030 `request` on `window.clipwallet.stacks`, registered on `window.wbip_providers` (WBIP-004) | +| `fuel` | `m/44'/1179993420'/i'/0/0` | `chains-fuel` | Fuel Ignition / testnet | FuelConnector announced with the `FuelConnector` event, dependency-free | +| `bitcoincash` | `m/44'/145'/0'/0/i` | `chains-bitcoincash` | Bitcoin Cash / chipnet, testnet4 | WalletConnect `bch` namespace (wc2-bch-bcr, as Cashonize/Paytaca); no injected standard | +| `substrate` (existing) | existing | `chains-substrate` | + Chainflip / Perseverance | existing `window.injectedWeb3` (what lp.chainflip.io uses) | +| `evm` (existing) | existing | `chains-evm` | + STRATO, Arc / STRATO Helium | existing EIP-6963 | + +The dapp side of each family: [Clip works with your dapp](../dapps/). + +## Not supported + +### Canton + +Canton is a network of participant (validator) nodes that host parties. Canton 3.x has "external parties" whose key is +held outside the node, and CIP-0103 defines a dapp API for wallets, but every read of a party's contracts and every +submission (prepare, sign the hash, execute) goes through the Ledger API of a participant that hosts the party, behind +that operator's authentication, with no public endpoint. A wallet can hold the key but can't see or move anything +without an operator relationship. + +**Closest honest support:** none in Clip Wallet as shipped. A kit-built wallet whose operator runs (or contracts) a +validator could add a family that talks to its own participant through a CIP-0103 wallet gateway: an operator product, +not a wallet derived from a recovery phrase. + +### Mixin + +Mixin's kernel is a UTXO DAG with CryptoNote-style one-time output keys. Its public RPC has no address or key index, +so a wallet can't find its own outputs without scanning every snapshot with the view key, and there is no BIP-39 +derivation standard for kernel keys. User funds live in Mixin's account system (Mixin Messenger and Safe, with +registered accounts and keys behind Mixin's API). + +**Closest honest support:** none. XIN as an ERC-20 on Ethereum is an asset, not the network. + +## Limits worth knowing + +- **STRATO** works through its Ethereum JSON-RPC: legacy (EIP-155) transactions only, gas unpriced, and a flat + 0.01 USDST (or voucher) per transaction that the approval shows as its own line. Contract calls are matched to + SolidVM functions by selector; STRATO's EIP-712 "function call" transactions are out of scope. +- **Arc** mainnet launched on 2026-09-16; its native gas is USDC (18 decimals on the EVM side). +- **Antelope** accounts (Vaulta, Telos, XPR Network) must be created on chain by someone, because they cost RAM. Clip + finds the accounts that use its key and says plainly when there is none yet. No dapp connector: send and receive only. +- **Internet Computer** replies aren't certificate-verified (no BLS check); nothing security-relevant relies on them. + An ICP transfer must be approved within 4 minutes (ingress expiry). There is no public transfer testnet: `icp:test` + is mainnet limited to DFINITY's test ledgers. No dapp connector: send and receive only. +- **THORChain** has no public testnet (stagenet runs on real funds). Swaps (deposit memos) decode as blind. +- **Provenance and Initia testnets:** Keplr derives pio-testnet-1 at coin type 1 and initiation-2 at 118; Clip uses the + mainnet coin types (505, 60) everywhere, so those testnet accounts differ from Keplr's. +- **Chainflip** has no FLIP transfer between State Chain accounts; liquidity-provider order calls are shown as the raw + call with a caution. +- **MultiversX** dapp connectivity is best effort, through an undocumented sdk-dapp hook: see + [MultiversX](../dapps/multiversx.md). diff --git a/apps/docs/src/security/vault-crypto.md b/apps/docs/src/security/vault-crypto.md index 47e58734..56e6cced 100644 --- a/apps/docs/src/security/vault-crypto.md +++ b/apps/docs/src/security/vault-crypto.md @@ -50,9 +50,10 @@ PRF input plays the same role. On macOS desktop, Touch ID releases a device secr XChaCha20-Poly1305(entropy)`, with the header as associated data, under a key from a fixed backup PRF input. The phrase never leaves the vault. The [backup service](../services/backup.md) stores the blob and can't open it. -## Key derivation for fourteen families +## Key derivation for every family -BIP-32 for secp256k1, SLIP-10 for ed25519, BIP32-Ed25519 for Cardano and Algorand, sr25519 for Polkadot SDK chains, -and the Stark curve's key grinding for Starknet. The paths match each ecosystem's popular wallets; see +BIP-32 for secp256k1 (Initia's ethsecp256k1 included), SLIP-10 for ed25519, BIP32-Ed25519 for Cardano and Algorand, +sr25519 for Polkadot SDK chains, and the Stark curve's key grinding for Starknet; Antelope signatures are made +canonical, as its chains require. The paths match each ecosystem's popular wallets; see [Vault](../architecture/vault.md#derivation). Every family is checked against its own SDK in the tests, with public test vectors only. diff --git a/apps/docs/src/snippets/dapps/cosmos.ts b/apps/docs/src/snippets/dapps/cosmos.ts new file mode 100644 index 00000000..98a4d9ca --- /dev/null +++ b/apps/docs/src/snippets/dapps/cosmos.ts @@ -0,0 +1,30 @@ +// Cosmos SDK chains: Clip's Keplr-compatible provider is window.clipwallet.cosmos (never window.keplr). A dapp written +// for Keplr swaps `window.keplr` for it; the offline signer goes to cosmjs as usual. +interface CosmosKey { + name: string; + algo: string; + pubKey: Uint8Array; + bech32Address: string; +} +interface StdSignature { + pub_key: { type: string; value: string }; + signature: string; +} +interface CosmosProvider { + enable(chainIds: string | string[]): Promise; + getKey(chainId: string): Promise; + signArbitrary(chainId: string, signer: string, data: string | Uint8Array): Promise; + getOfflineSigner(chainId: string): unknown; // an OfflineDirectSigner for SigningStargateClient.connectWithSigner + on(event: "keystorechange", listener: () => void): () => void; +} + +export async function connectCosmos(chainId = "osmo-test-5") { + const cosmos = (window as unknown as { clipwallet?: { cosmos?: CosmosProvider } }).clipwallet?.cosmos; + if (!cosmos) throw new Error("Clip Wallet isn't installed in this browser."); + await cosmos.enable(chainId); // the connect approval + const key = await cosmos.getKey(chainId); // key.bech32Address: osmo1… + // ADR-36 sign-in message, verifiable with @cosmjs/crypto or Keplr's verifyADR36Amino + const signature = await cosmos.signArbitrary(chainId, key.bech32Address, "Sign in to example.app"); + const signer = cosmos.getOfflineSigner(chainId); + return { address: key.bech32Address, signature, signer }; +} diff --git a/apps/docs/src/snippets/dapps/fuel.ts b/apps/docs/src/snippets/dapps/fuel.ts new file mode 100644 index 00000000..fcb0dfa9 --- /dev/null +++ b/apps/docs/src/snippets/dapps/fuel.ts @@ -0,0 +1,17 @@ +// Fuel: Clip announces a FuelConnector with the "FuelConnector" window event, so fuels-ts `new Fuel()` lists it. +// It is also at window.clipwallet.fuel, for `new Fuel({ connectors: [window.clipwallet.fuel] })`. +interface FuelConnectorLike { + name: string; + connect(): Promise; + accounts(): Promise; + signMessage(address: string, message: string): Promise; +} + +export async function connectFuel() { + const fuel = (window as unknown as { clipwallet?: { fuel?: FuelConnectorLike } }).clipwallet?.fuel; + if (!fuel) throw new Error("Clip Wallet isn't installed in this browser."); + await fuel.connect(); + const [address] = await fuel.accounts(); // a 0x… b256 address + const signature = await fuel.signMessage(address!, "Sign in to example.app"); // Signer.recoverAddress(hashMessage(…)) + return { address, signature }; +} diff --git a/apps/docs/src/snippets/dapps/stacks.ts b/apps/docs/src/snippets/dapps/stacks.ts new file mode 100644 index 00000000..309d4122 --- /dev/null +++ b/apps/docs/src/snippets/dapps/stacks.ts @@ -0,0 +1,20 @@ +// Stacks: Clip's SIP-030 provider is window.clipwallet.stacks, registered on window.wbip_providers (WBIP-004), which +// is where @stacks/connect 8 finds wallets. Without @stacks/connect, call it directly: +interface Sip030Response { + jsonrpc: "2.0"; + id: number; + result: T; +} +interface StacksProvider { + request(method: string, params?: unknown): Promise>; +} + +export async function connectStacks() { + const stacks = (window as unknown as { clipwallet?: { stacks?: StacksProvider } }).clipwallet?.stacks; + if (!stacks) throw new Error("Clip Wallet isn't installed in this browser."); + const { result } = await stacks.request<{ addresses: { symbol: string; address: string; publicKey: string }[] }>("stx_getAddresses", { network: "testnet" }); + const address = result.addresses[0]!.address; // ST… on testnet + const signed = await stacks.request<{ signature: string; publicKey: string }>("stx_signMessage", { message: "Sign in to example.app", network: "testnet" }); + const sent = await stacks.request<{ txid: string }>("stx_transferStx", { recipient: address, amount: "1", network: "testnet" }); + return { address, signature: signed.result.signature, txid: sent.result.txid }; +} diff --git a/apps/docs/src/snippets/dapps/tron.ts b/apps/docs/src/snippets/dapps/tron.ts new file mode 100644 index 00000000..8d6f0d0e --- /dev/null +++ b/apps/docs/src/snippets/dapps/tron.ts @@ -0,0 +1,37 @@ +// TRON: Clip announces a TIP-1193 provider with TIP-6963 under its own name (it never sets window.tronLink or +// window.tronWeb). Find it by its rdns, connect, then sign with the TronWeb subset it carries. +interface TronProvider { + request(args: { method: string; params?: unknown }): Promise; + tronWeb: { + defaultAddress: { base58: string | false }; + trx: { sign(transaction: unknown): Promise; signMessageV2(message: string): Promise }; + }; + on(event: "accountsChanged" | "chainChanged" | "disconnect", listener: (...args: unknown[]) => void): unknown; +} +interface AnnounceDetail { + info: { uuid: string; name: string; icon: string; rdns: string }; + provider: TronProvider; +} + +export function findTronWallet(rdns = "org.coldai.clipwallet"): Promise { + return new Promise((resolve, reject) => { + const onAnnounce = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (detail?.info.rdns !== rdns) return; + window.removeEventListener("TIP6963:announceProvider", onAnnounce); + resolve(detail.provider); + }; + window.addEventListener("TIP6963:announceProvider", onAnnounce); + window.dispatchEvent(new Event("TIP6963:requestProvider")); + setTimeout(() => reject(new Error("No TIP-6963 wallet with that rdns.")), 1000); + }); +} + +export async function connectTron() { + const tron = await findTronWallet(); + const [address] = (await tron.request({ method: "eth_requestAccounts" })) as string[]; // TIP-1102 + const chainId = await tron.request({ method: "eth_chainId" }); // "0xcd8690dc" on Nile + const signature = await tron.tronWeb.trx.signMessageV2("Sign in to example.app"); // TronWeb Trx.verifyMessageV2 + // Transactions: build them with your own TronWeb, then tron.tronWeb.trx.sign(transaction). + return { address, chainId, signature }; +} diff --git a/apps/docs/src/snippets/kit/inpage.content.ts b/apps/docs/src/snippets/kit/inpage.content.ts index 6b42b406..81011360 100644 --- a/apps/docs/src/snippets/kit/inpage.content.ts +++ b/apps/docs/src/snippets/kit/inpage.content.ts @@ -1,4 +1,4 @@ -// src/entrypoints/inpage.content.ts: 1Mask's providers for all 14 families (MAIN world), under your identity. +// src/entrypoints/inpage.content.ts: 1Mask's providers for every family (MAIN world), under your identity. import { defineContentScript } from "wxt/utils/define-content-script"; import { CONTENT_MATCHES, installInpage } from "@clip-wallet/extension-kit/inpage"; diff --git a/apps/docs/src/testing/matrices.md b/apps/docs/src/testing/matrices.md index 9735cce6..843b9640 100644 --- a/apps/docs/src/testing/matrices.md +++ b/apps/docs/src/testing/matrices.md @@ -5,7 +5,7 @@ Clip-specific on the dapp side. ## The dapp matrix -For each of the fourteen families, that ecosystem's own dapp library in a local page talks to the real public testnet. +For each family with a dapp standard, that ecosystem's own dapp library in a local page talks to the real public testnet. | Level | Passes when | | --- | --- | diff --git a/apps/extension/clip.config.ts b/apps/extension/clip.config.ts index 95cce2c3..efebfab4 100644 --- a/apps/extension/clip.config.ts +++ b/apps/extension/clip.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ // ColdAI orange; white text on orange buttons is the owner's preference (contrast 3.6:1 ≥ the schema's 3:1). theme: { accent: "#FF3C00", accentText: "#FFFFFF", font: "Inter", radius: 14 }, // All 14 families (testnets; mainnet stays gated below). - networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand"], + networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", "cosmos", "provenance", "thorchain", "initia", "tron", "xrpl", "antelope", "multiversx", "icp", "stacks", "fuel", "bitcoincash"], // rpOrigin unset: the extension's own origin is the WebAuthn RP (Chrome 122+). Set an https origin you // own (and add it to host_permissions) to keep passkeys stable across extension ids and browsers. passkeys: { enabled: true }, diff --git a/apps/extension/e2e/matrix.spec.ts b/apps/extension/e2e/matrix.spec.ts index de793bc9..2dfc8cde 100644 --- a/apps/extension/e2e/matrix.spec.ts +++ b/apps/extension/e2e/matrix.spec.ts @@ -126,6 +126,8 @@ async function openDapp(context: BrowserContext, dapp: string): Promise { }); const errors: string[] = []; page.on("pageerror", (e) => errors.push(e.message)); + // MATRIX_DEBUG=1: the dapp page's console in the test output (no secrets reach a dapp page). + if (process.env.MATRIX_DEBUG) page.on("console", (m) => process.stdout.write(`[${dapp} page] ${m.type()}: ${m.text().slice(0, 300)}\n`)); await page.goto(`${ORIGIN}/`); await page.waitForFunction(() => !!(window as unknown as { __matrix?: unknown }).__matrix, undefined, { timeout: 30_000 }).catch(() => { throw new Error(`${dapp} dapp didn't start: ${errors.join(" | ") || "no page error"}`); @@ -216,8 +218,10 @@ interface Spec { recipient?: RegExp; /** Message signing isn't part of this ecosystem's dapp API. */ noSign?: string; + /** L3/L4 is a call, not a transfer (Chainflip has no FLIP transfer): the approval title must match this instead. */ + call?: RegExp; } -const SPECS: Record = { +const SPECS: Partial> = { evm: { amount: "0.000000000000000001", symbol: "ETH" }, "hedera-evm": { amount: "0.00000001", symbol: "HBAR" }, // Hedera refuses transfers to yourself, so this one pays the matrix's EVM-path account (alias 0x05AC…). @@ -235,6 +239,17 @@ const SPECS: Record = { stellar: { amount: "0.0000001", symbol: "XLM" }, tezos: { amount: "0.000001", symbol: "XTZ" }, algorand: { amount: "0.000001", symbol: "ALGO", noSign: "Algorand's dapp APIs (use-wallet v5, ARC-1) have no message signing; ARC-60 signData is a draft Clip doesn't offer." }, + // networks87 + cosmos: { amount: "0.000001", symbol: "OSMO" }, + // java-tron refuses transfers to yourself: 1 sun to the Nile black-hole address (it exists, so no opening fee). + tron: { amount: "0.000001", symbol: "TRX", recipient: /T9yD14/ }, + // 1 µSTX to the testnet burn address (stx_transferStx refuses the sender as recipient). + stacks: { amount: "0.000001", symbol: "STX", recipient: /ST0000/ }, + fuel: { amount: "0.000000001", symbol: "ETH" }, + chainflip: { amount: "", symbol: "FLIP", call: /Register as a Chainflip liquidity provider/ }, + multiversx: { amount: "0.000000000000000001", symbol: "EGLD" }, + // rippled refuses an XRP payment to yourself (temREDUNDANT): L3 is a no-op AccountSet with a memo. + xrpl: { amount: "", symbol: "XRP", call: /Change your account settings/, noSign: "XLS-72d (the XRPL browser wallet standard) has no message signing." }, }; /** Hedera over WalletConnect answers with the 0.0.x account id: it must be the account behind our key's EVM alias. */ @@ -267,7 +282,7 @@ async function runTarget(target: Target, context: BrowserContext, extensionId: s } test.skip(!!why, why ?? ""); } - const spec = SPECS[target]; + const spec = SPECS[target]!; const net = TARGETS[target].network; const expected = addressOf(target); // The chain module derives the same address from the vault's public key (what the wallet shows on Receive). @@ -333,10 +348,14 @@ async function runTarget(target: Target, context: BrowserContext, extensionId: s const short = expected.slice(0, 6); const recipientShown = spec.recipient ? spec.recipient.test(text) : text.includes(short) || /yourself|your own|to you\b/i.test(text); const problems: string[] = []; - if (!text.includes(spec.symbol)) problems.push(`no ${spec.symbol}`); - // Amounts below the display precision show as "<0.000001 ETH" (the wallet's formatting); either form is right. - if (!text.includes(spec.amount) && !new RegExp(`<0\\.0*1\\s*${spec.symbol}`).test(text)) problems.push(`amount ${spec.amount} not shown`); - if (!recipientShown) problems.push(`recipient ${spec.recipient ?? `${short}…`} not shown`); + if (spec.call) { + if (!spec.call.test(view.title)) problems.push(`title "${view.title}" isn't ${spec.call}`); + } else { + if (!text.includes(spec.symbol)) problems.push(`no ${spec.symbol}`); + // Amounts below the display precision show as "<0.000001 ETH" (the wallet's formatting); either form is right. + if (!text.includes(spec.amount) && !new RegExp(`<0\\.0*1\\s*${spec.symbol}`).test(text)) problems.push(`amount ${spec.amount} not shown`); + if (!recipientShown) problems.push(`recipient ${spec.recipient ?? `${short}…`} not shown`); + } if (view.overflowing.length) problems.push(`row values overflow their label: ${view.overflowing.join(", ")}`); const fundsReason = view.notices.find((n) => /enough|can't pay|no \S+ yet|isn't on \S+ yet|doesn't exist (?:on \S+ )?yet|receive some \S+ first/i.test(n)); if (/unreadable|blind/i.test(view.title) && !funded && fundsReason) { diff --git a/apps/extension/e2e/matrix/addresses.json b/apps/extension/e2e/matrix/addresses.json index de11b2cd..6151ed87 100644 --- a/apps/extension/e2e/matrix/addresses.json +++ b/apps/extension/e2e/matrix/addresses.json @@ -1,5 +1,5 @@ { - "//": "Public receive addresses of the dapp matrix wallet (account 0 per family, testnet), derived from DAPP_MATRIX_MNEMONIC with ClipVault defaults, exactly as the extension background derives them. The phrase lives only in the git-excluded .env.dapp-matrix. matrix.spec.ts checks every dapp connect returns these.", + "//": "Public receive addresses of the dapp matrix wallet (account 0 per family, testnet), derived from DAPP_MATRIX_MNEMONIC with ClipVault defaults, exactly as the extension background derives them. The phrase lives only in the git-excluded .env.dapp-matrix. matrix.spec.ts checks every dapp connect returns these. networks87 entries are keyed by matrix target; 'address' is the spelling on the target's testnet, 'vaultAddress' the vault's own (mainnet) spelling when it differs.", "evm": { "address": "0x05ACD02A8E18c130D902FB732bb6AD22DA4f2717", "publicKey": "03d8ac1935c3bae4c197bd79e641ec34d576d58fb3c3fcf0dbc466b6a34b342928", @@ -70,5 +70,77 @@ "address": "4BSAZBB73QMGRVOUSB7YIFKHKKZ5CDBH2PWDTZGWSLCXU7ID67DLM2C7V4", "publicKey": "e0640c843fdc1868d5d4907f84154752b3d10c27d3ec39e4d692c57a7d03f7c6", "path": "m/44'/283'/0'/0/0" + }, + "cosmos": { + "address": "osmo1re86ed5ck45dnyex8ajvytrae4c5ct7xjm6edd", + "publicKey": "02996ee5775c1a4a3768f070349d88b29fae8fea7d893e39cd92f5258a7ee3fe75", + "path": "m/44'/118'/0'/0/0", + "vaultAddress": "cosmos1re86ed5ck45dnyex8ajvytrae4c5ct7x6qffml", + "dydx": "dydx1re86ed5ck45dnyex8ajvytrae4c5ct7xne8dmg", + "zig": "zig1re86ed5ck45dnyex8ajvytrae4c5ct7xm2vmeh" + }, + "provenance": { + "address": "tp16f3hw77j3vwa3chtnplx3qs4sn2z4ww0cdzgzd", + "publicKey": "0259c124b3fa12acf5a0eada8a480e3741a61262c2c74c7a90a5add75aea642edb", + "path": "m/44'/505'/0'/0/0", + "vaultAddress": "pb16f3hw77j3vwa3chtnplx3qs4sn2z4ww0tx8yq8" + }, + "thorchain": { + "address": "thor13pajudku86vd59c8wkr4ctwcxdy8qpnap6afpx", + "publicKey": "0312ef34472f7bda3a6897e21c526a629c396b9b777d7e1282d66664f3ce1f5036", + "path": "m/44'/931'/0'/0/0" + }, + "initia": { + "address": "init1qkkdq25wrrqnpkgzldejhd4dytdy7fch0qn2v7", + "publicKey": "03d8ac1935c3bae4c197bd79e641ec34d576d58fb3c3fcf0dbc466b6a34b342928", + "path": "m/44'/60'/0'/0/0" + }, + "tron": { + "address": "TGZxYSGcQHaZMcSB2c7ZB1n97yMk2UPz41", + "publicKey": "03ae797c9eb51a0664d6d6db08b7aa2093fea8b67ff7bdc32c372aec96c0e7142e", + "path": "m/44'/195'/0'/0/0" + }, + "xrpl": { + "address": "rDvWLtQL3Furv3sVzERai1MXvKFTQjZgrM", + "publicKey": "03fe696b09c509d16c13df23756973df369940903dd7981b31c5b5289ef44407b1", + "path": "m/44'/144'/0'/0/0" + }, + "antelope": { + "address": "PUB_K1_7VxUN24cMwi94AHSbLkSnbKNfwoN8wjV4Fv1FA8W7bXQdTVzro", + "publicKey": "0357ecf6e9045a00598d81f83b9e818b9dd3938b1473b9fea160cb284fc2af8e81", + "path": "m/44'/194'/0'/0/0" + }, + "multiversx": { + "address": "erd1qhkcc53w80nppnsprtzs7k2wh7xwvt54c8u88d0cd6pn3n6ldudshmqgfg", + "publicKey": "05ed8c522e3be610ce011ac50f594ebf8ce62e95c1f873b5f86e8338cf5f6f1b", + "path": "m/44'/508'/0'/0'/0'" + }, + "icp": { + "address": "gm6de-vncl6-w4cya-5z35g-cincb-f2zqe-6jhln-cnvjw-gvqf6-3shog-6ae", + "publicKey": "024441fa83b13a740213901f59b896d13723c08a4970c905026b5352a4e1537c98", + "path": "m/44'/223'/0'/0/0" + }, + "stacks": { + "address": "ST1EC4TTQPQX24TT3VY2H6YYW99N0HRQ5VMRB2M1T", + "publicKey": "02eeedf4fbb22fb0a2d49fdcea998350db0533f76719fafde4c17d0484a0dd8b39", + "path": "m/44'/5757'/0'/0/0", + "vaultAddress": "SP1EC4TTQPQX24TT3VY2H6YYW99N0HRQ5VQXK4TEJ" + }, + "fuel": { + "address": "0x1Cc3e8E1B340846d9Eb275D51435189deDe13f20801bbc3693B2D04c1B322A75", + "publicKey": "029369f709755465d14dcfae2ac912f7c8030e6e91c9d16f15ec3faae5fabe9083", + "path": "m/44'/1179993420'/0'/0/0" + }, + "bitcoincash": { + "address": "bchtest:qq5jt8xqr6gckvpnn4dr4ajfd023mf7fzcj5tplmu2", + "publicKey": "0294f196fc1ba00ed17aa43b05b6e047e7ee2e958067bc76e6b91efd2dcbfd9de4", + "path": "m/44'/145'/0'/0/0", + "vaultAddress": "bitcoincash:qq5jt8xqr6gckvpnn4dr4ajfd023mf7fzckx0xavmk" + }, + "chainflip": { + "address": "cFNXs2hYYRhGp1A4AfXaTbK3vQSgnSLvtvJrvJrCbNvhtTKc1", + "publicKey": "ce36fef24a0cbca4fff77554417922d15290a8296c08ef0759845140c1eab52d", + "path": "", + "vaultAddress": "5Gj64MPAV7C2ezdS5tpq4fVgDYteHCptB62RFxvaVyECHCQb" } } diff --git a/apps/extension/e2e/matrix/chain.ts b/apps/extension/e2e/matrix/chain.ts index 1ba28635..bea77bea 100644 --- a/apps/extension/e2e/matrix/chain.ts +++ b/apps/extension/e2e/matrix/chain.ts @@ -21,19 +21,41 @@ import { NEAR_TESTNET, createNearModule } from "@clip-wallet/chains-near"; import { STELLAR_TESTNET, createStellarModule } from "@clip-wallet/chains-stellar"; import { TEZOS_SHADOWNET, createTezosModule } from "@clip-wallet/chains-tezos"; import { ALGORAND_TESTNET, createAlgorandModule } from "@clip-wallet/chains-algorand"; +import { INITIA_TESTNET, OSMOSIS_TESTNET, PROVENANCE_TESTNET, createCosmosModule } from "@clip-wallet/chains-cosmos"; +import { TRON_NILE, createTronModule } from "@clip-wallet/chains-tron"; +import { STACKS_TESTNET, createStacksModule } from "@clip-wallet/chains-stacks"; +import { FUEL_TESTNET, createFuelModule } from "@clip-wallet/chains-fuel"; +import { BCH_CHIPNET, createBitcoinCashModule } from "@clip-wallet/chains-bitcoincash"; +import { MULTIVERSX_DEVNET, createMultiversXModule } from "@clip-wallet/chains-multiversx"; +import { ICP_TEST, createIcpModule } from "@clip-wallet/chains-icp"; +import { XRPL_TESTNET, createXrplModule } from "@clip-wallet/chains-xrpl"; +import { JUNGLE4, createAntelopeModule } from "@clip-wallet/chains-antelope"; import ADDRESSES from "./addresses.json" with { type: "json" }; export type MatrixFamily = Exclude; -/** "hedera-evm" = Hedera testnet reached through 1Mask's EIP-1193 provider (the EVM account, chain 296). */ -export type Target = Family | "hedera-evm"; +/** + * "hedera-evm" = Hedera testnet reached through 1Mask's EIP-1193 provider (the EVM account, chain 296). + * networks87: "cosmos" is Osmosis testnet (osmo-test-5), "chainflip" the Chainflip Perseverance testnet (substrate + * family), "multiversx" devnet through sdk-dapp's undocumented custom-provider hook. Provenance, Initia, ICP, Bitcoin + * Cash and Antelope are funding/balance targets only: no injected dapp standard Clip can answer under its own identity + * (Bitcoin Cash's is WalletConnect; docs/r1/networks87.md), so they have no dapp page. + */ +export type Target = + | "evm" | "hedera-evm" | "hedera" | "solana" | "bitcoin" | "sui" | "aptos" | "cardano" | "substrate" | "starknet" | "ton" | "near" | "stellar" | "tezos" | "algorand" + | "cosmos" | "tron" | "stacks" | "fuel" | "chainflip" | "xrpl" + | "multiversx" | "provenance" | "initia" | "icp" | "bitcoincash" | "antelope"; interface Entry { address: string; publicKey: string; path: string; taproot?: string; + /** The vault's own spelling when the target network spells it differently (Cosmos prefixes, Stacks ST, bchtest:, cF…). */ + vaultAddress?: string; } -const ADDR = ADDRESSES as unknown as Record; +const ADDR = ADDRESSES as unknown as Record; +/** addresses.json entry: per target where it has one (networks87), else per family. */ +const entryOf = (t: Target): Entry => ADDR[t] ?? ADDR[TARGETS[t].family]!; const SEPOLIA = EVM_NETWORKS.find((n) => n.chainId === 11155111)!; const HEDERA_EVM = HEDERA_EVM_NETWORKS.find((n) => n.chainId === 296)!; @@ -55,29 +77,45 @@ export const TARGETS: Record ChainModu stellar: { network: STELLAR_TESTNET, module: createStellarModule, family: "stellar", minimum: 15_000_000n, faucetHint: "1.5 XLM (account reserve 1 XLM + fees)" }, tezos: { network: TEZOS_SHADOWNET, module: createTezosModule, family: "tezos", minimum: 1_000_000n, faucetHint: "1 XTZ (shadownet; first op reveals the key)" }, algorand: { network: ALGORAND_TESTNET, module: createAlgorandModule, family: "algorand", minimum: 200_000n, faucetHint: "0.2 ALGO (min balance 0.1 + fees)" }, + // networks87 + cosmos: { network: OSMOSIS_TESTNET, module: () => createCosmosModule({ family: "cosmos" }), family: "cosmos", minimum: 100_000n, faucetHint: "0.1 OSMO (osmo-test-5)" }, + tron: { network: TRON_NILE, module: createTronModule, family: "tron", minimum: 2_000_000n, faucetHint: "2 TRX (Nile)" }, + stacks: { network: STACKS_TESTNET, module: createStacksModule, family: "stacks", minimum: 10_000n, faucetHint: "0.01 STX (testnet)" }, + fuel: { network: FUEL_TESTNET, module: createFuelModule, family: "fuel", minimum: 200n, faucetHint: "0.0000002 ETH (Fuel testnet)" }, + xrpl: { network: XRPL_TESTNET, module: createXrplModule, family: "xrpl", minimum: 1_100_000n, faucetHint: "1.1 XRP (testnet; 1 XRP base reserve + fees)" }, + chainflip: { network: SUBSTRATE_NETWORKS.find((n) => n.name === "Chainflip Perseverance")!, module: createSubstrateModule, family: "substrate", minimum: 10n ** 17n, faucetHint: "0.1 tFLIP funded from Sepolia" }, + provenance: { network: PROVENANCE_TESTNET, module: () => createCosmosModule({ family: "provenance" }), family: "provenance", minimum: 10n ** 9n, faucetHint: "1 HASH (pio-testnet-1)" }, + initia: { network: INITIA_TESTNET, module: () => createCosmosModule({ family: "initia" }), family: "initia", minimum: 100_000n, faucetHint: "0.1 INIT (initiation-2)" }, + multiversx: { network: MULTIVERSX_DEVNET, module: createMultiversXModule, family: "multiversx", minimum: 10n ** 16n, faucetHint: "0.01 xEGLD (devnet)" }, + icp: { network: ICP_TEST, module: createIcpModule, family: "icp", minimum: 100_000n, faucetHint: "0.001 TESTICP" }, + bitcoincash: { network: BCH_CHIPNET, module: createBitcoinCashModule, family: "bitcoincash", minimum: 5_000n, faucetHint: "5,000 sats (chipnet)" }, + antelope: { network: JUNGLE4, module: createAntelopeModule, family: "antelope", minimum: 1_0000n, faucetHint: "an account for the key, then 1 EOS + CPU (Jungle4)" }, }; -export const TARGET_ORDER: Target[] = ["evm", "hedera-evm", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand"]; +export const TARGET_ORDER: Target[] = [ + "evm", "hedera-evm", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", + "cosmos", "tron", "stacks", "fuel", "xrpl", "chainflip", "multiversx", "provenance", "initia", "icp", "bitcoincash", "antelope", +]; const hexToBytes = (h: string) => Uint8Array.from(Buffer.from(h.replace(/^0x/, ""), "hex")); /** The matrix account for a target, shaped as the wallet's background holds it. */ export function accountOf(t: Target): Account { const family = TARGETS[t].family; - const e = ADDR[family]; + const e = entryOf(t); return { id: `${family}:0`, family, index: 0, - curve: "secp256k1", + curve: family === "substrate" ? "sr25519" : family === "multiversx" ? "ed25519" : "secp256k1", derivationPath: e.path, publicKey: e.publicKey, - address: e.address, + address: e.vaultAddress ?? e.address, } as Account; } -export const addressOf = (t: Target): string => ADDR[TARGETS[t].family].address; -export const publicKeyOf = (t: Target): string => ADDR[TARGETS[t].family].publicKey; +export const addressOf = (t: Target): string => entryOf(t).address; +export const publicKeyOf = (t: Target): string => entryOf(t).publicKey; const modules = new Map(); const moduleOf = (t: Target) => { @@ -180,6 +218,20 @@ export function explorerTx(t: Target, id: string): string { return `https://shadownet.tzkt.io/${id}`; case "algorand": return `https://lora.algokit.io/testnet/transaction/${id}`; + case "cosmos": + return `https://www.mintscan.io/osmosis-testnet/tx/${id}`; + case "tron": + return `https://nile.tronscan.org/#/transaction/${id}`; + case "stacks": + return `https://explorer.hiro.so/txid/${id.startsWith("0x") ? id : `0x${id}`}?chain=testnet`; + case "fuel": + return `https://app-testnet.fuel.network/tx/${id}`; + case "chainflip": + return `https://scan.perseverance.chainflip.io/blocks/${id}`; + case "xrpl": + return `https://testnet.xrpl.org/transactions/${id}`; + case "multiversx": + return `https://devnet-explorer.multiversx.com/transactions/${id}`; default: return `${n.explorerUrl}/tx/${id}`; } @@ -254,6 +306,34 @@ export async function confirmTx(t: Target, id: string, timeoutMs = 120_000): Pro const r = await j(`https://api.shadownet.tzkt.io/v1/operations/${id}`).catch(() => []); return Array.isArray(r) && r.length > 0 && r.every((o: { status?: string }) => o.status === "applied"); } + case "cosmos": { + const r = await j(`${n.rpcUrls[0]!.replace(/\/$/, "")}/cosmos/tx/v1beta1/txs/${id}`).catch(() => null); + return r?.tx_response?.code === 0 && Number(r.tx_response.height) > 0; + } + case "tron": { + const r = await j("https://nile.trongrid.io/wallet/gettransactioninfobyid", { method: "POST", body: JSON.stringify({ value: id }) }).catch(() => null); + return !!r?.blockNumber && (r.receipt?.result === undefined || r.receipt.result === "SUCCESS") && r.result !== "FAILED"; + } + case "stacks": { + const r = await j(`https://api.testnet.hiro.so/extended/v1/tx/${id.startsWith("0x") ? id : `0x${id}`}`).catch(() => null); + return r?.tx_status === "success"; + } + case "fuel": { + const r = await j("https://testnet.fuel.network/v1/graphql", { method: "POST", body: JSON.stringify({ query: `{ transaction(id: "${id}") { status { __typename } } }` }) }).catch(() => null); + return r?.data?.transaction?.status?.__typename === "SuccessStatus"; + } + case "multiversx": { + const r = await j(`https://devnet-api.multiversx.com/transactions/${id}`).catch(() => null); + return r?.status === "success"; + } + case "xrpl": { + const r = await rpc("https://s.altnet.rippletest.net:51234", "tx", [{ transaction: id }]).catch(() => null); + return r?.validated === true && r?.meta?.TransactionResult === "tesSUCCESS"; + } + case "chainflip": { + const r = await rpc(n.rpcUrls[0]!, "chain_getBlock", [id]).catch(() => null); + return !!r; + } case "algorand": { const r = await fetch(`https://testnet-idx.4160.nodely.dev/v2/transactions/${id}`); if (!r.ok) return false; diff --git a/apps/extension/e2e/matrix/dapps/chainflip.ts b/apps/extension/e2e/matrix/dapps/chainflip.ts new file mode 100644 index 00000000..b19b45c9 --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/chainflip.ts @@ -0,0 +1,58 @@ +/** + * Chainflip State Chain (Perseverance testnet): the discovery and signing path lp.chainflip.io uses + * (@polkadot/extension-dapp over window.injectedWeb3, addresses re-encoded to ss58 2112 "cF…", signer.signRaw / + * signPayload). L2 is a signRaw verified with @polkadot/util-crypto signatureVerify. Chainflip has no plain FLIP + * transfer, so L3 sends the cheapest state-changing call an LP portal starts with, liquidityProvider.registerLpAccount, + * built with @polkadot/api and signed through the extension; every Chainflip call needs FLIP for its fee (NeedsFunds + * until the account is funded from Sepolia tFLIP). + */ +import { web3Accounts, web3Enable, web3FromSource } from "@polkadot/extension-dapp"; +import { ApiPromise, WsProvider } from "@polkadot/api"; +import { cryptoWaitReady, encodeAddress, signatureVerify } from "@polkadot/util-crypto"; +import { stringToHex, u8aWrapBytes } from "@polkadot/util"; +import { MESSAGE, NeedsFunds, expose } from "../dapp-kit"; + +const RPC = "wss://perseverance.chainflip.xyz"; +let address = ""; +let source = ""; + +expose({ + info: { dapp: "@polkadot/extension-dapp + @polkadot/api in a local page, Chainflip Perseverance", why: "what lp.chainflip.io uses to reach Polkadot-style wallets; the portal itself needs a funded LP account" }, + steps: { + connect: async () => { + const exts = await web3Enable("Clip dapp matrix"); + const clip = exts.find((e) => e.name === "clip-wallet"); + if (!clip) throw new Error(`clip-wallet not in injectedWeb3 (${exts.map((e) => e.name).join(", ")})`); + const accounts = await web3Accounts({ extensions: ["clip-wallet"] }); + source = accounts[0]!.meta.source; + address = encodeAddress(accounts[0]!.address, 2112); + return { address, source }; + }, + sign: async () => { + await cryptoWaitReady(); + const injector = await web3FromSource(source); + const { signature } = await injector.signer.signRaw!({ address, data: stringToHex(MESSAGE), type: "bytes" }); + return { valid: signatureVerify(u8aWrapBytes(MESSAGE), signature, address).isValid, how: "@polkadot/util-crypto signatureVerify over -wrapped message" }; + }, + send: async () => { + const api = await ApiPromise.create({ provider: new WsProvider(RPC), noInitWarn: true }); + const flip = (await api.query.flip!.account!(address)) as unknown as { balance: { toBigInt(): bigint } }; + const injector = await web3FromSource(source); + const tx = api.tx.liquidityProvider!.registerLpAccount!(); + if (flip.balance.toBigInt() === 0n) { + // Still let the wallet decode and sign it (the approval is L4), then stop before broadcasting. + await tx.signAsync(address, { signer: injector.signer }); + await api.disconnect(); + throw new NeedsFunds("signed, but the account has no FLIP for the fee (fund it from Sepolia tFLIP)"); + } + const id = await new Promise((resolve, reject) => { + tx.signAndSend(address, { signer: injector.signer }, ({ status, dispatchError }) => { + if (dispatchError) reject(new Error(dispatchError.toString())); + else if (status.isInBlock) resolve(status.asInBlock.toHex()); + }).catch(reject); + }); + await api.disconnect(); + return { id }; + }, + }, +}); diff --git a/apps/extension/e2e/matrix/dapps/cosmos.ts b/apps/extension/e2e/matrix/dapps/cosmos.ts new file mode 100644 index 00000000..d5131a88 --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/cosmos.ts @@ -0,0 +1,60 @@ +/** + * Cosmos (Osmosis testnet, osmo-test-5): the Keplr API as a Keplr dapp calls it, on Clip's own global + * (window.clipwallet.cosmos): enable + getOfflineSigner, whose getAccounts() cosmjs reads; ADR-36 signArbitrary + * verified the way Keplr's verifyADR36Amino does it (Keplr makeADR36AminoSignDoc rebuilt with @cosmjs/amino + * makeSignDoc + serializeSignDoc, sha256, @cosmjs/crypto Secp256k1.verifySignature, signer = pubkeyToAddress); + * and a 1-uosmo MsgSend to yourself with @cosmjs/stargate SigningStargateClient.signAndBroadcast (SIGN_MODE_DIRECT + * through the offline signer). @keplr-wallet/cosmos itself isn't bundled: its tiny-secp256k1 dependency needs a + * native build the workspace doesn't allow. + */ +import { makeSignDoc, pubkeyToAddress, serializeSignDoc } from "@cosmjs/amino"; +import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto"; +import { fromBase64, toBase64, toUtf8 } from "@cosmjs/encoding"; +import { GasPrice, SigningStargateClient, assertIsDeliverTxSuccess } from "@cosmjs/stargate"; +import { MESSAGE, NeedsFunds, expose, waitFor } from "../dapp-kit"; + +const CHAIN_ID = "osmo-test-5"; +const RPC = "https://rpc.osmotest5.osmosis.zone"; +const REST = "https://lcd.osmotest5.osmosis.zone"; + +interface KeplrLike { + enable(chainIds: string | string[]): Promise; + getOfflineSigner(chainId: string): { getAccounts(): Promise<{ address: string; algo: string; pubkey: Uint8Array }[]> } & Parameters[1]; + signArbitrary(chainId: string, signer: string, data: string): Promise<{ pub_key: { type: string; value: string }; signature: string }>; +} + +const provider = () => waitFor(() => (window as unknown as { clipwallet?: { cosmos?: KeplrLike } }).clipwallet?.cosmos, "window.clipwallet.cosmos"); +let address = ""; + +expose({ + info: { dapp: "Keplr API on window.clipwallet.cosmos + @cosmjs/stargate SigningStargateClient in a local page, osmo-test-5", why: "Osmosis dapps talk to Keplr's API through cosmjs; the hosted Osmosis frontend only knows wallet-specific globals" }, + steps: { + connect: async () => { + const k = await provider(); + await k.enable(CHAIN_ID); + const accounts = await k.getOfflineSigner(CHAIN_ID).getAccounts(); + address = accounts[0]!.address; + return { address, algo: accounts[0]!.algo, accounts: accounts.length }; + }, + sign: async () => { + const k = await provider(); + const sig = await k.signArbitrary(CHAIN_ID, address, MESSAGE); + const doc = makeSignDoc([{ type: "sign/MsgSignData", value: { signer: address, data: toBase64(toUtf8(MESSAGE)) } }], { gas: "0", amount: [] }, "", "", 0, 0); + const signerOk = pubkeyToAddress(sig.pub_key, "osmo") === address; + const valid = signerOk && (await Secp256k1.verifySignature(Secp256k1Signature.fromFixedLength(fromBase64(sig.signature)), sha256(serializeSignDoc(doc)), fromBase64(sig.pub_key.value))); + return { valid, how: "ADR-36 amino doc (Keplr makeADR36AminoSignDoc) via @cosmjs/amino, sha256, @cosmjs/crypto Secp256k1.verifySignature" }; + }, + send: async () => { + const k = await provider(); + const res = await fetch(`${REST}/cosmos/auth/v1beta1/accounts/${address}`); + if (res.status === 404) throw new NeedsFunds("the account doesn't exist on osmo-test-5 yet"); + // Osmosis x/txfees: the EIP-1559 base fee, with headroom, as the Osmosis frontend does. + const base = (await (await fetch(`${REST}/osmosis/txfees/v1beta1/cur_eip_base_fee`)).json()) as { base_fee?: string }; + const price = Math.max(0.025, Number(base.base_fee ?? "0.025") * 1.5); + const client = await SigningStargateClient.connectWithSigner(RPC, k.getOfflineSigner(CHAIN_ID), { gasPrice: GasPrice.fromString(`${price.toFixed(6)}uosmo`) }); + const r = await client.signAndBroadcast(address, [{ typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: { fromAddress: address, toAddress: address, amount: [{ denom: "uosmo", amount: "1" }] } }], "auto", "Clip Wallet dapp matrix"); + assertIsDeliverTxSuccess(r); + return { id: r.transactionHash }; + }, + }, +}); diff --git a/apps/extension/e2e/matrix/dapps/fuel.ts b/apps/extension/e2e/matrix/dapps/fuel.ts new file mode 100644 index 00000000..04443f7f --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/fuel.ts @@ -0,0 +1,72 @@ +/** + * Fuel (testnet): fuels-ts `Fuel` connector manager (the Fuel connector standard, discovery by the "FuelConnector" + * window event) picking Clip's connector from its connectors list, connect + accounts, Account.signMessage verified + * with fuels-ts `Signer.recoverAddress(hashMessage(msg), sig)`, and a 1-unit base-asset (ETH) transfer to yourself + * with `wallet.transfer`, which assembles the transaction on testnet.fuel.network and hands it to the connector. + */ +import type { FuelConnector } from "fuels"; +// fuels' own self-contained browser build. The package's "exports" map only offers the Node build, whose +// @fuel-ts/crypto imports pbkdf2Sync/createHmac/randomUUID from node:crypto, which the matrix bundler stubs out. +// @ts-ignore: the browser build has no declaration file; it is typed as the package just below. +import * as fuelsBrowser from "../../../node_modules/fuels/dist/browser.min.mjs"; +import { MESSAGE, NeedsFunds, expose } from "../dapp-kit"; + +const { Fuel, Provider, Signer, hashMessage } = fuelsBrowser as typeof import("fuels"); + +const TESTNET = "https://testnet.fuel.network/v1/graphql"; +const fuel = new Fuel({ storage: null }); +let address = ""; + +type Connector = FuelConnector; +const clipConnector = () => (window as unknown as { clipwallet?: { fuel?: Connector } }).clipwallet?.fuel; + +/** Clip's connector from the Fuel connectors list (it announces itself; re-announce if this Fuel came up later). */ +async function pickClip(): Promise { + const own = clipConnector(); + if (!own) throw new Error("window.clipwallet.fuel not available"); + for (let i = 0; i < 40; i++) { + const found = (await fuel.connectors()).find((c) => c === own || c.name === own.name); + if (found) { + await fuel.selectConnector(found.name); + return found; + } + // The documented discovery event, with the connector the wallet put on window (a Fuel created after the + // wallet's own announcements missed them). + if (i === 10) window.dispatchEvent(new CustomEvent("FuelConnector", { detail: own })); + await new Promise((r) => setTimeout(r, 100)); + } + throw new Error("Clip's connector never showed up in Fuel's connectors list"); +} + +expose({ + info: { + dapp: "fuels-ts Fuel connector manager (FuelConnector event discovery) + fuels Provider in a local page, Fuel testnet", + why: "fuels-ts Fuel is the connector API every Fuel dapp (and @fuels/react) uses; a local page avoids a dapp's own build and its WalletConnect setup", + }, + steps: { + connect: async () => { + const c = await pickClip(); + if (!(await fuel.connect())) throw new Error("connect was declined"); + const accounts = await fuel.accounts(); + address = (await fuel.currentAccount()) ?? accounts[0] ?? ""; + if (!address) throw new Error("no Fuel account"); + const network = await fuel.currentNetwork(); + return { address, connector: c.name, accounts, chainId: network.chainId }; + }, + sign: async () => { + const wallet = await fuel.getWallet(address, new Provider(TESTNET)); + const signature = await wallet.signMessage(MESSAGE); + const recovered = Signer.recoverAddress(hashMessage(MESSAGE), signature).toString(); + return { valid: recovered.toLowerCase() === address.toLowerCase(), how: "fuels-ts Signer.recoverAddress(hashMessage(message), signature)", signature }; + }, + send: async () => { + const provider = new Provider(TESTNET); + const wallet = await fuel.getWallet(address, provider); + const balance = await wallet.getBalance(await provider.getBaseAssetId()); + if (balance.isZero()) throw new NeedsFunds("the Fuel testnet account has no ETH (faucet-testnet.fuel.network)"); + const tx = await wallet.transfer(address, 1); + const result = await tx.waitForResult(); + return { id: tx.id, status: result.status }; + }, + }, +}); diff --git a/apps/extension/e2e/matrix/dapps/multiversx.ts b/apps/extension/e2e/matrix/dapps/multiversx.ts new file mode 100644 index 00000000..2363ea48 --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/multiversx.ts @@ -0,0 +1,78 @@ +/** + * MultiversX (devnet): @multiversx/sdk-dapp 5.7.3 driven headless (no unlock panel), the way an sdk-dapp dapp logs in + * once the user picked a provider. + * - connect: `initApp({ dAppConfig: { environment: "devnet", nativeAuth: false } })`, which merges + * `window.multiversx.providers` into `ProviderFactory.customProviders` (sdk-dapp's UNDOCUMENTED custom-provider hook: + * 1Mask adds Clip's own entry there, type "clipwallet"), then `ProviderFactory.create({ type: "clipwallet" })` and + * `login()` (native auth off here: with it, login() also asks for a sign-in signature, a second approval). + * - sign: `signMessage(new Message({ data }))` through sdk-dapp's DappProvider, checked with sdk-dapp's own + * `verifyMessage` (MessageComputer + UserVerifier). + * - send: a 1-attoEGLD transfer to yourself built with sdk-core 15, signed through DappProvider.signTransactions (sdk-dapp + * sets the nonce) and sent to the devnet gateway's /transaction/send. + * The page fails at connect if Clip's entry isn't in the custom-provider list: dapps that replace `window.multiversx` + * (MultiversX's template dapp does) never see it. + */ +import { Address, Message, MessageComputer, Transaction } from "@multiversx/sdk-core"; +import type { initApp as InitApp } from "@multiversx/sdk-dapp/out/methods/initApp/initApp"; +import type { verifyMessage as VerifyMessage } from "@multiversx/sdk-dapp/out/providers/DappProvider/helpers/signMessage/verifyMessage"; +import type { ProviderFactory as PF } from "@multiversx/sdk-dapp/out/providers/ProviderFactory"; +// sdk-dapp 5 has no package "exports"/"main": bundlers need the .mjs files, TypeScript finds the types next to them +// without the extension (imported as types above). +// @ts-expect-error untyped .mjs path +import { initApp as initAppImpl } from "@multiversx/sdk-dapp/out/methods/initApp/initApp.mjs"; +// @ts-expect-error untyped .mjs path +import { verifyMessage as verifyMessageImpl } from "@multiversx/sdk-dapp/out/providers/DappProvider/helpers/signMessage/verifyMessage.mjs"; +// @ts-expect-error untyped .mjs path +import { ProviderFactory as ProviderFactoryImpl } from "@multiversx/sdk-dapp/out/providers/ProviderFactory.mjs"; +import { MESSAGE, NeedsFunds, expose } from "../dapp-kit"; + +const initApp = initAppImpl as typeof InitApp; +const verifyMessage = verifyMessageImpl as typeof VerifyMessage; +const ProviderFactory = ProviderFactoryImpl as typeof PF; + +const GATEWAY = "https://devnet-gateway.multiversx.com"; +const TYPE = "clipwallet"; + +type DappProvider = Awaited>; +let provider: DappProvider; +let address = ""; + +expose({ + info: { + dapp: "@multiversx/sdk-dapp 5.7.3 initApp + ProviderFactory (custom provider from window.multiversx.providers), native auth, devnet", + why: "sdk-dapp is how MultiversX dapps connect wallets; it has no wallet discovery, so Clip lists itself through the undocumented window.multiversx.providers hook", + }, + steps: { + connect: async () => { + // Native auth off for L1: with it, login() also asks for a sign-in signature (a second approval the matrix's + // connect level doesn't answer). L2 signs a message instead. + await initApp({ dAppConfig: { environment: "devnet", nativeAuth: false } } as Parameters[0]); + const entry = (ProviderFactory.customProviders as { type: string; name: string }[]).find((p) => p.type === TYPE); + if (!entry) throw new Error("Clip Wallet isn't in sdk-dapp's custom providers (window.multiversx.providers)"); + provider = await ProviderFactory.create({ type: TYPE }); + const login = await provider.login(); + if (!login?.address) throw new Error("login returned no address"); + address = login.address; + return { address, name: entry.name }; + }, + sign: async () => { + const signed = await provider.signMessage(new Message({ address: Address.newFromBech32(address), data: new TextEncoder().encode(MESSAGE) })); + if (!signed) throw new Error("no signed message"); + const r = await verifyMessage(JSON.stringify(new MessageComputer().packMessage(signed))); + return { valid: r.isVerified && r.address === address && r.message === MESSAGE, how: "sdk-dapp verifyMessage (MessageComputer + UserVerifier)" }; + }, + send: async () => { + const acct = (await (await fetch(`${GATEWAY}/address/${address}`)).json()) as { data?: { account?: { balance?: string } } }; + if (BigInt(acct.data?.account?.balance ?? "0") < 100_000_000_000_000n) throw new NeedsFunds("the account has no devnet EGLD for the fee (devnet wallet faucet)"); + const me = Address.newFromBech32(address); + const tx = new Transaction({ sender: me, receiver: me, value: 1n, gasLimit: 50_000n, gasPrice: 1_000_000_000n, chainID: "D", version: 2 }); + const [signed] = await provider.signTransactions([tx]); + if (!signed) throw new Error("no signed transaction"); + const r = (await ( + await fetch(`${GATEWAY}/transaction/send`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(signed.toPlainObject()) }) + ).json()) as { data?: { txHash?: string }; error?: string }; + if (!r.data?.txHash) throw new Error(`devnet refused: ${r.error ?? "no hash"}`); + return { id: r.data.txHash }; + }, + }, +}); diff --git a/apps/extension/e2e/matrix/dapps/stacks.ts b/apps/extension/e2e/matrix/dapps/stacks.ts new file mode 100644 index 00000000..1863ba63 --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/stacks.ts @@ -0,0 +1,59 @@ +/** + * Stacks (testnet): @stacks/connect v8 `request()` (SIP-030) against the wallet it finds through WBIP-004 + * (`window.wbip_providers`, the registry Connect's own wallet picker reads): getAddresses, stx_signMessage verified with + * @stacks/encryption verifyMessageSignatureRsv (and the address re-derived from the returned key), and a 1 µSTX + * stx_transferStx that the wallet builds, signs and broadcasts. Stacks nodes refuse a transfer to the sender itself + * (mempool rejection TransferRecipientIsSender), so the µSTX goes to the testnet boot address ST000…2AMW42H. + * Unfunded: the wallet still decodes and signs (broadcast: false), then NeedsFunds. + */ +import { request } from "@stacks/connect"; +import { verifyMessageSignatureRsv } from "@stacks/encryption"; +import { getAddressFromPublicKey } from "@stacks/transactions"; +import { MESSAGE, NeedsFunds, expose, waitFor } from "../dapp-kit"; + +const HIRO = "https://api.testnet.hiro.so"; +/** Testnet boot address (the boot contracts' deployer): nobody's account, any µSTX sent there just stays. */ +const SINK = "ST000000000000000000002AMW42H"; +type Provider = Parameters[0] extends infer O ? (O extends { provider?: infer P } ? P : never) : never; +let provider: Provider; +let address = ""; +let publicKey = ""; + +/** @stacks/connect-ui `getProviderFromId`: the WBIP-004 id is the provider's path on window. */ +const fromId = (id: string) => id.split(".").reduce((o, k) => o?.[k], window); + +expose({ + info: { + dapp: "@stacks/connect 8 request() (SIP-030) + WBIP-004 discovery + @stacks/encryption in a local page, testnet", + why: "Connect's request() is what Stacks dapps call (the picker modal lists wbip_providers; the page picks Clip's entry instead of clicking the modal)", + }, + steps: { + connect: async () => { + const entry = await waitFor( + () => ((window as unknown as { wbip_providers?: { id: string; name: string }[] }).wbip_providers ?? []).find((p) => p.name === "Clip Wallet"), + "Clip Wallet in window.wbip_providers", + ); + provider = fromId(entry.id); + if (!provider) throw new Error(`${entry.id} not on window`); + const r = await request({ provider, enableLocalStorage: false }, "getAddresses", { network: "testnet" }); + const stx = r.addresses.find((a) => a.address.startsWith("ST")); + if (!stx) throw new Error(`no testnet STX address in ${JSON.stringify(r.addresses)}`); + address = stx.address; + publicKey = stx.publicKey; + return { address, publicKey, providerId: entry.id }; + }, + sign: async () => { + const r = await request({ provider, enableLocalStorage: false }, "stx_signMessage", { message: MESSAGE }); + const valid = verifyMessageSignatureRsv({ message: MESSAGE, signature: r.signature, publicKey: r.publicKey }) && getAddressFromPublicKey(r.publicKey, "testnet") === address; + return { valid, how: "@stacks/encryption verifyMessageSignatureRsv (\\x17Stacks Signed Message prefix) + getAddressFromPublicKey === connected address" }; + }, + send: async () => { + const bal = await fetch(`${HIRO}/extended/v1/address/${address}/stx`).then((x) => (x.ok ? (x.json() as Promise<{ balance?: string }>) : { balance: "0" })); + const funded = BigInt(bal.balance ?? "0") > 10_000n; + const r = await request({ provider, enableLocalStorage: false }, "stx_transferStx", { recipient: SINK, amount: "1", memo: "clip matrix", network: "testnet", ...(funded ? {} : { broadcast: false }) } as never); + if (!funded) throw new NeedsFunds("signed, but the account has no testnet STX"); + if (!r.txid) throw new Error(`no txid in ${JSON.stringify(r)}`); + return { id: r.txid, publicKey }; + }, + }, +}); diff --git a/apps/extension/e2e/matrix/dapps/tron.ts b/apps/extension/e2e/matrix/dapps/tron.ts new file mode 100644 index 00000000..0a5d08c6 --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/tron.ts @@ -0,0 +1,74 @@ +/** + * TRON (Nile): discovery over TIP-6963 and the TIP-1193 provider it announces, as a TRON dapp written against the + * standards does it; TronWeb 6 for everything else. + * - connect: `TIP6963:requestProvider` → pick Clip's announcement by rdns → `eth_requestAccounts` (TIP-1102) on Nile + * (`wallet_switchEthereumChain`, TIP-3326, if the provider starts elsewhere). + * - sign: `provider.tronWeb.trx.signMessageV2` (TIP-104), checked with TronWeb's static `Trx.verifyMessageV2`, which + * recovers the signer's address. + * - send: a 1-sun TRX transfer built with TronWeb's transactionBuilder.sendTrx (no key), signed with + * `provider.tronWeb.trx.sign` and broadcast with TronWeb. TRON refuses transfers to yourself ("Cannot transfer TRX + * to yourself"), so it goes to the zero "black hole" address, which exists on Nile (no account-opening fee). + * No @tronweb3/tronwallet-adapters: none of its adapters connects a generic TIP-6963 wallet (TronLinkAdapter and + * TokenPocketAdapter listen to TIP-6963 but take only their own wallet's announcement, `info.name === "TronLink"` / …). + */ +import { TronWeb, Trx } from "tronweb"; +import { MESSAGE, NeedsFunds, expose, waitFor } from "../dapp-kit"; + +const NILE = "https://nile.trongrid.io"; +const NILE_CHAIN_ID = "0xcd8690dc"; +const BLACK_HOLE = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; +const RDNS = "org.coldai.clipwallet"; + +interface Tip1193 { + request(a: { method: string; params?: unknown }): Promise; + tronWeb: { trx: { sign(tx: unknown): Promise<{ txID: string; signature: string[] }>; signMessageV2(m: string): Promise } }; +} +interface Announced { + info: { uuid: string; name: string; icon: string; rdns: string }; + provider: Tip1193; +} + +const tronWeb = new TronWeb({ fullHost: NILE }); +const announced: Announced[] = []; +window.addEventListener("TIP6963:announceProvider", (e) => announced.push((e as CustomEvent).detail)); +window.dispatchEvent(new Event("TIP6963:requestProvider")); + +let provider: Tip1193; +let address = ""; + +expose({ + info: { + dapp: "TIP-6963 discovery + TIP-1193 provider + TronWeb 6 in a local page, Nile", + why: "TIP-6963/TIP-1193 is the TRON standard for injected wallets; tronwallet-adapters has no adapter for a generic TIP-6963 wallet", + }, + steps: { + connect: async () => { + const d = await waitFor(() => announced.find((a) => a.info.rdns === RDNS), "Clip Wallet (TIP-6963)"); + provider = d.provider; + if ((await provider.request({ method: "eth_chainId" })) !== NILE_CHAIN_ID) { + await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: NILE_CHAIN_ID }] }); + } + const accounts = (await provider.request({ method: "eth_requestAccounts" })) as string[]; + address = accounts[0]!; + return { address, chainId: await provider.request({ method: "eth_chainId" }), name: d.info.name }; + }, + sign: async () => { + const signature = await provider.tronWeb.trx.signMessageV2(MESSAGE); + return { valid: Trx.verifyMessageV2(MESSAGE, signature) === address, how: "TronWeb Trx.verifyMessageV2 (TIP-104 signMessageV2)" }; + }, + send: async () => { + // java-tron can't build a transaction for an account that doesn't exist yet. + const account = (await tronWeb.trx.getAccount(address)) as { address?: string }; + if (!account.address) throw new NeedsFunds("the account doesn't exist on Nile yet (receive TRX first)"); + const tx = await tronWeb.transactionBuilder.sendTrx(BLACK_HOLE, 1, address); + const signed = await provider.tronWeb.trx.sign(tx); + const r = (await tronWeb.trx.sendRawTransaction(signed as never)) as { result?: boolean; txid?: string; code?: string; message?: string }; + if (!r.result) { + const why = r.message && /^[0-9a-f]+$/i.test(r.message) ? new TextDecoder().decode(Uint8Array.from(r.message.match(/../g)!.map((h) => parseInt(h, 16)))) : r.message; + if (/balance is not sufficient|account resource insufficient/i.test(why ?? "")) throw new NeedsFunds(`Nile refused: ${why}`); + throw new Error(`Nile refused: ${r.code ?? ""} ${why ?? ""}`.trim()); + } + return { id: r.txid ?? signed.txID }; + }, + }, +}); diff --git a/apps/extension/e2e/matrix/dapps/xrpl.ts b/apps/extension/e2e/matrix/dapps/xrpl.ts new file mode 100644 index 00000000..91599255 --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/xrpl.ts @@ -0,0 +1,50 @@ +/** + * XRP Ledger (testnet, xrpl:1): XLS-72d Browser Wallet Standard discovery exactly as @xrpl-wallet-standard/app's + * getRegisterdXRPLWallets does it (getWallets() filtered by REQUIRED_FEATURES: standard:connect, standard:events, + * xrpl:signTransaction, xrpl:signAndSubmitTransaction), then xrpl:signAndSubmitTransaction with options.autofill. + * + * L2 (sign a message) doesn't exist: XLS-72d has no message-signing feature, so there's no `sign` step. + * L3 is a no-op AccountSet carrying a memo, not "1 drop to yourself": rippled refuses an XRP Payment to the sending + * account (temREDUNDANT), so it never reaches a ledger. The AccountSet costs only the fee and changes nothing. + */ +import { getWallets } from "@wallet-standard/app"; +import { NeedsFunds, expose, waitFor } from "../dapp-kit"; + +// testnet.xrpl-labs.com answers CORS preflights from a page; s.altnet.rippletest.net:51234 does not. +const RPC = "https://testnet.xrpl-labs.com/"; +/** @xrpl-wallet-standard/core REQUIRED_FEATURES (packages/core/src/utils.ts). */ +const REQUIRED_FEATURES = ["standard:connect", "standard:events", "xrpl:signTransaction", "xrpl:signAndSubmitTransaction"] as const; +type F = Record; + +let wallet: ReturnType["get"]>[number]; +let account: (typeof wallet)["accounts"][number]; + +async function rpc(method: string, params: Record) { + const res = await fetch(RPC, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method, params: [params] }) }); + return ((await res.json()) as { result: Record }).result; +} + +expose({ + info: { + dapp: "XLS-72d Wallet Standard discovery (@wallet-standard/app + the @xrpl-wallet-standard/app filter) in a local page, testnet", + why: "@xrpl-wallet-standard/app only wraps getWallets() with that filter but pulls in xrpl.js and an alpha Xahau fork; the page runs the same filter", + }, + steps: { + connect: async () => { + const xrpl = () => getWallets().get().filter((w) => REQUIRED_FEATURES.every((f) => f in w.features)); + wallet = await waitFor(() => xrpl().find((w) => w.name === "Clip Wallet"), "Clip Wallet (XRPL)"); + const r = await (wallet.features as F)["standard:connect"].connect(); + account = r.accounts[0]!; + return { address: account.address, chains: wallet.chains }; + }, + send: async () => { + const info = await rpc("account_info", { account: account.address, ledger_index: "current" }); + if (info.status === "error") throw new NeedsFunds(`the account isn't activated on testnet yet (${String(info.error)})`); + const tx_json = { TransactionType: "AccountSet", Account: account.address, Memos: [{ Memo: { MemoData: Array.from(new TextEncoder().encode("clip dapp matrix"), (b) => b.toString(16).padStart(2, "0")).join("").toUpperCase() } }] }; + const r = await (wallet.features as F)["xrpl:signAndSubmitTransaction"].signAndSubmitTransaction({ tx_json, account, network: "xrpl:1", options: { autofill: true } }); + const result = r.tx_json?.meta?.TransactionResult; + if (r.tx_json?.validated && result !== "tesSUCCESS") throw new Error(`the ledger answered ${result}`); + return { id: r.tx_hash, validated: !!r.tx_json?.validated }; + }, + }, +}); diff --git a/apps/extension/e2e/phase2.spec.ts b/apps/extension/e2e/phase2.spec.ts index 3a08e2ef..55faa2c7 100644 --- a/apps/extension/e2e/phase2.spec.ts +++ b/apps/extension/e2e/phase2.spec.ts @@ -1,5 +1,6 @@ /** - * Phase 2: all 14 chain families (real wiring) and the new screens (fixture mode). + * Phase 2 and networks87: every chain family the build ships (real wiring) and the new screens (fixture mode). + * THORChain has no public testnet, so a testnet build has no THORChain account to list. */ import path from "node:path"; import type { Page } from "@playwright/test"; @@ -25,12 +26,28 @@ const ADDRESS: Record = { stellar: /^G[A-Z2-7]{55}$/, tezos: /^tz1[1-9A-HJ-NP-Za-km-z]{33}$/, algorand: /^[A-Z2-7]{58}$/, + // networks87: the vault's own (mainnet) spelling; Receive spells it per network (receiveAddress). + cosmos: /^cosmos1[02-9ac-hj-np-z]{38}$/, + provenance: /^pb1[02-9ac-hj-np-z]{38}$/, + initia: /^init1[02-9ac-hj-np-z]{38}$/, + tron: /^T[1-9A-HJ-NP-Za-km-z]{33}$/, + xrpl: /^r[1-9A-HJ-NP-Za-km-z]{24,34}$/, + antelope: /^PUB_K1_[1-9A-HJ-NP-Za-km-z]{50}$/, + multiversx: /^erd1[02-9ac-hj-np-z]{58}$/, + icp: /^([a-z2-7]{5}-){10}[a-z2-7]{3}$/, + stacks: /^SP[0-9A-HJKMNP-TV-Z]{38,39}$/, + fuel: /^0x[0-9a-fA-F]{64}$/, + bitcoincash: /^bitcoincash:q[02-9ac-hj-np-z]{41}$/, }; /** One coin per family, as Receive lists them (Substrate testnets: Westend and Paseo). */ -const RECEIVE_SYMBOLS = ["ETH", "HBAR", "SOL", "BTC", "SUI", "APT", "ADA", "WND", "PAS", "STRK", "GRAM", "NEAR", "XLM", "XTZ", "ALGO"]; +const RECEIVE_SYMBOLS = [ + "ETH", "HBAR", "SOL", "BTC", "SUI", "APT", "ADA", "WND", "PAS", "STRK", "GRAM", "NEAR", "XLM", "XTZ", "ALGO", + // networks87 testnets (Chainflip Perseverance's FLIP, Jungle4's EOS, MultiversX devnet EGLD, ICP test ledger TESTICP) + "OSMO", "ZIG", "HASH", "INIT", "TRX", "XRP", "EOS", "TLOS", "XPR", "EGLD", "TESTICP", "STX", "BCH", "FLIP", +]; -extensionTest(REAL_BUILD)("real: accounts for all 14 families derive after onboarding and show in Receive", async ({ context, extensionId }) => { +extensionTest(REAL_BUILD)("real: accounts for every family derive after onboarding and show in Receive", async ({ context, extensionId }) => { const page = await openPage(context, extensionId, "popup.html"); const shot = shotOf(page); await onboard(page); @@ -64,6 +81,13 @@ extensionTest(REAL_BUILD)("real: accounts for all 14 families derive after onboa await expect(page.getByTestId("receive-address")).toHaveText(sui); await expect(page.getByRole("img", { name: /QR code/ })).toBeVisible(); await shot("receive-sui"); + + // networks87: an address spelled per network. OSMO on osmo-test-5 receives on the osmo1… form of the vault's key. + await page.getByRole("button", { name: "Back", exact: true }).click(); + await page.getByRole("button", { name: "Receive" }).click(); + await page.locator(".clip-asset-row", { has: page.locator(".clip-asset-row__symbol", { hasText: /^OSMO$/ }) }).first().click(); + await expect(page.getByTestId("receive-address")).toHaveText(/^osmo1[02-9ac-hj-np-z]{38}$/); + await shot("receive-osmo"); }); extensionTest(FIXTURE_BUILD)("fixtures: Stake, Swap, Explore, Backup and Accounts screens", async ({ context, extensionId }) => { diff --git a/apps/extension/e2e/shots/matrix/chainflip-approval.png b/apps/extension/e2e/shots/matrix/chainflip-approval.png new file mode 100644 index 00000000..b266597e Binary files /dev/null and b/apps/extension/e2e/shots/matrix/chainflip-approval.png differ diff --git a/apps/extension/e2e/shots/matrix/chainflip-sign.png b/apps/extension/e2e/shots/matrix/chainflip-sign.png new file mode 100644 index 00000000..de35715b Binary files /dev/null and b/apps/extension/e2e/shots/matrix/chainflip-sign.png differ diff --git a/apps/extension/e2e/shots/matrix/cosmos-sign.png b/apps/extension/e2e/shots/matrix/cosmos-sign.png new file mode 100644 index 00000000..66a5cd00 Binary files /dev/null and b/apps/extension/e2e/shots/matrix/cosmos-sign.png differ diff --git a/apps/extension/e2e/shots/matrix/fuel-sign.png b/apps/extension/e2e/shots/matrix/fuel-sign.png new file mode 100644 index 00000000..75ccd56b Binary files /dev/null and b/apps/extension/e2e/shots/matrix/fuel-sign.png differ diff --git a/apps/extension/e2e/shots/matrix/multiversx-sign.png b/apps/extension/e2e/shots/matrix/multiversx-sign.png new file mode 100644 index 00000000..1039009a Binary files /dev/null and b/apps/extension/e2e/shots/matrix/multiversx-sign.png differ diff --git a/apps/extension/e2e/shots/matrix/results.json b/apps/extension/e2e/shots/matrix/results.json index 3832c0d9..5bf15a8a 100644 --- a/apps/extension/e2e/shots/matrix/results.json +++ b/apps/extension/e2e/shots/matrix/results.json @@ -34,12 +34,12 @@ }, "approval": { "status": "pass", - "why": "Send <0.000001 ETH to 0x05AC…2717", + "why": "Send <0.000001 ETH to 0x05AC\u20262717", "detail": { - "title": "Send <0.000001 ETH to 0x05AC…2717", + "title": "Send <0.000001 ETH to 0x05AC\u20262717", "network": "Sepolia", "rows": "Fee $0.07 Ready in about 10 seconds To 0x05ACD02A8E18c130D902FB732bb6AD22DA4f2717 Amount <0.000001 ETH Requested by matrix-dapp.example Network fee at most 0.000074 ETH", - "details": "1 Network fee 2 Send <0.000001 ETH to 0x05AC…2717 If it fails, nothing leaves your balance.", + "details": "1 Network fee 2 Send <0.000001 ETH to 0x05AC\u20262717 If it fails, nothing leaves your balance.", "notices": [ "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." ], @@ -96,12 +96,12 @@ }, "approval": { "status": "pass", - "why": "Send <0.000001 HBAR to 0x05AC…2717", + "why": "Send <0.000001 HBAR to 0x05AC\u20262717", "detail": { - "title": "Send <0.000001 HBAR to 0x05AC…2717", + "title": "Send <0.000001 HBAR to 0x05AC\u20262717", "network": "Hedera Testnet (EVM)", "rows": "From Your balance Fee $0.0023 Ready in about 10 seconds To 0x05ACD02A8E18c130D902FB732bb6AD22DA4f2717 Amount <0.000001 HBAR Requested by matrix-dapp.example Network fee at most 0.067385 HBAR", - "details": "1 Network fee 2 Send <0.000001 HBAR to 0x05AC…2717 −<0.000001 HBAR If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "details": "1 Network fee 2 Send <0.000001 HBAR to 0x05AC\u20262717 \u2212<0.000001 HBAR If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", "notices": [ "We couldn't preview the exact result of this on the network. Check the details before you approve.", "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." @@ -184,12 +184,12 @@ }, "approval": { "status": "pass", - "why": "Send 0.000000001 SOL to G1zR…dxzk", + "why": "Send 0.000000001 SOL to G1zR\u2026dxzk", "detail": { - "title": "Send 0.000000001 SOL to G1zR…dxzk", + "title": "Send 0.000000001 SOL to G1zR\u2026dxzk", "network": "Solana Devnet", "rows": "Fee $0.0006 Ready in a moment To G1zRMYPBYE6CvsvZY1gAwfNnr5ZjW6eZJeGARQZvdxzk (you) gets 0.000000001 SOL Network fee 0.000005 SOL", - "details": "1 Network fee 2 Send 0.000000001 SOL to G1zR…dxzk If it fails, nothing leaves your balance.", + "details": "1 Network fee 2 Send 0.000000001 SOL to G1zR\u2026dxzk If it fails, nothing leaves your balance.", "notices": [ "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." ], @@ -312,12 +312,12 @@ }, "approval": { "status": "pass", - "why": "Send 0.000000001 SUI to 0x6103…c5f9", + "why": "Send 0.000000001 SUI to 0x6103\u2026c5f9", "detail": { - "title": "Send 0.000000001 SUI to 0x6103…c5f9", + "title": "Send 0.000000001 SUI to 0x6103\u2026c5f9", "network": "Sui Testnet", "rows": "Fee $0.0012 Ready in a moment Sends to Your own account Network fee 0.00100988 SUI (at most 0.002988)", - "details": "1 Network fee 2 Send 0.000000001 SUI to 0x6103…c5f9 If it fails, nothing leaves your balance.", + "details": "1 Network fee 2 Send 0.000000001 SUI to 0x6103\u2026c5f9 If it fails, nothing leaves your balance.", "notices": [ "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." ], @@ -434,7 +434,7 @@ "approval": { "title": "Sign a message for matrix-dapp.example", "network": "Cardano Preprod", - "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet) Signed as addr_test1qr…v5dr79", + "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet) Signed as addr_test1qr\u2026v5dr79", "details": "1 Sign a message for matrix-dapp.example If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", "notices": [ "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." @@ -508,12 +508,12 @@ }, "approval": { "status": "pass", - "why": "Send 0.000000000001 WND to 5Gj64M…HCQb", + "why": "Send 0.000000000001 WND to 5Gj64M\u2026HCQb", "detail": { - "title": "Send 0.000000000001 WND to 5Gj64M…HCQb", + "title": "Send 0.000000000001 WND to 5Gj64M\u2026HCQb", "network": "Westend", "rows": "Fee $0.02 Ready in about 10 seconds To 5Gj64MPAV7C2ezdS5tpq4fVgDYteHCptB62RFxvaVyECHCQb Amount 0.000000000001 WND Network fee 0.015627473774 WND Valid for about 6 minutes Sent by matrix-dapp.example (it gets your signature)", - "details": "1 Network fee 2 Send 0.000000000001 WND to 5Gj64M…HCQb If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "details": "1 Network fee 2 Send 0.000000000001 WND to 5Gj64M\u2026HCQb If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", "notices": [ "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." ], @@ -571,12 +571,12 @@ }, "approval": { "status": "pass", - "why": "Send 0.000000000000000001 STRK to 0x014f…e6b8", + "why": "Send 0.000000000000000001 STRK to 0x014f\u2026e6b8", "detail": { - "title": "Send 0.000000000000000001 STRK to 0x014f…e6b8", + "title": "Send 0.000000000000000001 STRK to 0x014f\u2026e6b8", "network": "Starknet Sepolia", "rows": "Fee $0.001 Ready in about 10 seconds To 0x014ff78c22689465708f38822fe1cc843d857e3b29647ba01908074e65d8e6b8 Amount 0.000000000000000001 STRK Network fee 0.018781775980538304 STRK Network fee at most 0.042259649934599701 STRK", - "details": "1 Network fee 2 Send 0.000000000000000001 STRK to 0x014f…e6b8 If it fails, nothing leaves your balance.", + "details": "1 Network fee 2 Send 0.000000000000000001 STRK to 0x014f\u2026e6b8 If it fails, nothing leaves your balance.", "notices": [ "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." ], @@ -634,12 +634,12 @@ }, "approval": { "status": "pass", - "why": "Send 0.000000001 GRAM to 0QBQ…aXFi", + "why": "Send 0.000000001 GRAM to 0QBQ\u2026aXFi", "detail": { - "title": "Send 0.000000001 GRAM to 0QBQ…aXFi", + "title": "Send 0.000000001 GRAM to 0QBQ\u2026aXFi", "network": "TON Testnet", - "rows": "From Your balance Fee $0.0011 Ready in about 5 seconds To 0QBQBJfGnfXxWzVRhBaupSxxbZH81iSAlSPw7YdZNopaaXFi Amount 0.000000001 GRAM Wallet Not active yet. This first transfer also activates it. Network fee ≈ 0.000742136 GRAM", - "details": "1 Network fee 2 Send 0.000000001 GRAM to 0QBQ…aXFi −<0.000001 GRAM Settles once delivery is proven. If it doesn't arrive in time, the money comes back to you. These changes are estimated; this network can't preview them.", + "rows": "From Your balance Fee $0.0011 Ready in about 5 seconds To 0QBQBJfGnfXxWzVRhBaupSxxbZH81iSAlSPw7YdZNopaaXFi Amount 0.000000001 GRAM Wallet Not active yet. This first transfer also activates it. Network fee \u2248 0.000742136 GRAM", + "details": "1 Network fee 2 Send 0.000000001 GRAM to 0QBQ\u2026aXFi \u2212<0.000001 GRAM Settles once delivery is proven. If it doesn't arrive in time, the money comes back to you. These changes are estimated; this network can't preview them.", "notices": [ "Paying on this network from another one isn't available yet. Today we can pay on Hedera.", "You don't have enough GRAM for this and its network fee.", @@ -764,12 +764,12 @@ }, "approval": { "status": "pass", - "why": "Send 0.0000001 XLM to GB4N…VYVJ", + "why": "Send 0.0000001 XLM to GB4N\u2026VYVJ", "detail": { - "title": "Send 0.0000001 XLM to GB4N…VYVJ", + "title": "Send 0.0000001 XLM to GB4N\u2026VYVJ", "network": "Stellar Testnet", "rows": "Fee $0.00 Ready in about 5 seconds To GB4NG3E6SHS5PKVVIRSYJZFG5GKLBLHLR7B46F2HJF5L2FZIKGYBVYVJ Asset XLM (native) Valid until 2026-10-05T14:04:00Z Network fee up to 0.00001 XLM Sent by matrix-dapp.example (it gets the signed transaction)", - "details": "1 Network fee 2 Send 0.0000001 XLM to GB4N…VYVJ If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "details": "1 Network fee 2 Send 0.0000001 XLM to GB4N\u2026VYVJ If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", "notices": [ "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." ], @@ -906,7 +906,7 @@ "levels": { "connect": { "status": "pass", - "why": "EIP-6963 \"Use Clip Wallet\" → eth_requestAccounts → 0x05ACD02A8E18c130D902FB732bb6AD22DA4f2717" + "why": "EIP-6963 \"Use Clip Wallet\" \u2192 eth_requestAccounts \u2192 0x05ACD02A8E18c130D902FB732bb6AD22DA4f2717" }, "sign": { "status": "n/a", @@ -925,5 +925,390 @@ "evm-live-connected.png" ], "at": "2026-10-05T14:00:16.723Z" + }, + "cosmos": { + "target": "cosmos", + "network": "cosmos:osmo-test-5", + "dapp": "Keplr API on window.clipwallet.cosmos + @cosmjs/stargate SigningStargateClient in a local page, osmo-test-5", + "dappWhy": "Osmosis dapps talk to Keplr's API through cosmjs; the hosted Osmosis frontend only knows wallet-specific globals", + "levels": { + "connect": { + "status": "pass", + "why": "account osmo1re86ed5ck45dnyex8ajvytrae4c5ct7xjm6edd", + "detail": { + "address": "osmo1re86ed5ck45dnyex8ajvytrae4c5ct7xjm6edd", + "algo": "secp256k1", + "accounts": 1 + } + }, + "sign": { + "status": "pass", + "why": "ADR-36 amino doc (Keplr makeADR36AminoSignDoc) via @cosmjs/amino, sha256, @cosmjs/crypto Secp256k1.verifySignature", + "detail": { + "valid": true, + "how": "ADR-36 amino doc (Keplr makeADR36AminoSignDoc) via @cosmjs/amino, sha256, @cosmjs/crypto Secp256k1.verifySignature", + "approval": { + "title": "Sign a message for matrix-dapp.example", + "network": "Osmosis Testnet", + "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet)", + "details": "1 Sign a message for matrix-dapp.example If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + } + }, + "approval": { + "status": "skip", + "why": "needs funds first: the account doesn't exist on osmo-test-5 yet" + }, + "send": { + "status": "skip", + "why": "needs 0.1 OSMO on Osmosis Testnet at osmo1re86ed5ck45dnyex8ajvytrae4c5ct7xjm6edd; has 0", + "detail": { + "declinedAs": "NeedsFunds" + } + } + }, + "shots": [ + "cosmos-sign.png" + ], + "at": "2026-10-06T12:16:20.574Z" + }, + "tron": { + "target": "tron", + "network": "tron:0xcd8690dc", + "dapp": "TIP-6963 discovery + TIP-1193 provider + TronWeb 6 in a local page, Nile", + "dappWhy": "TIP-6963/TIP-1193 is the TRON standard for injected wallets; tronwallet-adapters has no adapter for a generic TIP-6963 wallet", + "levels": { + "connect": { + "status": "pass", + "why": "account TGZxYSGcQHaZMcSB2c7ZB1n97yMk2UPz41", + "detail": { + "address": "TGZxYSGcQHaZMcSB2c7ZB1n97yMk2UPz41", + "chainId": "0xcd8690dc", + "name": "Clip Wallet" + } + }, + "sign": { + "status": "pass", + "why": "TronWeb Trx.verifyMessageV2 (TIP-104 signMessageV2)", + "detail": { + "valid": true, + "how": "TronWeb Trx.verifyMessageV2 (TIP-104 signMessageV2)", + "approval": { + "title": "Sign a message for matrix-dapp.example", + "network": "TRON Nile Testnet", + "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet)", + "details": "1 Sign a message for matrix-dapp.example If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + } + }, + "approval": { + "status": "skip", + "why": "needs funds first: the account doesn't exist on Nile yet (receive TRX first)" + }, + "send": { + "status": "skip", + "why": "needs 2 TRX on TRON Nile Testnet at TGZxYSGcQHaZMcSB2c7ZB1n97yMk2UPz41; has 0", + "detail": { + "declinedAs": "NeedsFunds" + } + } + }, + "shots": [ + "tron-sign.png" + ], + "at": "2026-10-06T12:16:37.425Z" + }, + "stacks": { + "target": "stacks", + "network": "stacks:2147483648", + "dapp": "@stacks/connect 8 request() (SIP-030) + WBIP-004 discovery + @stacks/encryption in a local page, testnet", + "dappWhy": "Connect's request() is what Stacks dapps call (the picker modal lists wbip_providers; the page picks Clip's entry instead of clicking the modal)", + "levels": { + "connect": { + "status": "pass", + "why": "account ST1EC4TTQPQX24TT3VY2H6YYW99N0HRQ5VMRB2M1T", + "detail": { + "address": "ST1EC4TTQPQX24TT3VY2H6YYW99N0HRQ5VMRB2M1T", + "publicKey": "02eeedf4fbb22fb0a2d49fdcea998350db0533f76719fafde4c17d0484a0dd8b39", + "providerId": "clipwallet.stacks" + } + }, + "sign": { + "status": "pass", + "why": "@stacks/encryption verifyMessageSignatureRsv (\\x17Stacks Signed Message prefix) + getAddressFromPublicKey === connected address", + "detail": { + "valid": true, + "how": "@stacks/encryption verifyMessageSignatureRsv (\\x17Stacks Signed Message prefix) + getAddressFromPublicKey === connected address", + "approval": { + "title": "Sign a message for matrix-dapp.example", + "network": "Stacks Testnet", + "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet)", + "details": "1 Sign a message for matrix-dapp.example If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + } + }, + "approval": { + "status": "pass", + "why": "Send 0.000001 STX to ST000\u2026W42H", + "detail": { + "title": "Send 0.000001 STX to ST000\u2026W42H", + "network": "Stacks Testnet", + "rows": "From Your balance Fee 0.000387 STX Ready in about 10 seconds To ST000000000000000000002AMW42H Amount 0.000001 STX Memo clip matrix Network fee 0.000387 STX Nonce 0", + "details": "1 Network fee 2 Send 0.000001 STX to ST000\u2026W42H \u22120.000001 STX If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + }, + "send": { + "status": "pass", + "why": "https://explorer.hiro.so/txid/0xdcb787f4291329664e5b5d8df9dac80770639b8df2a45394259153a435b2d069?chain=testnet", + "detail": { + "id": "dcb787f4291329664e5b5d8df9dac80770639b8df2a45394259153a435b2d069", + "publicKey": "02eeedf4fbb22fb0a2d49fdcea998350db0533f76719fafde4c17d0484a0dd8b39" + } + } + }, + "shots": [ + "stacks-sign.png", + "stacks-approval.png" + ], + "at": "2026-10-06T12:16:54.327Z" + }, + "fuel": { + "target": "fuel", + "network": "fuel:0", + "dapp": "fuels-ts Fuel connector manager (FuelConnector event discovery) + fuels Provider in a local page, Fuel testnet", + "dappWhy": "fuels-ts Fuel is the connector API every Fuel dapp (and @fuels/react) uses; a local page avoids a dapp's own build and its WalletConnect setup", + "levels": { + "connect": { + "status": "pass", + "why": "account 0x1Cc3e8E1B340846d9Eb275D51435189deDe13f20801bbc3693B2D04c1B322A75", + "detail": { + "address": "0x1Cc3e8E1B340846d9Eb275D51435189deDe13f20801bbc3693B2D04c1B322A75", + "connector": "Clip Wallet", + "accounts": [ + "0x1Cc3e8E1B340846d9Eb275D51435189deDe13f20801bbc3693B2D04c1B322A75" + ], + "chainId": 0 + } + }, + "sign": { + "status": "pass", + "why": "fuels-ts Signer.recoverAddress(hashMessage(message), signature)", + "detail": { + "valid": true, + "how": "fuels-ts Signer.recoverAddress(hashMessage(message), signature)", + "signature": "0x37441bc6e6fd214cfdec31ed174524898ba4b4f4d11cfc4c5f1c98bf915c4734babb7814aeab15e704f85743b58be752fd8acb58a186ea4cc100ecd431168082", + "approval": { + "title": "Sign a message for matrix-dapp.example", + "network": "Fuel Testnet", + "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet)", + "details": "1 Sign a message for matrix-dapp.example If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + } + }, + "approval": { + "status": "skip", + "why": "needs funds first: the Fuel testnet account has no ETH (faucet-testnet.fuel.network)" + }, + "send": { + "status": "skip", + "why": "needs 0.0000002 ETH on Fuel Testnet at 0x1Cc3e8E1B340846d9Eb275D51435189deDe13f20801bbc3693B2D04c1B322A75; has 0", + "detail": { + "declinedAs": "NeedsFunds" + } + } + }, + "shots": [ + "fuel-sign.png" + ], + "at": "2026-10-06T12:17:27.393Z" + }, + "chainflip": { + "target": "chainflip", + "network": "polkadot:7a5d4db858ada1d20ed6ded4933c3331", + "dapp": "@polkadot/extension-dapp + @polkadot/api in a local page, Chainflip Perseverance", + "dappWhy": "what lp.chainflip.io uses to reach Polkadot-style wallets; the portal itself needs a funded LP account", + "levels": { + "connect": { + "status": "pass", + "why": "account cFNXs2hYYRhGp1A4AfXaTbK3vQSgnSLvtvJrvJrCbNvhtTKc1", + "detail": { + "address": "cFNXs2hYYRhGp1A4AfXaTbK3vQSgnSLvtvJrvJrCbNvhtTKc1", + "source": "clip-wallet" + } + }, + "sign": { + "status": "pass", + "why": "@polkadot/util-crypto signatureVerify over -wrapped message", + "detail": { + "valid": true, + "how": "@polkadot/util-crypto signatureVerify over -wrapped message", + "approval": { + "title": "Sign a message for matrix-dapp.example", + "network": "Westend", + "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet)", + "details": "1 Sign a message for matrix-dapp.example If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + } + }, + "approval": { + "status": "pass", + "why": "Register as a Chainflip liquidity provider", + "detail": { + "title": "Register as a Chainflip liquidity provider", + "network": "Chainflip Perseverance", + "rows": "Fee 0.000006 FLIP Ready in about 10 seconds Network fee 0.000006079018 FLIP Valid for about 6 minutes Sent by matrix-dapp.example (it gets your signature)", + "details": "1 Network fee 2 Register as a Chainflip liquidity provider Settles once delivery is proven. If it doesn't arrive in time, the money comes back to you. These changes are estimated; this network can't preview them.", + "notices": [ + "Paying on this network from another one isn't available yet. Today we can pay on Hedera.", + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": false, + "overflowing": [], + "declinedAs": "ProviderRpcError 4001" + } + }, + "send": { + "status": "skip", + "why": "needs 0.1 FLIP on Chainflip Perseverance at cFNXs2hYYRhGp1A4AfXaTbK3vQSgnSLvtvJrvJrCbNvhtTKc1; has 0", + "detail": { + "declinedAs": "ProviderRpcError 4001" + } + } + }, + "shots": [ + "chainflip-sign.png", + "chainflip-approval.png" + ], + "at": "2026-10-06T12:17:43.985Z" + }, + "multiversx": { + "target": "multiversx", + "network": "mvx:D", + "dapp": "@multiversx/sdk-dapp 5.7.3 initApp + ProviderFactory (custom provider from window.multiversx.providers), native auth, devnet", + "dappWhy": "sdk-dapp is how MultiversX dapps connect wallets; it has no wallet discovery, so Clip lists itself through the undocumented window.multiversx.providers hook", + "levels": { + "connect": { + "status": "pass", + "why": "account erd1qhkcc53w80nppnsprtzs7k2wh7xwvt54c8u88d0cd6pn3n6ldudshmqgfg", + "detail": { + "address": "erd1qhkcc53w80nppnsprtzs7k2wh7xwvt54c8u88d0cd6pn3n6ldudshmqgfg", + "name": "Clip Wallet" + } + }, + "sign": { + "status": "pass", + "why": "sdk-dapp verifyMessage (MessageComputer + UserVerifier)", + "detail": { + "valid": true, + "how": "sdk-dapp verifyMessage (MessageComputer + UserVerifier)", + "approval": { + "title": "Sign a message for matrix-dapp.example", + "network": "MultiversX Devnet", + "rows": "Message Clip Wallet dapp matrix: sign-in check (testnet)", + "details": "1 Sign a message for matrix-dapp.example If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + } + }, + "approval": { + "status": "skip", + "why": "needs funds first: the account has no devnet EGLD for the fee (devnet wallet faucet)" + }, + "send": { + "status": "skip", + "why": "needs 0.01 EGLD on MultiversX Devnet at erd1qhkcc53w80nppnsprtzs7k2wh7xwvt54c8u88d0cd6pn3n6ldudshmqgfg; has 0", + "detail": { + "declinedAs": "NeedsFunds" + } + } + }, + "shots": [ + "multiversx-sign.png" + ], + "at": "2026-10-06T12:25:02.345Z" + }, + "xrpl": { + "target": "xrpl", + "network": "xrpl:1", + "dapp": "XLS-72d Wallet Standard discovery (@wallet-standard/app + the @xrpl-wallet-standard/app filter) in a local page, testnet", + "dappWhy": "@xrpl-wallet-standard/app only wraps getWallets() with that filter but pulls in xrpl.js and an alpha Xahau fork; the page runs the same filter", + "levels": { + "connect": { + "status": "pass", + "why": "account rDvWLtQL3Furv3sVzERai1MXvKFTQjZgrM", + "detail": { + "address": "rDvWLtQL3Furv3sVzERai1MXvKFTQjZgrM", + "chains": [ + "xrpl:1", + "xrpl:2" + ] + } + }, + "sign": { + "status": "n/a", + "why": "XLS-72d (the XRPL browser wallet standard) has no message signing." + }, + "approval": { + "status": "pass", + "why": "Change your account settings", + "detail": { + "title": "Change your account settings", + "network": "XRP Ledger Testnet", + "rows": "Fee 0.00001 XRP Ready in about 5 seconds Memo clip dapp matrix Network fee 0.00001 XRP", + "details": "1 Network fee 2 Change your account settings If it fails, nothing leaves your balance. These changes are estimated; this network can't preview them.", + "notices": [ + "matrix-dapp.example isn't a site Clip Wallet recognises. Only continue if you opened it yourself." + ], + "approveEnabled": true, + "overflowing": [] + } + }, + "send": { + "status": "pass", + "why": "https://testnet.xrpl.org/transactions/601CC50503CD39B6783D609863C218159DC44EE1C7E34FB4DC51EA13BD8D8E32", + "detail": { + "id": "601CC50503CD39B6783D609863C218159DC44EE1C7E34FB4DC51EA13BD8D8E32", + "validated": true + } + } + }, + "shots": [ + "xrpl-approval.png" + ], + "at": "2026-10-06T12:25:22.497Z" } } diff --git a/apps/extension/e2e/shots/matrix/stacks-approval.png b/apps/extension/e2e/shots/matrix/stacks-approval.png new file mode 100644 index 00000000..efcd671b Binary files /dev/null and b/apps/extension/e2e/shots/matrix/stacks-approval.png differ diff --git a/apps/extension/e2e/shots/matrix/stacks-sign.png b/apps/extension/e2e/shots/matrix/stacks-sign.png new file mode 100644 index 00000000..2d34398b Binary files /dev/null and b/apps/extension/e2e/shots/matrix/stacks-sign.png differ diff --git a/apps/extension/e2e/shots/matrix/tron-sign.png b/apps/extension/e2e/shots/matrix/tron-sign.png new file mode 100644 index 00000000..c70b5a48 Binary files /dev/null and b/apps/extension/e2e/shots/matrix/tron-sign.png differ diff --git a/apps/extension/e2e/shots/matrix/xrpl-approval.png b/apps/extension/e2e/shots/matrix/xrpl-approval.png new file mode 100644 index 00000000..ccc262f7 Binary files /dev/null and b/apps/extension/e2e/shots/matrix/xrpl-approval.png differ diff --git a/apps/extension/package.json b/apps/extension/package.json index c174be2d..2fef3397 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -41,25 +41,41 @@ "@aptos-labs/wallet-standard": "^2.0.0", "@cardano-foundation/cardano-verify-datasignature": "1.0.11", "@clip-wallet/chains-algorand": "workspace:*", + "@clip-wallet/chains-antelope": "workspace:*", "@clip-wallet/chains-aptos": "workspace:*", "@clip-wallet/chains-bitcoin": "workspace:*", + "@clip-wallet/chains-bitcoincash": "workspace:*", "@clip-wallet/chains-cardano": "workspace:*", + "@clip-wallet/chains-cosmos": "workspace:*", "@clip-wallet/chains-evm": "workspace:*", + "@clip-wallet/chains-fuel": "workspace:*", "@clip-wallet/chains-hedera": "workspace:*", + "@clip-wallet/chains-icp": "workspace:*", + "@clip-wallet/chains-multiversx": "workspace:*", "@clip-wallet/chains-near": "workspace:*", "@clip-wallet/chains-solana": "workspace:*", + "@clip-wallet/chains-stacks": "workspace:*", "@clip-wallet/chains-starknet": "workspace:*", "@clip-wallet/chains-stellar": "workspace:*", "@clip-wallet/chains-substrate": "workspace:*", "@clip-wallet/chains-sui": "workspace:*", "@clip-wallet/chains-tezos": "workspace:*", "@clip-wallet/chains-ton": "workspace:*", + "@clip-wallet/chains-tron": "workspace:*", + "@clip-wallet/chains-xrpl": "workspace:*", "@clip-wallet/connect": "workspace:*", "@clip-wallet/core": "workspace:*", "@clip-wallet/kit-modules": "workspace:*", "@clip-wallet/ui": "workspace:*", + "@cosmjs/amino": "^0.39.0", + "@cosmjs/crypto": "^0.39.0", + "@cosmjs/encoding": "^0.39.0", + "@cosmjs/stargate": "^0.39.0", "@hashgraph/hedera-wallet-connect": "2.1.3", "@hiero-ledger/sdk": "^2.89.1", + "@multiversx/sdk-core": "^15.4.1", + "@multiversx/sdk-dapp": "5.7.3", + "@multiversx/sdk-dapp-utils": "^3.1.0", "@mysten/sui": "^2.34.0", "@mysten/wallet-standard": "^0.21.32", "@near-js/crypto": "^2.5.1", @@ -78,6 +94,9 @@ "@solana/wallet-standard-util": "^1.1.4", "@solana/wallet-standard-wallet-adapter-base": "^1.1.6", "@solana/web3.js": "^1.99.0", + "@stacks/connect": "8.2.7", + "@stacks/encryption": "7.6.0", + "@stacks/transactions": "7.6.0", "@starknet-io/get-starknet-core": "4.0.8", "@stellar/stellar-base": "^15.0.0", "@taquito/taquito": "25.0.0", @@ -92,12 +111,17 @@ "@wallet-standard/app": "^1.1.1", "@wxt-dev/module-react": "^1.2.2", "algosdk": "^3.8.0", + "axios": "^1.20.0", + "bignumber.js": "^9.3.1", "bip322-js": "4.0.0", "buffer": "6.0.3", "cborg": "^6.1.3", "esbuild": "^0.28.2", "events": "3.3.0", + "fuels": "0.103.0", + "protobufjs": "^7.6.6", "starknet": "^10.8.0", + "tronweb": "^6.5.1", "viem": "^2.57.1", "wagmi": "^3.7.7", "wxt": "^0.21.4" diff --git a/apps/extension/screenshots/home-dark.png b/apps/extension/screenshots/home-dark.png index 8455ce3c..77fa0ecb 100644 Binary files a/apps/extension/screenshots/home-dark.png and b/apps/extension/screenshots/home-dark.png differ diff --git a/apps/extension/screenshots/receive-all-families.png b/apps/extension/screenshots/receive-all-families.png index fc1020b6..7b62c9cf 100644 Binary files a/apps/extension/screenshots/receive-all-families.png and b/apps/extension/screenshots/receive-all-families.png differ diff --git a/apps/extension/screenshots/receive-osmo.png b/apps/extension/screenshots/receive-osmo.png new file mode 100644 index 00000000..6a5d510f Binary files /dev/null and b/apps/extension/screenshots/receive-osmo.png differ diff --git a/apps/mobile/clip.config.ts b/apps/mobile/clip.config.ts index ff3bd72f..896bc14d 100644 --- a/apps/mobile/clip.config.ts +++ b/apps/mobile/clip.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ icon: "./assets/icon.svg", theme: { accent: "#FF3C00", accentText: "#FFFFFF", font: "Inter", radius: 14 }, // All 14 families, testnets only (same as the extension). - networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand"], + networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", "cosmos", "provenance", "thorchain", "initia", "tron", "xrpl", "antelope", "multiversx", "icp", "stacks", "fuel", "bitcoincash"], passkeys: { enabled: true }, walletConnect: {}, services: MAINNET ? {} : TESTNET_SERVICES, diff --git a/docs/r1/dapp-matrix-funding.md b/docs/r1/dapp-matrix-funding.md index a646e06b..63cff53b 100644 --- a/docs/r1/dapp-matrix-funding.md +++ b/docs/r1/dapp-matrix-funding.md @@ -81,3 +81,29 @@ Both Hedera rows are this matrix's wallet, on Hedera testnet: account, as they would with MetaMask. Send to it as an EVM address (the portal faucet accepts both). 10 HBAR each is plenty; 2 HBAR each is the floor the matrix checks for. + +## networks87 accounts (2026-10-06) + +Account 0 of the new families, derived from the same matrix phrase (public keys in `addresses.json`; `node +scripts/dapp-matrix-balances.mjs` re-derives every address with the family's own chain module: all "ok"). Addresses +are spelled for the testnet. I used only plain public APIs with no captcha, account or terms (Hiro's STX faucet API, +the XRPL testnet faucet API); everything else, and every account creation, is for you. + +| Target | Testnet | Receive address | Faucet | What the faucet needs | Matrix minimum | Status | +| --- | --- | --- | --- | --- | --- | --- | +| Stacks | testnet | `ST1EC4TTQPQX24TT3VY2H6YYW99N0HRQ5VMRB2M1T` | `POST https://api.testnet.hiro.so/extended/v1/faucets/stx?address=…` | **API-only** | 0.01 STX | **Funded by me:** 500 STX, tx `0x5fa11ca9fd59c0900a9d6bf45589ca310993d20c1999f9d596d04fa6ab151cd3`; L3 passed | +| XRP Ledger | testnet | `rDvWLtQL3Furv3sVzERai1MXvKFTQjZgrM` | `POST https://faucet.altnet.rippletest.net/accounts {"destination":"r…"}` | **API-only** | 1.1 XRP (1 XRP reserve + fees) | **Funded by me:** 100 XRP, tx `7207EF46614D51E4134F93EFAC19E4FF8E1CCAF2789102933D82307631D976EB`; L3 passed | +| Cosmos (Osmosis) | osmo-test-5 | `osmo1re86ed5ck45dnyex8ajvytrae4c5ct7xjm6edd` | [faucet.testnet.osmosis.zone](https://faucet.testnet.osmosis.zone) | Cloudflare Turnstile captcha | 0.1 OSMO | **You fund this** | +| TRON | Nile | `TGZxYSGcQHaZMcSB2c7ZB1n97yMk2UPz41` | [nileex.io/join/getJoinPage](https://nileex.io/join/getJoinPage) | Turnstile captcha | 2 TRX | **You fund this** | +| Fuel | testnet | `0x1Cc3e8E1B340846d9Eb275D51435189deDe13f20801bbc3693B2D04c1B322A75` | [faucet-testnet.fuel.network](https://faucet-testnet.fuel.network) | Cloudflare challenge | any ETH (0.0000002) | **You fund this** | +| Chainflip | Perseverance | `cFNXs2hYYRhGp1A4AfXaTbK3vQSgnSLvtvJrvJrCbNvhtTKc1` | tFLIP (ERC-20 on Sepolia `0xdC27c60956cB065D19F08bb69a707E37b36d8086`) from the Chainflip Discord, then fund the State Chain account at [auctions.perseverance.chainflip.io](https://auctions.perseverance.chainflip.io) from the matrix EVM account (it has Sepolia ETH) | Discord account | 0.1 FLIP | **You fund this** | +| MultiversX | devnet | `erd1qhkcc53w80nppnsprtzs7k2wh7xwvt54c8u88d0cd6pn3n6ldudshmqgfg` | [devnet-wallet.multiversx.com](https://devnet-wallet.multiversx.com) faucet (r3d4.fr/faucet untested) | Logged-in wallet + reCAPTCHA | 0.01 EGLD | **You fund this** | +| Initia | initiation-2 | `init1qkkdq25wrrqnpkgzldejhd4dytdy7fch0qn2v7` | [app.testnet.initia.xyz/faucet](https://app.testnet.initia.xyz/faucet) | Turnstile captcha | 0.1 INIT | balance only (no matrix page) | +| Provenance | pio-testnet-1 | `tp16f3hw77j3vwa3chtnplx3qs4sn2z4ww0cdzgzd` | none: the explorer faucet reached end of life | n/a | 1 HASH | no public faucet found | +| ICP | `icp:test` (DFINITY test ledgers on mainnet) | principal `gm6de-vncl6-w4cya-5z35g-cincb-f2zqe-6jhln-cnvjw-gvqf6-3shog-6ae` | [faucet.internetcomputer.org](https://faucet.internetcomputer.org) (10 TESTICP) | Web form (no login or captcha seen) | 0.001 TESTICP | **You fund this** (balance only) | +| Bitcoin Cash | chipnet | `bchtest:qq5jt8xqr6gckvpnn4dr4ajfd023mf7fzcj5tplmu2` | [tbch.googol.cash](https://tbch.googol.cash) | Image captcha | 5,000 sats | **You fund this** (balance only) | +| Antelope | Jungle4 | key `PUB_K1_7VxUN24cMwi94AHSbLkSnbKNfwoN8wjV4Fv1FA8W7bXQdTVzro` (no account yet) | account: `POST https://jungle4.greymass.com/account/create` (no captcha) or [monitor.jungletestnet.io](https://monitor.jungletestnet.io); tokens: Jungle4 faucet (reCAPTCHA). Telos testnet: `POST https://api-dev.telos.net/v1/testnet/account` then `/faucet/`; XPR testnet needs an e-mail code | Creating an account is yours to do | an account + 1 EOS + CPU | **You create the account** (balance only) | +| dYdX, ZIGChain | dydx-testnet-4, zig-test-2 | `dydx1re86ed5ck45dnyex8ajvytrae4c5ct7xne8dmg`, `zig1re86ed5ck45dnyex8ajvytrae4c5ct7xm2vmeh` | dYdX `POST faucet.v4testnet.dydx.exchange/faucet/native-token` (unverified); faucet.zigchain.com (Cloudflare challenge) | | | not matrix targets (same provider as Osmosis) | +| THORChain | none | `thor13pajudku86vd59c8wkr4ctwcxdy8qpnap6afpx` (mainnet) | no public testnet (stagenet uses real funds) | | | not testable without mainnet funds | + +Then: `pnpm --filter @clip-wallet/extension matrix -- -g "matrix: (cosmos|tron|fuel|chainflip|multiversx)"`. diff --git a/docs/r1/dapp-matrix.md b/docs/r1/dapp-matrix.md index fb65bec7..70973a59 100644 --- a/docs/r1/dapp-matrix.md +++ b/docs/r1/dapp-matrix.md @@ -76,6 +76,31 @@ Tezos's first operation (reveal + transfer) Screenshots: `apps/extension/e2e/shots/matrix/` (`-sign.png`, `-approval.png`, `evm-live-connected.png`). Raw results: `apps/extension/e2e/shots/matrix/results.json`. +## networks87 rows (2026-10-06) + +The families added for the remaining CLPR networks ([networks87.md](networks87.md)), same levels and rules, run with +`pnpm --filter @clip-wallet/extension matrix -- -g "matrix: (cosmos|tron|stacks|fuel|xrpl|chainflip|multiversx)"`. +The existing rows were re-run in the same session with no change (all previous passes still pass). + +| Family | Testnet | Dapp library / discovery | L1 connect | L2 sign | L3 send (tx) | L4 approval | +| --- | --- | --- | --- | --- | --- | --- | +| Cosmos SDK (Osmosis) | osmo-test-5 | cosmjs `SigningStargateClient` over Clip's Keplr-compatible `window.clipwallet.cosmos` | pass | pass (ADR-36, `@cosmjs/crypto` `Secp256k1.verifySignature`) | skip: needs 0.1 OSMO | skip: "the account doesn't exist on osmo-test-5 yet" | +| TRON | Nile | raw TIP-6963 discovery + TIP-1193 (no tronwallet adapter accepts a third-party TIP-6963 wallet), TronWeb | pass | pass (TronWeb `Trx.verifyMessageV2`) | skip: needs 2 TRX | skip: "the account doesn't exist on Nile yet" | +| Stacks | testnet | `@stacks/connect` 8 `request()` through WBIP-004 `window.wbip_providers` | pass | pass (`verifyMessageSignatureRsv`) | **pass** [0xdcb787f4…d069](https://explorer.hiro.so/txid/0xdcb787f4291329664e5b5d8df9dac80770639b8df2a45394259153a435b2d069?chain=testnet) | pass: "Send 0.000001 STX to ST000…W42H" | +| Fuel | testnet | fuels-ts `Fuel` connectors list → Clip's FuelConnector | pass | pass (`Signer.recoverAddress(hashMessage(…))`) | skip: needs testnet ETH | skip: needs funds first | +| XRP Ledger | testnet | `@wallet-standard/app` with the `@xrpl-wallet-standard/app` feature filter (XLS-72d) | pass | n/a (XLS-72d has no message signing) | **pass** [601CC505…8E32](https://testnet.xrpl.org/transactions/601CC50503CD39B6783D609863C218159DC44EE1C7E34FB4DC51EA13BD8D8E32) (no-op AccountSet with a memo: rippled refuses a payment to yourself) | pass: "Change your account settings", memo shown | +| Chainflip (substrate) | Perseverance | `@polkadot/extension-dapp` + `@polkadot/api`, as lp.chainflip.io | pass (cF… address) | pass (`signatureVerify`) | skip: needs FLIP (no plain FLIP transfer; L3 is `liquidityProvider.registerLpAccount`) | pass: "Register as a Chainflip liquidity provider" | +| MultiversX | devnet | `@multiversx/sdk-dapp` 5.7.3 `initApp` + `ProviderFactory` (custom provider from the **undocumented** `window.multiversx.providers` hook) | pass | pass (sdk-dapp `verifyMessage`) | skip: needs devnet EGLD | skip: needs funds first | + +No L1/L2 rows: Antelope, ICP (no wallet-side standard Clip can answer as itself), Bitcoin Cash (WalletConnect only; +needs a project id), Provenance, Initia (same Keplr-compatible provider as Osmosis, no separate page), THORChain (no +public testnet). Screenshots: `apps/extension/e2e/shots/matrix/{cosmos,tron,stacks,fuel,chainflip,multiversx}-sign.png`, +`stacks-approval.png`, `xrpl-approval.png`, `chainflip-approval.png`. + +Fixes the run found: the XRPL page and chains-xrpl now use `testnet.xrpl-labs.com` first (rippled's own testnet host +answers no CORS preflight, so a page's fetch failed); the MultiversX page connects without native auth (with it, +`login()` asks for a second, sign-in approval inside L1). + ## Which dapp each row uses, and why Live public dapps come first in the brief. In practice: diff --git a/docs/r1/networks87.md b/docs/r1/networks87.md new file mode 100644 index 00000000..3b439d15 --- /dev/null +++ b/docs/r1/networks87.md @@ -0,0 +1,200 @@ +# networks87: Clip Wallet on every CLPR network + +The 87 networks are `NETWORKS` in coldai-clpr-landing `artifacts/coldai-web/src/data/clpr-data.ts` (the networks with +CLPR verifier code in ColdAI's pull requests to LFDT-CLPR). The coverage below is computed from the wallet's own +catalogue, not written by hand: `packages/engine/test/networks87.test.ts` maps each CLPR network to the mainnet(s) in +`walletNetworks()` with every family on, fails if anything but the two networks below is missing, and prints the table +(`pnpm --filter @clip-wallet/engine exec vitest run test/networks87.test.ts`). + +## Result + +- **85 of the 87 CLPR networks are supported on mainnet.** Before this work it was 67 (Arc was in the list only as a + testnet, and 19 networks were missing). +- **Not supported: Canton and Mixin.** Neither has a model in which a self-custodial key wallet can hold and move funds + on its own (details below). +- **Supported mainnets, by the landing page's counting rule** (EVM `testnet: false` entries without "Hedera (EVM)", plus + every other family's mainnets, Polkadot SDK networks included): **93**, up from 74. 60 EVM + Hedera, Solana, + Bitcoin, Sui, Aptos, Cardano, Starknet, TON, NEAR, Stellar, Tezos, Algorand (1 each) + 5 Polkadot SDK (Polkadot, + Kusama, both Asset Hubs, Chainflip) + Osmosis, dYdX, ZIGChain + Provenance, THORChain, Initia + TRON, XRP Ledger, + MultiversX, Internet Computer, Stacks, Fuel, Bitcoin Cash + Vaulta, Telos (native), XPR Network. +- 93 counts networks the wallet ships, CLPR or not (Polkadot, Kusama and the Asset Hubs aren't CLPR networks; Telos + counts once as an EVM and once natively). + +**Wording that is true:** "Clip Wallet supports 85 of the 87 CLPR networks (all except Canton and Mixin, which have no +self-custodial wallet model) and 93 mainnets in total." **"All 87 CLPR networks" is not true** and can't be made true by +wallet code: see Canton and Mixin. + +## Gap table + +"already" = covered before this branch; "**new**" = added here. Network ids are CAIP-2 where a namespace exists +(sources in each package README). Dapp connectivity is per family; every family also sends and receives in the wallet +itself. + +| CLPR network | Status | Module and mainnet network id | Dapp connectivity | Notes | +| --- | --- | --- | --- | --- | +| Abstract | already | `chains-evm` `eip155:2741` | EIP-6963 / EIP-1193 | | +| Algorand | already | `chains-algorand` `algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k` | existing provider | | +| Anubis | already | `chains-evm` `eip155:6714` | EIP-6963 / EIP-1193 | | +| Arbitrum Nova | already | `chains-evm` `eip155:42170` | EIP-6963 / EIP-1193 | | +| Arbitrum One | already | `chains-evm` `eip155:42161` | EIP-6963 / EIP-1193 | | +| Arc | **new** | `chains-evm` `eip155:5042` | EIP-6963 / EIP-1193 | Arc mainnet (5042) went live 2026-09-16; before, only the testnet was in the list | +| Aurora | already | `chains-evm` `eip155:1313161554` | EIP-6963 / EIP-1193 | | +| Avalanche C-Chain | already | `chains-evm` `eip155:43114` | EIP-6963 / EIP-1193 | | +| BNB Smart Chain | already | `chains-evm` `eip155:56` | EIP-6963 / EIP-1193 | | +| BOB | already | `chains-evm` `eip155:60808` | EIP-6963 / EIP-1193 | | +| BOT Chain | already | `chains-evm` `eip155:677` | EIP-6963 / EIP-1193 | | +| Base | already | `chains-evm` `eip155:8453` | EIP-6963 / EIP-1193 | | +| Bifrost Network | already | `chains-evm` `eip155:3068` | EIP-6963 / EIP-1193 | EVM layer | +| Bitcoin | already | `chains-bitcoin` `bip122:000000000019d6689c085ae165831e93` | existing provider | | +| Bitcoin Cash | **new** | `chains-bitcoincash` `bip122:000000000000000000651ef99cb9fcbe` | WalletConnect wc2-bch-bcr (no injected standard) | | +| Bittensor | already | `chains-evm` `eip155:964` | EIP-6963 / EIP-1193 | EVM layer | +| Blast | already | `chains-evm` `eip155:81457` | EIP-6963 / EIP-1193 | | +| Canton | missing | **not supported** | n/a | see below | +| Cardano | already | `chains-cardano` `cip34:1-764824073` | existing provider | | +| Celo | already | `chains-evm` `eip155:42220` | EIP-6963 / EIP-1193 | | +| Chainflip | **new** | `chains-substrate` `polkadot:8b8c140b0af9db70686583e3f6bf2a59` | window.injectedWeb3 (polkadot extension-dapp) | State Chain FLIP from Flip.Account; no FLIP transfer exists (redeem to Ethereum); LP-portal calls decoded | +| Conflux | already | `chains-evm` `eip155:1030` | EIP-6963 / EIP-1193 | Conflux eSpace (EVM); Core Space not supported | +| Core | already | `chains-evm` `eip155:1116` | EIP-6963 / EIP-1193 | | +| Cronos | already | `chains-evm` `eip155:25` | EIP-6963 / EIP-1193 | EVM layer | +| Ethereum | already | `chains-evm` `eip155:1` | EIP-6963 / EIP-1193 | | +| Flare | already | `chains-evm` `eip155:14` | EIP-6963 / EIP-1193 | | +| Fraxtal | already | `chains-evm` `eip155:252` | EIP-6963 / EIP-1193 | | +| GRX Chain | already | `chains-evm` `eip155:1110` | EIP-6963 / EIP-1193 | | +| Gnosis Chain | already | `chains-evm` `eip155:100` | EIP-6963 / EIP-1193 | | +| Hydration | already | `chains-evm` `eip155:222222` | EIP-6963 / EIP-1193 | EVM layer | +| Hyperliquid | already | `chains-evm` `eip155:999` | EIP-6963 / EIP-1193 | HyperEVM | +| Immutable zkEVM | already | `chains-evm` `eip155:13371` | EIP-6963 / EIP-1193 | | +| Injective | already | `chains-evm` `eip155:1776` | EIP-6963 / EIP-1193 | EVM layer | +| Ink | already | `chains-evm` `eip155:57073` | EIP-6963 / EIP-1193 | | +| KUB Chain | already | `chains-evm` `eip155:96` | EIP-6963 / EIP-1193 | | +| Kaia | already | `chains-evm` `eip155:8217` | EIP-6963 / EIP-1193 | Kaia is EVM (8217) | +| Katana | already | `chains-evm` `eip155:747474` | EIP-6963 / EIP-1193 | | +| Kava | already | `chains-evm` `eip155:2222` | EIP-6963 / EIP-1193 | EVM layer | +| MANTRA | already | `chains-evm` `eip155:5888` | EIP-6963 / EIP-1193 | EVM layer | +| Mantle | already | `chains-evm` `eip155:5000` | EIP-6963 / EIP-1193 | | +| MegaETH | already | `chains-evm` `eip155:4326` | EIP-6963 / EIP-1193 | | +| Mezo | already | `chains-evm` `eip155:31612` | EIP-6963 / EIP-1193 | EVM layer | +| Mixin | missing | **not supported** | n/a | see below | +| Monad | already | `chains-evm` `eip155:143` | EIP-6963 / EIP-1193 | | +| NEAR | already | `chains-near` `near:mainnet` | existing provider | | +| OP Mainnet | already | `chains-evm` `eip155:10` | EIP-6963 / EIP-1193 | | +| Osmosis | **new** | `chains-cosmos` `cosmos:osmosis-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 118 | +| Plasma | already | `chains-evm` `eip155:9745` | EIP-6963 / EIP-1193 | | +| Plume | already | `chains-evm` `eip155:98866` | EIP-6963 / EIP-1193 | | +| Polygon PoS | already | `chains-evm` `eip155:137` | EIP-6963 / EIP-1193 | | +| Provenance | **new** | `chains-cosmos` `cosmos:pio-mainnet-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 505 (Keplr's testnet uses 1: different key there) | +| PulseChain | already | `chains-evm` `eip155:369` | EIP-6963 / EIP-1193 | | +| RISE | already | `chains-evm` `eip155:4153` | EIP-6963 / EIP-1193 | | +| Reya | already | `chains-evm` `eip155:1729` | EIP-6963 / EIP-1193 | | +| Robinhood Chain | already | `chains-evm` `eip155:4663` | EIP-6963 / EIP-1193 | | +| Ronin | already | `chains-evm` `eip155:2020` | EIP-6963 / EIP-1193 | | +| Rootstock | already | `chains-evm` `eip155:30` | EIP-6963 / EIP-1193 | | +| STRATO | **new** | `chains-evm` `eip155:123354377739506` | EIP-6963 / EIP-1193 | SolidVM, but nodes take signed legacy Ethereum transactions; flat 0.01 USDST fee per transaction | +| Sei | already | `chains-evm` `eip155:1329` | EIP-6963 / EIP-1193 | EVM layer | +| Solana | already | `chains-solana` `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | existing provider | | +| Soneium | already | `chains-evm` `eip155:1868` | EIP-6963 / EIP-1193 | | +| Stable | already | `chains-evm` `eip155:988` | EIP-6963 / EIP-1193 | EVM layer | +| Stacks | **new** | `chains-stacks` `stacks:1` | SIP-030 + WBIP-004 | | +| Starknet | already | `chains-starknet` `starknet:SN_MAIN` | existing provider | | +| Stellar | already | `chains-stellar` `stellar:pubnet` | existing provider | | +| THORChain | **new** | `chains-cosmos` `cosmos:thorchain-1` | Keplr-compatible API at window.clipwallet.cosmos | mainnet only: stagenet runs on real funds | +| TON | already | `chains-ton` `ton:-239` | existing provider | | +| TRON | **new** | `chains-tron` `tron:0x2b6653dc` | TIP-1193 + TIP-6963 | | +| Telos | already | `chains-evm` `eip155:40` | EIP-6963 / EIP-1193 | Telos EVM (and Telos native in chains-antelope) | +| Unichain | already | `chains-evm` `eip155:130` | EIP-6963 / EIP-1193 | | +| Vaulta | **new** | `chains-antelope` `antelope:aca376f206b8fc25a6ed44dbdc66547c` | none: send/receive only | accounts are names created on chain; receive needs one for this key | +| World Chain | already | `chains-evm` `eip155:480` | EIP-6963 / EIP-1193 | | +| X Layer | already | `chains-evm` `eip155:196` | EIP-6963 / EIP-1193 | | +| XPR Network | **new** | `chains-antelope` `antelope:384da888112027f0321850a169f737c3` | none: send/receive only | accounts are names created on chain | +| XRP Ledger | **new** | `chains-xrpl` `xrpl:0` | XLS-72d Wallet Standard (no message signing) | | +| ZIGChain | **new** | `chains-cosmos` `cosmos:zigchain-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 118 | +| ZKsync Era | already | `chains-evm` `eip155:324` | EIP-6963 / EIP-1193 | | +| dYdX | **new** | `chains-cosmos` `cosmos:dydx-mainnet-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 118 | +| Linea | already | `chains-evm` `eip155:59144` | EIP-6963 / EIP-1193 | | +| Scroll | already | `chains-evm` `eip155:534352` | EIP-6963 / EIP-1193 | | +| Morph | already | `chains-evm` `eip155:2818` | EIP-6963 / EIP-1193 | | +| ICP | **new** | `chains-icp` `icp:737ba355e855bd4b61279056603e0550` | none: send/receive only (ICRC-94 discovery still a draft) | no public transfer testnet: icp:test = mainnet limited to DFINITY test ledgers; replies not certificate-verified | +| MultiversX | **new** | `chains-multiversx` `mvx:1` | best effort: sdk-dapp's undocumented window.multiversx.providers hook | | +| Initia | **new** | `chains-cosmos` `cosmos:interwoven-1` | Keplr-compatible API at window.clipwallet.cosmos | coin type 60, ethsecp256k1: the EVM key | +| Fuel | **new** | `chains-fuel` `fuel:9889` | FuelConnector event | | +| Tezos | already | `chains-tezos` `tezos:NetXdQprcVkpaWU` | existing provider | | +| Etherlink | already | `chains-evm` `eip155:42793` | EIP-6963 / EIP-1193 | EVM layer of Tezos smart rollups | + +## What was added + +| Family (vault) | Path (account `i`) | Package | Mainnets / testnets | Dapp standard in 1Mask | +| --- | --- | --- | --- | --- | +| `cosmos` | `m/44'/118'/0'/0/i` | `chains-cosmos` | Osmosis, dYdX, ZIGChain / osmo-test-5, dydx-testnet-4, zig-test-2 | Keplr-compatible API (`enable`, `getKey`, `signDirect`, `signAmino`, `signArbitrary`, `verifyArbitrary`, `sendTx`, offline signers) at `window.clipwallet.cosmos`; never `window.keplr` | +| `provenance` | `m/44'/505'/0'/0/i` | `chains-cosmos` | Provenance / pio-testnet-1 | same | +| `thorchain` | `m/44'/931'/0'/0/i` | `chains-cosmos` | THORChain / none (stagenet uses real funds) | same | +| `initia` | `m/44'/60'/0'/0/i` (ethsecp256k1) | `chains-cosmos` | Initia / initiation-2 | same | +| `tron` | `m/44'/195'/0'/0/i` | `chains-tron` | TRON / Nile, Shasta | TIP-1193 provider announced with TIP-6963; TronWeb-shaped signing subset (`trx.sign`, `signMessageV2`) | +| `xrpl` | `m/44'/144'/i'/0/0` | `chains-xrpl` | XRP Ledger / testnet, devnet | XLS-72d (Wallet Standard `xrpl:signTransaction`, `xrpl:signAndSubmitTransaction`); no message signing in the standard | +| `antelope` | `m/44'/194'/0'/0/i` (K1, canonical signatures) | `chains-antelope` | Vaulta, Telos, XPR Network / Jungle4, Telos testnet, XPR testnet | none: send/receive only (WharfKit wallet plugins live in the dapp; Anchor Link/ESR and Scatter injection would mean posing as Anchor or Scatter) | +| `multiversx` | `m/44'/508'/0'/0'/i'` | `chains-multiversx` | MultiversX / devnet, testnet | best effort: an entry on sdk-dapp's `window.multiversx.providers` custom-provider hook under Clip's own name. **Undocumented and subject to change**; it only works on dapps that don't reset `window.multiversx` (MultiversX's own template dapp does). WalletConnect `mvx_*` methods are handled by the module | +| `icp` | `m/44'/223'/0'/0/i` | `chains-icp` | Internet Computer / `icp:test` (mainnet limited to DFINITY test ledgers) | none: ICRC-94 (extension discovery) is still a draft; Plug's `window.ic.plug` is proprietary | +| `stacks` | `m/44'/5757'/0'/0/i` | `chains-stacks` | Stacks / testnet | SIP-030 `request` on `window.clipwallet.stacks`, registered on `window.wbip_providers` (WBIP-004) | +| `fuel` | `m/44'/1179993420'/i'/0/0` | `chains-fuel` | Fuel Ignition / testnet | FuelConnector announced with the `FuelConnector` event, dependency-free | +| `bitcoincash` | `m/44'/145'/0'/0/i` | `chains-bitcoincash` | Bitcoin Cash / chipnet, testnet4 | WalletConnect `bch` namespace (wc2-bch-bcr, as Cashonize/Paytaca); no injected standard | +| `substrate` (existing) | existing | `chains-substrate` | + Chainflip / Perseverance | existing `window.injectedWeb3` (what lp.chainflip.io uses) | +| `evm` (existing) | existing | `chains-evm` | + STRATO, Arc / STRATO Helium | existing EIP-6963 | + +All derivations are in `packages/vault` (the only package that touches keys) and are cross-checked for the public +"abandon … about" phrase against each ecosystem's SDK (`packages/vault/test/families87.test.ts`: cosmjs, xrpl.js, +TronWeb, WharfKit, MultiversX sdk-core, @dfinity/identity-secp256k1, @stacks/wallet-sdk, fuels-ts, libauth), and every +module's paths and addresses are checked against the vault. Chain modules serialize transactions by hand +(protobuf, XRPL binary codec, Antelope ABI, SIP-005, Fuel tx format, CBOR/Candid, CashAddr/BIP-143 with FORKID) and +are cross-checked byte for byte against the official SDKs, which are devDependencies only; each module's runtime +dependencies are `@clip-wallet/core`, `@noble/*` and `@scure/base`. + +## Networks without a self-custodial wallet model + +### Canton + +Canton is a network of participant (validator) nodes that host parties; the CLPR verifier for it is t-of-n operator +attestation and was run only against a local sandbox (clpr-smart-contracts-canton `docs/chains/canton.md`). Canton 3.x +does have "external parties" whose key is held outside the node, and CIP-0103 defines a dApp API for wallets. But +every read of a party's contracts and every submission (prepare → sign hash → execute) goes through the Ledger API of a +participant that hosts the party, behind that operator's authentication, and there is no public endpoint. A wallet can +hold the key, but it can't see or move anything without an operator relationship. **Closest honest support:** none in +Clip as shipped. A kit-built wallet whose operator runs (or contracts) a validator could add a family that talks to +its own participant through a CIP-0103 wallet gateway; that is an operator product, not a phrase-derived wallet. + +### Mixin + +Mixin's kernel is a UTXO DAG with CryptoNote-style one-time output keys. Its public RPC (`kernel.mixin.dev`; +methods in MixinNetwork/mixin `rpc/internal/server/http.go`: getinfo, gettransaction, getutxo, listsnapshots, +sendrawtransaction, …) has no address or key index, so a wallet can't find its own outputs without scanning every +snapshot with the view key, and there is no BIP-39 derivation standard for kernel keys. User funds live in Mixin's +account system (Mixin Messenger / Safe, registered accounts and TIP-derived keys behind api.mixin.one). The CLPR +verifier reads the kernel's collective signatures, which is a different problem from holding funds. +**Closest honest support:** none in Clip. XIN as an ERC-20 on Ethereum is an asset, not the network. + +## Other honest limits + +- **STRATO** works through its Ethereum JSON-RPC: legacy (EIP-155) transactions only, gas unpriced, a flat + 0.01 USDST (or voucher) per transaction that the approval screen shows as its own line. Contract calls are matched to + SolidVM functions by selector; apps that need STRATO's EIP-712 "function call" transactions are out of scope. +- **Arc** mainnet launched on 2026-09-16; native gas is USDC (18 decimals on the EVM side). +- **Antelope** accounts must be created on chain by someone (they cost RAM). Clip finds the accounts that use its key + (`get_accounts_by_authorizers`, Hyperion fallback) and says plainly when there is none yet. +- **ICP** replies aren't certificate-verified (no BLS check); nothing security-relevant relies on them. An ICP + transfer must be approved within 4 minutes (ingress expiry). +- **THORChain** has no public testnet. Swaps (deposit memos) decode as blind. +- **Provenance and Initia testnets:** Keplr derives pio-testnet-1 at coin type 1 and initiation-2 at 118; Clip uses + the mainnet coin types (505, 60) everywhere, so those testnet accounts differ from Keplr's. +- **Chainflip** has no FLIP transfer between State Chain accounts; LP order calls are shown as the raw call with a + caution. +- **Public test accounts:** the "abandon … about" accounts are used by strangers on several testnets (XRPL account 0 + has its master key disabled; the TRON Nile account's owner permission was moved). Unit tests use fixtures; the dapp + matrix uses its own wallet. + +## Live checks done while building (testnets, public test vector account unless said) + +- Fuel testnet: one 1-unit self-transfer through fuels-ts `Fuel` → Clip's connector → the module (0x5717…9db6), paid + from a balance the public test account already held. +- XRPL testnet: a payment and an autofilled AccountSet validated `tesSUCCESS` (account index 7, faucet-funded). +- Jungle4: an Antelope transfer accepted up to CPU billing (`tx_cpu_usage_exceeded`), proving serialization and + signature. +- Cosmos (osmo-test-5, dydx-testnet-4, zig-test-2), MultiversX devnet, ICP, Chainflip (both networks): transactions + accepted by simulate/validation endpoints without broadcasting (ICP: "insufficient funds" from the ledger). diff --git a/llms.txt b/llms.txt index 7d636fe2..f4b6aa75 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # Clip Wallet -> Non-custodial multi-chain wallet for every CLPR network (14 families), and a kit for building your own on every platform: @clip-wallet/* packages, @clip-wallet/extension-kit, desktop-kit and mobile-kit, create-clip-wallet and a Scaffold-HBAR template. Pre-release: test networks only. pnpm monorepo, TypeScript, Apache-2.0. +> Non-custodial multi-chain wallet for the CLPR networks (85 of 87, 26 families), and a kit for building your own on every platform: @clip-wallet/* packages, @clip-wallet/extension-kit, desktop-kit and mobile-kit, create-clip-wallet and a Scaffold-HBAR template. Pre-release: test networks only. pnpm monorepo, TypeScript, Apache-2.0. Read first: AGENTS.md (rules + recipes), .harness/spec.md (product), .harness/prd.md (user stories), packages/core/src/index.ts (the contract), packages/config/src/index.ts (clip.config.ts schema). Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Markdown; every code sample is a typechecked file in apps/docs/src/snippets). Index of the pages agents need most: "## Docs" below. @@ -35,7 +35,7 @@ Developer docs: https://coldai.org/clip/docs/ (sources: apps/docs/src, plain Mar ## Packages (all published from packages/*, one version, npm provenance) - core: shared types (the contract). Additive changes only. - config: clip.config.ts schema (zod): name, description, rdns, homepage, icon, extension.key, theme, networks, route {mode, filters, settleOnHedera}, compatibilityMode, hardware, walletConnect, passkeys, services {backupUrl, mediaProxyUrl, linkRelayUrl, clipHandles}, mainnet. defineConfig, validateConfig, mainnetProblems, walletKey, MAINNET_ACKNOWLEDGEMENT. -- vault: phrase, derivation (14 families), encrypted storage, approval-bound signing, passkey unlock, passkey backup blob. KATs in packages/vault/test. +- vault: phrase, derivation (26 families), encrypted storage, approval-bound signing, passkey unlock, passkey backup blob. KATs in packages/vault/test. - chains-evm, -hedera, -solana, -bitcoin, -sui, -aptos, -cardano, -substrate, -starknet, -ton, -near, -stellar, -tezos, -algorand: ChainModule per family. - 1mask: connectors (EIP-1193/6963, Wallet Standard, AIP-62, CIP-30, injectedWeb3, get-starknet, TON Connect, NEAR/Stellar/Algorand globals, Beacon relay, WalletConnect v2) announcing the wallet's identity. - engine: approvals, permissions, portfolio, catalog (engine/catalog), wiring shared by extension and mobile. diff --git a/packages/1mask/src/background/bitcoincash.ts b/packages/1mask/src/background/bitcoincash.ts new file mode 100644 index 00000000..dab098b4 --- /dev/null +++ b/packages/1mask/src/background/bitcoincash.ts @@ -0,0 +1,46 @@ +import type { Family, Network } from "@clip-wallet/core"; +import { rpcError } from "../shared/errors.js"; +import { BCH_SIGNING_METHODS, BCH_WC, bchAddressOn, bchNetworkForWcChain } from "../shared/bitcoincash.js"; +import type { P2RouterInternals } from "./p2-families.js"; + +/** + * Background side of Bitcoin Cash over WalletConnect (wc2-bch-bcr). There is no injected BCH standard to implement + * (no discovery mechanism wallets share; Paytaca's extension injects its own `window.paytaca`, which another wallet + * mustn't imitate), so Clip appears to BCH dapps through WalletConnect under its own metadata. + * + * The WalletConnect session layer (walletconnect/namespaces.ts, wallet.ts) maps the "bch" namespace and its + * "bch:bitcoincash" / "bch:bchtest" chains onto the registry's bip122 ids with `bchNetworkForWcChain` and answers + * bch_getAddresses from the session itself. This dispatcher (for a future injected path) answers bch_getAddresses with the account's CashAddr spelled for the session's network and + * hands signing methods to chains-bitcoincash unchanged. + */ +export const BCH_METHODS_ALLOWED = { local: [BCH_WC.getAddresses], signing: BCH_SIGNING_METHODS } as const; + +const FAMILY: Family = "bitcoincash"; + +export function createBitcoinCashDispatcher(r: Pick) { + async function addresses(origin: string, net: Network): Promise { + const out: string[] = []; + for (const a of await r.accounts(origin, FAMILY)) { + const s = bchAddressOn(a.address, net.id); + if (s) out.push(s); + } + return out; + } + + /** `chain` may be a wc2-bch-bcr chain ("bch:bchtest") or a CAIP-2 id from the registry. */ + async function dispatch(origin: string, family: Family, method: string, params: unknown, chain: string | undefined): Promise { + if (family !== FAMILY) throw rpcError.unsupportedMethod(method); + const net = r.requireNetwork(FAMILY, origin, bchNetworkForWcChain(chain) ?? chain); + if (method === BCH_WC.getAddresses) { + await r.requirePermission(origin, FAMILY); + return addresses(origin, net); + } + if (BCH_SIGNING_METHODS.includes(method)) { + await r.requirePermission(origin, FAMILY); + return r.approve(r.makeReq(origin, FAMILY, net, method, params ?? {})); + } + throw rpcError.unsupportedMethod(method); + } + + return { dispatch }; +} diff --git a/packages/1mask/src/background/cosmos.ts b/packages/1mask/src/background/cosmos.ts new file mode 100644 index 00000000..0dbc1249 --- /dev/null +++ b/packages/1mask/src/background/cosmos.ts @@ -0,0 +1,160 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { rpcError } from "../shared/errors.js"; +import { COSMOS_FAMILIES, COSMOS_INJECTED, type CosmosKeyWire, cosmosChainOf } from "../shared/cosmos.js"; +import { bech32Decode, bech32Encode, bytesOfHex, hexOf } from "../shared/cosmos-bech32.js"; +import type { ExposedAccount } from "../shared/protocol.js"; + +/** + * Background side of the Keplr-compatible provider (inpage/cosmos.ts) for the four Cosmos key families: "cosmos" + * (coin 118: Osmosis, dYdX, ZIGChain), "provenance" (505), "thorchain" (931), "initia" (60, ethsecp256k1). + * The router keeps the policy (rate limits, approvals, timeouts); this file knows the method list, which account a + * request names, and how an account is spelled on each chain. Wiring (router.ts / methods.ts, by the coordinator): + * + * const cosmos = createCosmosDispatcher({ permitted, requirePermission, accounts, connect, approve, makeReq, + * requireNetwork, revoke, read: (req) => withTimeout(opts.handle(req), readMs, req.id) }); + * if (COSMOS_DISPATCH_FAMILIES.has(family)) return cosmos.dispatch(origin, family, method, params, chain); + * injectedAllowlist(family) ∪= cosmosInjectedAllowlist(family) + * + * and the engine/service answers `cosmos_sendTx` / `cosmos_verifyArbitrary` through chains-cosmos `read()` without + * an approval, like Cardano's read methods. + */ + +export const COSMOS_DISPATCH_FAMILIES: ReadonlySet = new Set(COSMOS_FAMILIES); + +export const COSMOS_METHODS_ALLOWED = { + /** Silent account read and disconnect. */ + local: [COSMOS_INJECTED.accounts, COSMOS_INJECTED.disable], + connect: [COSMOS_INJECTED.enable], + /** Permission needed, no approval. */ + permitted: [COSMOS_INJECTED.getKey], + /** Permission needed, answered by the chain module's read() (no approval). */ + readOnly: [COSMOS_INJECTED.sendTx, COSMOS_INJECTED.verifyArbitrary], + signing: [COSMOS_INJECTED.signDirect, COSMOS_INJECTED.signAmino, COSMOS_INJECTED.signArbitrary], +} as const; + +export function cosmosInjectedAllowlist(family: Family): ReadonlySet { + return COSMOS_DISPATCH_FAMILIES.has(family) ? new Set(Object.values(COSMOS_METHODS_ALLOWED).flat()) : new Set(); +} + +export interface CosmosRouterInternals { + permitted(origin: string, family: Family): Promise; + requirePermission(origin: string, family: Family): Promise; + accounts(origin: string, family: Family): Promise; + connect(origin: string, family: Family, net: Network, method: string, params: unknown): Promise; + approve(req: DappRequest): Promise; + makeReq(origin: string, family: Family, net: Network, method: string, params: unknown): DappRequest; + requireNetwork(family: Family, origin: string, chain: string | undefined): Network; + revoke(origin: string, family: Family): Promise; + /** opts.handle with the read timeout and no approval. Absent: sendTx / verifyArbitrary answer 4200. */ + read?(req: DappRequest): Promise; +} + +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); + +/** The account's 20 address bytes, from its address (any bech32 spelling) or, failing that, nothing. */ +function addressBytesOf(a: ExposedAccount): Uint8Array | null { + const d = bech32Decode(a.address); + return d && d.data.length === 20 ? d.data : null; +} + +/** Keplr Key for `chainId`, as JSON (bytes hex; the page turns them into Uint8Arrays). */ +export function cosmosKeyFor(account: ExposedAccount, chainId: string): CosmosKeyWire { + const chain = cosmosChainOf(chainId); + const bytes = addressBytesOf(account); + if (!chain || !bytes || !account.publicKey) throw rpcError.internal("This account can't be used on that chain."); + return { + algo: chain.algo, + pubKey: account.publicKey, + address: hexOf(bytes), + bech32Address: bech32Encode(chain.prefix, bytes), + // Lower-case hex (a valid EIP-55 spelling). Keplr checksums it; that needs keccak, which the page bundle doesn't carry. + ethereumHexAddress: `0x${hexOf(bytes)}`, + }; +} + +export function createCosmosDispatcher(r: CosmosRouterInternals) { + /** The connected account (first) for the family, or 4100. */ + const mine = async (origin: string, family: Family): Promise => { + const list = await r.accounts(origin, family); + const a = list[0]; + if (!a) throw rpcError.unauthorized(); + return a; + }; + + /** The signer the page named must be one of this site's accounts, spelled with this chain's prefix. */ + const requireOwnSigner = async (origin: string, family: Family, chainId: string, signer: unknown) => { + const chain = cosmosChainOf(chainId); + const d = typeof signer === "string" ? bech32Decode(signer) : null; + if (!chain || !d || d.prefix !== chain.prefix) throw rpcError.unauthorized("Signer mismatched"); + const list = await r.accounts(origin, family); + if (!list.some((a) => { + const b = addressBytesOf(a); + return !!b && hexOf(b) === hexOf(d.data); + })) { + throw rpcError.unauthorized("Signer mismatched"); + } + }; + + async function dispatch(origin: string, family: Family, method: string, params: unknown, chain: string | undefined): Promise { + if (!COSMOS_DISPATCH_FAMILIES.has(family)) throw rpcError.unsupportedMethod(method); + + if (method === COSMOS_INJECTED.accounts) return (await r.permitted(origin, family)) ? r.accounts(origin, family) : []; + if (method === COSMOS_INJECTED.disable) { + await r.revoke(origin, family); + return null; + } + + const net = r.requireNetwork(family, origin, chain); + if (net.family !== family || !net.id.startsWith("cosmos:")) throw rpcError.chainDisconnected(); + const chainId = net.id.slice("cosmos:".length); + if (!cosmosChainOf(chainId)) throw rpcError.chainDisconnected(); + + if (method === COSMOS_INJECTED.enable) { + if (await r.permitted(origin, family)) return r.accounts(origin, family); + const ids = obj(params).chainIds; + return r.connect(origin, family, net, method, { chainIds: Array.isArray(ids) ? ids.filter((x) => typeof x === "string").slice(0, 32) : [chainId] }); + } + + if (method === COSMOS_INJECTED.getKey) { + await r.requirePermission(origin, family); + return cosmosKeyFor(await mine(origin, family), chainId); + } + + if ((COSMOS_METHODS_ALLOWED.readOnly as readonly string[]).includes(method)) { + await r.requirePermission(origin, family); + if (!r.read) throw rpcError.unsupportedMethod(method); + if (method === COSMOS_INJECTED.sendTx) { + const p = obj(params); + if (typeof p.tx !== "string" || !p.tx) throw rpcError.invalidParams("Expected { tx: base64, mode }."); + if (p.mode !== "sync" && p.mode !== "async" && p.mode !== "block") throw rpcError.invalidParams("mode must be sync, async or block."); + } + return r.read(r.makeReq(origin, family, net, method, params)); + } + + if ((COSMOS_METHODS_ALLOWED.signing as readonly string[]).includes(method)) { + await r.requirePermission(origin, family); + const p = obj(params); + await requireOwnSigner(origin, family, chainId, method === COSMOS_INJECTED.signArbitrary ? p.signer : p.signerAddress); + if (method === COSMOS_INJECTED.signDirect) { + const doc = obj(p.signDoc); + if (typeof doc.bodyBytes !== "string" || typeof doc.authInfoBytes !== "string" || typeof doc.chainId !== "string") { + throw rpcError.invalidParams("Expected { signerAddress, signDoc: { bodyBytes, authInfoBytes, chainId, accountNumber } }."); + } + if (doc.chainId !== chainId) throw rpcError.invalidParams("Unmatched chain id with the sign doc"); + } + if (method === COSMOS_INJECTED.signAmino) { + const doc = obj(p.signDoc); + // An ADR-36 doc (chain_id "") is a message signature, allowed on any chain; otherwise the doc names this chain. + if (typeof doc.chain_id !== "string" || (doc.chain_id !== "" && doc.chain_id !== chainId)) throw rpcError.invalidParams("Unmatched chain id with the sign doc"); + } + if (method === COSMOS_INJECTED.signArbitrary && (typeof p.data !== "string" || !p.data)) throw rpcError.invalidParams("Expected { signer, data: base64 }."); + return r.approve(r.makeReq(origin, family, net, method, params)); + } + + throw rpcError.unsupportedMethod(method); + } + + return { dispatch }; +} + +export { bytesOfHex }; diff --git a/packages/1mask/src/background/fuel.ts b/packages/1mask/src/background/fuel.ts new file mode 100644 index 00000000..53bc375e --- /dev/null +++ b/packages/1mask/src/background/fuel.ts @@ -0,0 +1,108 @@ +import type { DappRequest, Family, Network, NetworkId } from "@clip-wallet/core"; +import { rpcError } from "../shared/errors.js"; +import { FUEL_INJECTED, fuelChainId, fuelConnectorNetwork } from "../shared/fuel.js"; +import type { ExposedAccount, OneMaskEvent } from "../shared/protocol.js"; + +/** + * Background side of the Fuel connector (inpage/fuel.ts). The router keeps the policy (permissions, approvals, rate + * limits, per-site networks) and lends its closures as `FuelRouterInternals` (the same shape as StarknetTonHelpers); + * this file knows the connector's method list, how its params name the account, and Fuel networks + * ({ url, chainId }, fuels-ts connectors/types `Network`). Wire it into router.ts for `family === "fuel"`, and + * `fuelInjectedAllowlist()` into methods.ts. + */ + +export const FUEL_METHODS_ALLOWED = { + /** Answered here, no prompt. */ + local: [FUEL_INJECTED.accounts, FUEL_INJECTED.disconnect, FUEL_INJECTED.currentNetwork, FUEL_INJECTED.networks, FUEL_INJECTED.selectNetwork], + connect: [FUEL_INJECTED.connect], + signing: [FUEL_INJECTED.sendTransaction, FUEL_INJECTED.signTransaction, FUEL_INJECTED.signMessage], +} as const; + +export function fuelInjectedAllowlist(): ReadonlySet { + return new Set([...FUEL_METHODS_ALLOWED.local, ...FUEL_METHODS_ALLOWED.connect, ...FUEL_METHODS_ALLOWED.signing]); +} + +export interface FuelRouterInternals { + permitted(origin: string, family: Family): Promise; + accounts(origin: string, family: Family): Promise; + /** Connect approval + permission grant; resolves with the accounts the site now sees. */ + connect(origin: string, family: Family, net: Network, method: string, params: unknown): Promise; + approve(req: DappRequest): Promise; + makeReq(origin: string, family: Family, net: Network, method: string, params: unknown): DappRequest; + selectedNetwork(origin: string, family: Family): Network | undefined; + setSelected(origin: string, family: Family, id: NetworkId): void; + candidates(family: Family): Network[]; + emit(origin: string, family: Family, event: OneMaskEvent, data?: unknown): void; + revoke(origin: string, family: Family, o?: { silent?: boolean }): Promise; +} + +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); + +const sameUrl = (a: string, b: string) => { + try { + const x = new URL(a); + const y = new URL(b); + return x.host === y.host && x.pathname.replace(/\/+$/, "") === y.pathname.replace(/\/+$/, ""); + } catch { + return false; + } +}; + +/** Fuel addresses are 0x-hex in any case (checksummed or not): compare lower-case. */ +const sameAddress = (a: string, b: string) => a.toLowerCase() === b.toLowerCase(); + +export function createFuelDispatcher(h: FuelRouterInternals) { + const FAMILY: Family = "fuel"; + + const selected = (origin: string): Network => { + const net = h.selectedNetwork(origin, FAMILY); + if (!net) throw rpcError.chainDisconnected("No Fuel network is available."); + return net; + }; + + async function dispatch(origin: string, method: string, params: unknown): Promise { + switch (method) { + case FUEL_INJECTED.accounts: + return (await h.permitted(origin, FAMILY)) ? h.accounts(origin, FAMILY) : []; + case FUEL_INJECTED.connect: + if (await h.permitted(origin, FAMILY)) return h.accounts(origin, FAMILY); + return h.connect(origin, FAMILY, selected(origin), method, {}); + case FUEL_INJECTED.disconnect: + await h.revoke(origin, FAMILY); + return null; + case FUEL_INJECTED.currentNetwork: + // FuelConnector.currentNetwork: answered "even if the connection is not established". + return fuelConnectorNetwork(selected(origin)); + case FUEL_INJECTED.networks: + return h.candidates(FAMILY).map(fuelConnectorNetwork); + case FUEL_INJECTED.selectNetwork: { + const p = obj(params); + const net = h.candidates(FAMILY).find( + (n) => (typeof p.chainId === "number" && fuelChainId(n.id) === p.chainId) || (typeof p.url === "string" && n.rpcUrls.some((u) => sameUrl(u, p.url as string))), + ); + // Never add networks: only the ones that ship with the wallet. + if (!net) throw rpcError.unrecognizedChain(String(p.chainId ?? p.url ?? "")); + const before = h.selectedNetwork(origin, FAMILY); + h.setSelected(origin, FAMILY, net.id); + if (before?.id !== net.id) h.emit(origin, FAMILY, "chainChanged", fuelConnectorNetwork(net)); + return true; + } + } + + if ((FUEL_METHODS_ALLOWED.signing as readonly string[]).includes(method)) { + if (!(await h.permitted(origin, FAMILY))) throw rpcError.unauthorized(); + const p = obj(params); + if (typeof p.address !== "string") throw rpcError.invalidParams("Expected { address, … }."); + const list = await h.accounts(origin, FAMILY); + if (!list.some((a) => sameAddress(a.address, p.address as string))) throw rpcError.unauthorized("That account is not connected to this site."); + if (method === FUEL_INJECTED.signMessage ? p.message === undefined : p.transaction === undefined) { + throw rpcError.invalidParams(method === FUEL_INJECTED.signMessage ? "Expected { address, message }." : "Expected { address, transaction }."); + } + return h.approve(h.makeReq(origin, FAMILY, selected(origin), method, params)); + } + + throw rpcError.unsupportedMethod(method); + } + + return { dispatch }; +} diff --git a/packages/1mask/src/background/index.ts b/packages/1mask/src/background/index.ts index 8d824509..30d8fa58 100644 --- a/packages/1mask/src/background/index.ts +++ b/packages/1mask/src/background/index.ts @@ -39,3 +39,17 @@ export { } from "./starknet-ton.js"; export { createCallsDispatch, type CallsRouterHelpers } from "./eip5792.js"; export * from "../shared/calls.js"; +/* networks87: dispatchers live in router.ts; hosts need the connect and read-only method names. */ +export { COSMOS_CONNECT_METHODS, COSMOS_INJECTED, COSMOS_FAMILIES } from "../shared/cosmos.js"; +export { TRON_CONNECT_METHODS } from "./tron.js"; +export { STACKS_INJECTED } from "../shared/stacks.js"; +export { FUEL_CONNECT_METHODS } from "../shared/fuel.js"; +import { COSMOS_CONNECT_METHODS as _COSMOS_CONNECT, COSMOS_INJECTED as _COSMOS } from "../shared/cosmos.js"; +import { TRON_CONNECT_METHODS as _TRON_CONNECT } from "./tron.js"; +import { STACKS_INJECTED as _STACKS } from "../shared/stacks.js"; +import { FUEL_CONNECT_METHODS as _FUEL_CONNECT } from "../shared/fuel.js"; +import { MULTIVERSX_CONNECT_METHODS as _MVX_CONNECT } from "./multiversx.js"; +/** Methods the host treats as a connect approval (Keplr enable, TIP-1193 accounts, SIP-030 addresses, FuelConnector connect, MultiversX provider login). */ +export const N87_CONNECT_METHODS: readonly string[] = [..._COSMOS_CONNECT, ..._TRON_CONNECT, _STACKS.connect, ..._FUEL_CONNECT, ..._MVX_CONNECT]; +/** Read-only chain calls answered by a chain module without an approval (Keplr sendTx of an already-signed tx, verifyArbitrary). */ +export const N87_CHAIN_READ: readonly string[] = [_COSMOS.sendTx, _COSMOS.verifyArbitrary]; diff --git a/packages/1mask/src/background/methods.ts b/packages/1mask/src/background/methods.ts index 301f9b5d..e903e263 100644 --- a/packages/1mask/src/background/methods.ts +++ b/packages/1mask/src/background/methods.ts @@ -2,6 +2,12 @@ import type { Family } from "@clip-wallet/core"; import { METHOD_PROVIDER_STATE, METHOD_WS_STATE } from "../shared/protocol.js"; import { APTOS_CONNECT_METHODS, APTOS_LOCAL_METHODS, APTOS_SIGNING_METHODS, SUI_SIGNING_METHODS } from "../shared/move-methods.js"; import { p2InjectedAllowlist } from "./p2-families.js"; +import { cosmosInjectedAllowlist } from "./cosmos.js"; +import { tronInjectedAllowlist } from "./tron.js"; +import { stacksInjectedAllowlist } from "./stacks.js"; +import { fuelInjectedAllowlist } from "./fuel.js"; +import { xrplAllowlist } from "../shared/xrpl.js"; +import { multiversxInjectedAllowlist } from "./multiversx.js"; import { cardanoSubstrateAllowlist } from "./cardano-substrate.js"; import { starknetTonAllowlist } from "./starknet-ton.js"; import { HEDERA_INJECTED_METHODS } from "../shared/hedera.js"; @@ -126,8 +132,23 @@ export function injectedAllowlist(family: Family): ReadonlySet { case "starknet": case "ton": return starknetTonAllowlist(family); + case "cosmos": + case "provenance": + case "thorchain": + case "initia": + return cosmosInjectedAllowlist(family); + case "tron": + return tronInjectedAllowlist(); + case "stacks": + return stacksInjectedAllowlist(); + case "fuel": + return fuelInjectedAllowlist(); + case "xrpl": + return xrplAllowlist(); + case "multiversx": + return multiversxInjectedAllowlist(); default: - // Phase 2 families register their injected methods here as their connectors land. + // Families without an injected provider (antelope, icp; bitcoincash over WalletConnect): send and receive only. return new Set(); } } diff --git a/packages/1mask/src/background/multiversx.ts b/packages/1mask/src/background/multiversx.ts new file mode 100644 index 00000000..bb7e61e6 --- /dev/null +++ b/packages/1mask/src/background/multiversx.ts @@ -0,0 +1,85 @@ +import { rpcError } from "../shared/errors.js"; +import { MULTIVERSX_INJECTED, nativeAuthOrigin } from "../shared/multiversx-methods.js"; +import type { P2RouterInternals } from "./p2-families.js"; + +/** + * Background side of the MultiversX provider (inpage/multiversx.ts). Same shape as createTronDispatcher: the router + * keeps the policy (permissions, approvals, rate limits, timeouts) and lends its closures; this file only knows the + * method list and which account a request names. Signing goes to chains-multiversx unchanged (mvx_signTransactions / + * mvx_signMessage), which checks the sender and chain again and decodes in plain words. + * + * Wiring (router.ts / methods.ts, by the coordinator): + * const multiversx = createMultiversXDispatcher({ permitted, requirePermission, accounts, connect, approve, makeReq, requireNetwork, revoke }); + * if (family === "multiversx") return multiversx.dispatch(origin, method, params, chain); + * injectedAllowlist("multiversx") = multiversxInjectedAllowlist() + */ + +export const MULTIVERSX_METHODS_ALLOWED = { + local: [MULTIVERSX_INJECTED.accounts, MULTIVERSX_INJECTED.disconnect], + connect: [MULTIVERSX_INJECTED.connect], + signing: [MULTIVERSX_INJECTED.signTransactions, MULTIVERSX_INJECTED.signMessage], +} as const; + +export const MULTIVERSX_CONNECT_METHODS = [MULTIVERSX_INJECTED.connect] as const; + +/** For methods.ts `injectedAllowlist`. */ +export function multiversxInjectedAllowlist(): ReadonlySet { + const a = MULTIVERSX_METHODS_ALLOWED; + return new Set([...a.local, ...a.connect, ...a.signing]); +} + +const MAX_TXS = 20; +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); + +function originOf(url: string): string | null { + try { + return new URL(url).origin; + } catch { + return null; + } +} + +export function createMultiversXDispatcher(r: P2RouterInternals) { + async function dispatch(origin: string, method: string, params: unknown, chain: string | undefined): Promise { + // Silent account read (no prompt). + if (method === MULTIVERSX_INJECTED.accounts) return (await r.permitted(origin, "multiversx")) ? r.accounts(origin, "multiversx") : []; + + if (method === MULTIVERSX_INJECTED.disconnect) { + await r.revoke(origin, "multiversx"); + return null; + } + + if (method === MULTIVERSX_INJECTED.connect) { + if (await r.permitted(origin, "multiversx")) return r.accounts(origin, "multiversx"); + return r.connect(origin, "multiversx", r.requireNetwork("multiversx", origin, chain), method, {}); + } + + if ((MULTIVERSX_METHODS_ALLOWED.signing as readonly string[]).includes(method)) { + await r.requirePermission(origin, "multiversx"); + const p = obj(params); + const mine = new Set((await r.accounts(origin, "multiversx")).map((a) => a.address)); + const own = (address: unknown) => { + if (typeof address !== "string" || !mine.has(address)) throw rpcError.unauthorized("That account is not connected to this site."); + }; + if (p.address !== undefined) own(p.address); + if (method === MULTIVERSX_INJECTED.signTransactions) { + const txs = p.transactions; + if (!Array.isArray(txs) || txs.length === 0 || txs.length > MAX_TXS) throw rpcError.invalidParams(`Expected { transactions: 1 to ${MAX_TXS} plain transactions }.`); + for (const t of txs) own(obj(t).sender); + } else { + if (typeof p.message !== "string" || typeof p.address !== "string") throw rpcError.invalidParams("Expected { message, address }."); + // A native-auth login token names the site it signs in to. Another site's token is refused outright: signing + // it would hand this site a login to that one. + const token = p.message.startsWith(p.address) ? p.message.slice(p.address.length) : ""; + const site = token ? nativeAuthOrigin(token) : null; + if (site !== null && originOf(site) !== originOf(origin)) throw rpcError.unauthorized("This sign-in is for a different site."); + } + const net = r.requireNetwork("multiversx", origin, chain); + return r.approve(r.makeReq(origin, "multiversx", net, method, params)); + } + + throw rpcError.unsupportedMethod(method); + } + + return { dispatch }; +} diff --git a/packages/1mask/src/background/router.ts b/packages/1mask/src/background/router.ts index c72ed424..dbfb1b77 100644 --- a/packages/1mask/src/background/router.ts +++ b/packages/1mask/src/background/router.ts @@ -27,6 +27,14 @@ import { HEDERA_WC_PAIR, isWalletConnectPairingUri } from "../shared/hedera.js"; import type { PermissionStore } from "./permissions.js"; import { createStarknetTonDispatch, type StarknetTonOptions } from "./starknet-ton.js"; import { createCallsDispatch } from "./eip5792.js"; +import { COSMOS_DISPATCH_FAMILIES, createCosmosDispatcher } from "./cosmos.js"; +import { createTronDispatcher } from "./tron.js"; +import { createStacksDispatcher } from "./stacks.js"; +import { stacksAddressOn } from "../shared/stacks.js"; +import { bchAddressOn } from "../shared/bitcoincash.js"; +import { createFuelDispatcher } from "./fuel.js"; +import { createXrplDispatcher } from "./xrpl.js"; +import { createMultiversXDispatcher } from "./multiversx.js"; import { isCallsMethod, type CallsHost } from "../shared/calls.js"; /** Background side of a runtime port (chrome.runtime.Port satisfies it). */ @@ -299,10 +307,31 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { pendingConnect.delete(key); } const list = await accounts(origin, family); - emit(origin, family, "accountsChanged", family === "evm" ? list.map((a) => a.address) : list); + // Stacks / Bitcoin Cash spell the account per network: the network the site connected on is the one its later + // accountsChanged events are spelled for (they have no switch-network method of their own). + if (family === "stacks" || family === "bitcoincash") setSelected(origin, family, net.id); + emit(origin, family, "accountsChanged", await eventAccounts(origin, family, list)); return list; }; + /** + * What an accountsChanged event carries: EVM addresses; Stacks and Bitcoin Cash accounts spelled for the site's + * selected network (Account.address is the vault's mainnet "SP…" / "bitcoincash:" form), the way their dispatchers + * answer `accounts`; the account list otherwise. + */ + const eventAccounts = async (origin: string, family: Family, list: ExposedAccount[]): Promise => { + if (family === "evm") return list.map((a) => a.address); + if (family !== "stacks" && family !== "bitcoincash") return list; + const net = selectedNetwork(origin, family); + if (!net) return []; + const out: ExposedAccount[] = []; + for (const a of list) { + const address = family === "stacks" ? await stacksAddressOn(a.address, net.id) : bchAddressOn(a.address, net.id); + if (address) out.push({ ...a, address }); + } + return out; + }; + const emit = (origin: string, family: Family, event: OneMaskEvent, data?: unknown) => { const msg: PortEvent = { type: "event", family, event, data }; for (const p of ports.get(origin) ?? []) { @@ -510,7 +539,7 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { const res = await approve(makeReq(origin, family, net, method, { inputs })); if (!(await permitted(origin, family))) { await opts.permissions.grant(origin, family); - emit(origin, family, "accountsChanged", await accounts(origin, family)); + emit(origin, family, "accountsChanged", await eventAccounts(origin, family, await accounts(origin, family))); } return res; } @@ -584,6 +613,36 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { opts, ); + /* ------------------------------------------------------------ networks87: Cosmos SDK, TRON, Stacks, Fuel */ + + const n87Internals = { + permitted, + requirePermission, + accounts, + connect, + approve, + makeReq, + requireNetwork, + revoke: (origin: string, family: Family) => revoke(origin, family), + }; + const cosmos = createCosmosDispatcher({ ...n87Internals, read: (req) => withTimeout(opts.handle(req), readMs, req.id) }); + const tron = createTronDispatcher(n87Internals); + const xrpl = createXrplDispatcher(n87Internals); + const multiversx = createMultiversXDispatcher(n87Internals); + const stacks = createStacksDispatcher({ ...n87Internals, networks: () => candidates("stacks") }); + const fuel = createFuelDispatcher({ + permitted, + accounts, + connect, + approve, + makeReq, + selectedNetwork, + setSelected, + candidates, + emit, + revoke: (origin, family, o) => revoke(origin, family, o), + }); + /* ------------------------------------------------------------ EIP-5792 (opt-in) */ const callsDispatch = opts.calls ? createCallsDispatch({ permitted, accounts, approve, makeReq, candidates }, opts.calls) : undefined; @@ -614,6 +673,12 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { if (family === "cardano" || family === "substrate") return dispatchCardanoSubstrate(cardanoSubstrateHelpers, origin, family, method, params, chain); if (family === "starknet") return starknetTon.starknet(origin, method, params); if (family === "ton") return starknetTon.ton(origin, method, params); + if (COSMOS_DISPATCH_FAMILIES.has(family)) return cosmos.dispatch(origin, family, method, params, chain); + if (family === "tron") return tron.dispatch(origin, method, params, chain); + if (family === "stacks") return stacks.dispatch(origin, family, method, params, chain); + if (family === "fuel") return fuel.dispatch(origin, method, params); + if (family === "xrpl") return xrpl.dispatch(origin, method, params, chain); + if (family === "multiversx") return multiversx.dispatch(origin, method, params, chain); if (family === "hedera" && method === HEDERA_WC_PAIR) { const uri = (params as { uri?: unknown } | undefined)?.uri; if (!opts.walletConnectPair) throw rpcError.unsupportedMethod(method); @@ -680,8 +745,7 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { for (const origin of ports.keys()) { for (const f of family ? [family] : FAMILIES) { if (!(await permitted(origin, f))) continue; - const list = await accounts(origin, f); - emit(origin, f, "accountsChanged", f === "evm" ? list.map((a) => a.address) : list); + emit(origin, f, "accountsChanged", await eventAccounts(origin, f, await accounts(origin, f))); } } }; diff --git a/packages/1mask/src/background/stacks.ts b/packages/1mask/src/background/stacks.ts new file mode 100644 index 00000000..b677d920 --- /dev/null +++ b/packages/1mask/src/background/stacks.ts @@ -0,0 +1,92 @@ +import type { Family, Network } from "@clip-wallet/core"; +import { rpcError } from "../shared/errors.js"; +import type { ExposedAccount } from "../shared/protocol.js"; +import { STACKS_CHAINS, STACKS_INJECTED, STACKS_SIGNING_METHODS, stacksAddressOn, stacksNetworkName } from "../shared/stacks.js"; +import type { P2RouterInternals } from "./p2-families.js"; + +/** + * Background side of the injected Stacks provider (inpage/stacks.ts, SIP-030 `request`). The router keeps the + * policy (permissions, approvals, rate limits, timeouts); this file knows Stacks' method list and that an + * account's address is spelled per network (Account.address is the mainnet "SP…" form; the testnet is "ST…"). + * Wired into router.ts by the integration step, like createP2Dispatcher. + */ + +export const STACKS_METHODS_ALLOWED = { + local: [STACKS_INJECTED.accounts, STACKS_INJECTED.disconnect, STACKS_INJECTED.getNetworks], + connect: [STACKS_INJECTED.connect], + signing: STACKS_SIGNING_METHODS, +} as const; + +/** For methods.ts `injectedAllowlist`. */ +export function stacksInjectedAllowlist(): ReadonlySet { + return new Set([...STACKS_METHODS_ALLOWED.local, ...STACKS_METHODS_ALLOWED.connect, ...STACKS_METHODS_ALLOWED.signing]); +} + +export interface StacksRouterInternals extends Pick { + /** The wallet's Stacks networks (for stx_getNetworks). Default: only the one the site is on. */ + networks?(): Network[]; +} + +const FAMILY: Family = "stacks"; +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); + +export function createStacksDispatcher(r: StacksRouterInternals) { + /** The site's accounts, spelled for `net`. */ + async function spelled(origin: string, net: Network, list?: ExposedAccount[]): Promise { + const accounts = list ?? (await r.accounts(origin, FAMILY)); + const out: ExposedAccount[] = []; + for (const a of accounts) { + const address = await stacksAddressOn(a.address, net.id); + if (address) out.push({ ...a, address }); + } + return out; + } + + async function dispatch(origin: string, family: Family, method: string, params: unknown, chain: string | undefined): Promise { + if (family !== FAMILY) throw rpcError.unsupportedMethod(method); + + if (method === STACKS_INJECTED.accounts) { + if (!(await r.permitted(origin, FAMILY))) return []; + return spelled(origin, r.requireNetwork(FAMILY, origin, chain)); + } + + if (method === STACKS_INJECTED.disconnect) { + await r.revoke(origin, FAMILY); + return null; + } + + if (method === STACKS_INJECTED.connect) { + const net = r.requireNetwork(FAMILY, origin, chain); + const list = (await r.permitted(origin, FAMILY)) ? await r.accounts(origin, FAMILY) : await r.connect(origin, FAMILY, net, method, params ?? {}); + return spelled(origin, net, list); + } + + if (method === STACKS_INJECTED.getNetworks) { + await r.requirePermission(origin, FAMILY); + const active = r.requireNetwork(FAMILY, origin, chain); + const nets = (r.networks?.() ?? [active]).filter((n) => n.family === FAMILY && stacksNetworkName(n.id)); + const shape = (n: Network) => { + const name = stacksNetworkName(n.id)!; + return { id: name, chainId: STACKS_CHAINS[name].chainId, transactionVersion: STACKS_CHAINS[name].transactionVersion }; + }; + return { active: stacksNetworkName(active.id), networks: nets.map(shape) }; + } + + if (STACKS_SIGNING_METHODS.includes(method)) { + await r.requirePermission(origin, FAMILY); + const net = r.requireNetwork(FAMILY, origin, chain); + const address = obj(params).address; + if (address !== undefined && address !== null) { + const mine = await spelled(origin, net); + if (typeof address !== "string" || !mine.some((a) => a.address === address.trim().toUpperCase())) { + throw rpcError.unauthorized("That account is not connected to this site."); + } + } + return r.approve(r.makeReq(origin, FAMILY, net, method, params ?? {})); + } + + throw rpcError.unsupportedMethod(method); + } + + return { dispatch }; +} diff --git a/packages/1mask/src/background/tron.ts b/packages/1mask/src/background/tron.ts new file mode 100644 index 00000000..06e80db4 --- /dev/null +++ b/packages/1mask/src/background/tron.ts @@ -0,0 +1,62 @@ +import { rpcError } from "../shared/errors.js"; +import { TRON_INJECTED } from "../shared/tron-methods.js"; +import type { P2RouterInternals } from "./p2-families.js"; + +/** + * Background side of the injected TRON provider (inpage/tron.ts). Same shape as createP2Dispatcher: the router keeps + * the policy (permissions, approvals, rate limits, timeouts) and lends its closures; this file only knows TRON's + * method list and where its params name the signer. eth_chainId / wallet_switchEthereumChain are answered in the page + * (the page passes its chain with every call and requireNetwork checks it against the registry). + */ + +export const TRON_METHODS_ALLOWED = { + local: [TRON_INJECTED.accounts, TRON_INJECTED.disconnect], + connect: [TRON_INJECTED.connect], + signing: [TRON_INJECTED.signTransaction, TRON_INJECTED.signMessage], +} as const; + +export const TRON_CONNECT_METHODS = [TRON_INJECTED.connect] as const; + +/** For methods.ts `injectedAllowlist`. */ +export function tronInjectedAllowlist(): ReadonlySet { + const a = TRON_METHODS_ALLOWED; + return new Set([...a.local, ...a.connect, ...a.signing]); +} + +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); + +export function createTronDispatcher(r: P2RouterInternals) { + async function dispatch(origin: string, method: string, params: unknown, chain: string | undefined): Promise { + // Silent account read (no prompt). + if (method === TRON_INJECTED.accounts) return (await r.permitted(origin, "tron")) ? r.accounts(origin, "tron") : []; + + if (method === TRON_INJECTED.disconnect) { + await r.revoke(origin, "tron"); + return null; + } + + if (method === TRON_INJECTED.connect) { + if (await r.permitted(origin, "tron")) return r.accounts(origin, "tron"); + return r.connect(origin, "tron", r.requireNetwork("tron", origin, chain), method, params ?? {}); + } + + if ((TRON_METHODS_ALLOWED.signing as readonly string[]).includes(method)) { + await r.requirePermission(origin, "tron"); + const p = obj(params); + // The signer is named by `address` (Reown TRON params); chains-tron also checks it against the account. + const address = p.address; + const list = await r.accounts(origin, "tron"); + if (address !== undefined && (typeof address !== "string" || !list.some((a) => a.address === address))) { + throw rpcError.unauthorized("That account is not connected to this site."); + } + if (method === TRON_INJECTED.signTransaction && (!p.transaction || typeof p.transaction !== "object")) throw rpcError.invalidParams("Expected { address, transaction }."); + if (method === TRON_INJECTED.signMessage && typeof p.message !== "string") throw rpcError.invalidParams("Expected { address, message }."); + const net = r.requireNetwork("tron", origin, chain); + return r.approve(r.makeReq(origin, "tron", net, method, params)); + } + + throw rpcError.unsupportedMethod(method); + } + + return { dispatch }; +} diff --git a/packages/1mask/src/background/xrpl.ts b/packages/1mask/src/background/xrpl.ts new file mode 100644 index 00000000..c6a2e152 --- /dev/null +++ b/packages/1mask/src/background/xrpl.ts @@ -0,0 +1,59 @@ +import { rpcError } from "../shared/errors.js"; +import { XRPL_INJECTED, XRPL_METHODS_ALLOWED, xrplAllowlist, xrplChainId } from "../shared/xrpl.js"; +import type { P2RouterInternals } from "./p2-families.js"; + +/** + * Background side of the XLS-72d XRPL wallet (inpage/xrpl.ts). The router keeps the policy (permissions, + * approvals, rate limits, timeouts); this file only knows the method list and where XLS-72d params name the account + * and the network. To wire it (coordinator): `injectedAllowlist("xrpl")` → `xrplAllowlist()`, and in `dispatchRaw` + * `if (family === "xrpl") return xrpl.dispatch(origin, method, params, chain)` with + * `const xrpl = createXrplDispatcher({ permitted, requirePermission, accounts, connect, approve, makeReq, requireNetwork, revoke })`. + * + * DappRequest params for both signing methods: `{ tx_json, account: "r…", network: "xrpl:", options? }` with + * `networkId` = that network (chains-xrpl normalize() checks every field again before anything is signed). + */ +export { xrplAllowlist, XRPL_METHODS_ALLOWED }; + +export type XrplRouterInternals = Pick; + +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); + +export function createXrplDispatcher(r: XrplRouterInternals) { + async function dispatch(origin: string, method: string, params: unknown, chain: string | undefined): Promise { + if (!xrplAllowlist().has(method)) throw rpcError.unsupportedMethod(method); + + if (method === XRPL_INJECTED.state) return (await r.permitted(origin, "xrpl")) ? r.accounts(origin, "xrpl") : []; + + if (method === XRPL_INJECTED.disconnect) { + await r.revoke(origin, "xrpl"); + return null; + } + + if (method === XRPL_INJECTED.connect) { + if (await r.permitted(origin, "xrpl")) return r.accounts(origin, "xrpl"); + return r.connect(origin, "xrpl", r.requireNetwork("xrpl", origin, chain === undefined ? undefined : xrplChainId(chain) ?? chain), method, params ?? {}); + } + + // xrpl:signTransaction / xrpl:signAndSubmitTransaction + await r.requirePermission(origin, "xrpl"); + const p = obj(params); + const tx = obj(p.tx_json); + if (!Object.keys(tx).length) throw rpcError.invalidParams("Expected { tx_json, account, network }."); + const list = await r.accounts(origin, "xrpl"); + for (const a of [p.account, tx.Account]) { + if (a === undefined) continue; + if (typeof a !== "string" || !list.some((x) => x.address === a)) throw rpcError.unauthorized("That account is not connected to this site."); + } + if (p.account === undefined && tx.Account === undefined) throw rpcError.invalidParams("Expected { tx_json, account, network }."); + const fromParams = p.network === undefined ? undefined : xrplChainId(p.network); + if (p.network !== undefined && !fromParams) throw rpcError.invalidParams("network must be an XRPL chain such as xrpl:1."); + const hint = chain === undefined ? undefined : (xrplChainId(chain) ?? chain); + if (fromParams && hint && fromParams !== hint) throw rpcError.invalidParams("The request names two different networks."); + const net = r.requireNetwork("xrpl", origin, fromParams ?? hint); + const wire: Record = { tx_json: tx, account: p.account ?? tx.Account, network: net.id }; + if (p.options !== undefined) wire.options = obj(p.options); + return r.approve(r.makeReq(origin, "xrpl", net, method, wire)); + } + + return { dispatch }; +} diff --git a/packages/1mask/src/inpage/cosmos.ts b/packages/1mask/src/inpage/cosmos.ts new file mode 100644 index 00000000..c0da98e6 --- /dev/null +++ b/packages/1mask/src/inpage/cosmos.ts @@ -0,0 +1,323 @@ +import type { Family, Network } from "@clip-wallet/core"; +import type { WalletIdentity } from "../shared/config.js"; +import { ProviderRpcError, RpcErrorCode } from "../shared/errors.js"; +import { + COSMOS_FAMILIES, + COSMOS_INJECTED, + type CosmosChainInfoWithoutEndpoints, + type CosmosFamily, + type CosmosKeyWire, + chainInfoWithoutEndpoints, + cosmosChainOf, + b64FromBytes, + bytesFromB64, +} from "../shared/cosmos.js"; +import { bytesOfHex } from "../shared/cosmos-bech32.js"; +import { Emitter } from "./emitter.js"; +import { DEFAULT_GLOBAL_KEY, exposeOnGlobal, type InjectedOptions } from "./injected-base.js"; +import type { InpageTransport } from "./transport.js"; + +/** + * Keplr-compatible Cosmos provider at `window.clipwallet.cosmos` (never `window.keplr`: Clip announces its own + * identity). Method names, arguments and results follow Keplr's `Keplr` interface (@keplr-wallet/types 0.13.41 + * src/wallet/keplr.ts; https://docs.keplr.app/api/guide/enable-connection, get-key, sign-a-message, sign-arbitrary, + * broadcast-tx, suggest-chain, https://docs.keplr.app/api/use-with/cosmjs), so a dapp written for Keplr works by + * swapping `window.keplr` for this object. Offline signers follow @keplr-wallet/provider 0.13.41 src/cosmjs.ts + * (chain-id and signer checks before signing; getAccounts reports algo "secp256k1", as Keplr does). + * + * Every chain id maps to one Clip family by the wallet's network list ("cosmos:"), so permissions, + * accounts and approvals are per family; the background re-checks everything. Holds no secrets. + * + * Account changes: `on("keystorechange", …)` and a window event `_keystorechange` (Keplr's documented + * equivalent is `keplr_keystorechange`, https://docs.keplr.app/api/guide/custom-event; Clip doesn't fire Keplr's). + */ + +/** Keplr Key (types/src/wallet/keplr.ts L29-42). */ +export interface CosmosKey { + readonly name: string; + readonly algo: string; + readonly pubKey: Uint8Array; + readonly address: Uint8Array; + readonly bech32Address: string; + readonly ethereumHexAddress: string; + readonly isNanoLedger: boolean; + readonly isKeystone: boolean; +} + +export interface KeplrSignOptions { + readonly preferNoSetFee?: boolean; + readonly preferNoSetMemo?: boolean; + readonly disableBalanceCheck?: boolean; +} + +export interface StdSignature { + readonly pub_key: { readonly type: string; readonly value: string }; + readonly signature: string; +} + +/** cosmjs StdSignDoc (amino JSON). */ +export interface StdSignDoc { + readonly chain_id: string; + readonly account_number: string; + readonly sequence: string; + readonly timeout_height?: string; + readonly fee: { readonly amount: readonly { denom: string; amount: string }[]; readonly gas: string; readonly payer?: string; readonly granter?: string }; + readonly msgs: readonly { type: string; value: unknown }[]; + readonly memo: string; +} + +/** Account number as Keplr passes it (a `long` Long), a bigint (cosmjs ≥ 0.32 / cosmjs-types), a number or a string. */ +type AccountNumber = { toString(): string } | bigint | number | string | null | undefined; + +export interface DirectSignDoc { + bodyBytes?: Uint8Array | null; + authInfoBytes?: Uint8Array | null; + chainId?: string | null; + accountNumber?: AccountNumber; +} + +export interface AccountData { + readonly address: string; + readonly algo: "secp256k1"; + readonly pubkey: Uint8Array; +} + +export type BroadcastMode = "block" | "sync" | "async"; + +const enc = new TextEncoder(); + +function plainError(message: string, code: number = RpcErrorCode.InvalidParams): ProviderRpcError { + return new ProviderRpcError(code, message); +} + +export class ClipCosmosProvider { + readonly isClipWallet = true; + readonly name: string; + readonly icon: string; + /** Keplr fields: version, mode, defaultOptions (pages may change defaultOptions, as with Keplr). */ + readonly version = "0.12.0"; + readonly mode = "extension" as const; + defaultOptions: { sign?: KeplrSignOptions } = {}; + + readonly #identity: WalletIdentity; + readonly #networks: Network[]; + readonly #transport: InpageTransport; + readonly #events = new Emitter(); + readonly #keys = new Map(); + readonly #fire: () => void; + + constructor(identity: WalletIdentity, networks: Network[], transport: InpageTransport, fire: () => void) { + this.#identity = identity; + this.name = identity.name; + this.icon = identity.icon; + this.#networks = networks.filter((n) => (COSMOS_FAMILIES as readonly string[]).includes(n.family)); + this.#transport = transport; + this.#fire = fire; + transport.onEvent((family, event) => { + if (!(COSMOS_FAMILIES as readonly Family[]).includes(family)) return; + if (event !== "accountsChanged" && event !== "disconnect") return; + for (const chainId of [...this.#keys.keys()]) if (this.#familyOf(chainId) === family) this.#keys.delete(chainId); + this.#events.emit("keystorechange"); + this.#fire(); + }); + } + + #familyOf(chainId: string): CosmosFamily { + if (typeof chainId !== "string" || !chainId) throw plainError("chain id not set"); + const net = this.#networks.find((n) => n.id === `cosmos:${chainId}`); + if (!net || !cosmosChainOf(chainId)) throw plainError(`There is no chain info for ${chainId}`, RpcErrorCode.UnrecognizedChain); + return net.family as CosmosFamily; + } + + #request(chainId: string, method: string, params?: unknown): Promise { + return this.#transport.request(this.#familyOf(chainId), method, params, `cosmos:${chainId}`); + } + + /** Keplr enable(chainIds): one connect approval per family not yet connected. */ + async enable(chainIds: string | string[]): Promise { + const ids = typeof chainIds === "string" ? [chainIds] : chainIds; + if (!Array.isArray(ids) || ids.length === 0) throw plainError("chain id not set"); + const byFamily = new Map(); + for (const id of ids) { + const f = this.#familyOf(id); + byFamily.set(f, [...(byFamily.get(f) ?? []), id]); + } + for (const [, list] of byFamily) { + const first = list[0]!; + const have = (await this.#request(first, COSMOS_INJECTED.accounts)) as unknown[]; + if (!Array.isArray(have) || have.length === 0) await this.#request(first, COSMOS_INJECTED.enable, { chainIds: list }); + } + } + + /** Keplr disable(chainIds?): forgets this site's permission for those chains' families (all when omitted). */ + async disable(chainIds?: string | string[]): Promise { + const ids = chainIds === undefined ? undefined : typeof chainIds === "string" ? [chainIds] : chainIds; + const families = new Set(); + if (ids) for (const id of ids) families.add(this.#familyOf(id)); + else for (const n of this.#networks) families.add(n.family as CosmosFamily); + for (const f of families) { + const chainId = this.#networks.find((n) => n.family === f)!.id.slice("cosmos:".length); + await this.#request(chainId, COSMOS_INJECTED.disable); + } + this.#keys.clear(); + } + + async getKey(chainId: string): Promise { + const k = (await this.#request(chainId, COSMOS_INJECTED.getKey)) as CosmosKeyWire; + const key: CosmosKey = Object.freeze({ + name: this.#identity.name, + algo: k.algo, + pubKey: bytesOfHex(k.pubKey), + address: bytesOfHex(k.address), + bech32Address: k.bech32Address, + ethereumHexAddress: k.ethereumHexAddress, + isNanoLedger: false, + isKeystone: false, + }); + this.#keys.set(chainId, key); + return key; + } + + async signDirect(chainId: string, signer: string, signDoc: DirectSignDoc, _signOptions?: KeplrSignOptions): Promise<{ signed: DirectSignDoc & { bodyBytes: Uint8Array; authInfoBytes: Uint8Array; chainId: string }; signature: StdSignature }> { + if (!signDoc || typeof signDoc !== "object") throw plainError("Expected a sign doc."); + const accountNumber = signDoc.accountNumber === null || signDoc.accountNumber === undefined ? "0" : String(signDoc.accountNumber); + const wire = { + bodyBytes: b64FromBytes(signDoc.bodyBytes ?? new Uint8Array()), + authInfoBytes: b64FromBytes(signDoc.authInfoBytes ?? new Uint8Array()), + chainId: signDoc.chainId ?? "", + accountNumber, + }; + const res = (await this.#request(chainId, COSMOS_INJECTED.signDirect, { signerAddress: signer, signDoc: wire })) as { + signed: { bodyBytes: string; authInfoBytes: string; chainId: string; accountNumber: string }; + signature: StdSignature; + }; + // The account number keeps the caller's own type (Long, bigint…): Clip never changes the doc it signs. + return { + signed: { ...signDoc, bodyBytes: bytesFromB64(res.signed.bodyBytes), authInfoBytes: bytesFromB64(res.signed.authInfoBytes), chainId: res.signed.chainId }, + signature: res.signature, + }; + } + + async signAmino(chainId: string, signer: string, signDoc: StdSignDoc, _signOptions?: KeplrSignOptions): Promise<{ signed: StdSignDoc; signature: StdSignature }> { + return (await this.#request(chainId, COSMOS_INJECTED.signAmino, { signerAddress: signer, signDoc })) as { signed: StdSignDoc; signature: StdSignature }; + } + + /** ADR-36 (Keplr signArbitrary): a string is signed as its UTF-8 bytes. */ + async signArbitrary(chainId: string, signer: string, data: string | Uint8Array): Promise { + const bytes = typeof data === "string" ? enc.encode(data) : data; + if (!(bytes instanceof Uint8Array)) throw plainError("Expected a string or bytes."); + return (await this.#request(chainId, COSMOS_INJECTED.signArbitrary, { signer, data: b64FromBytes(bytes), isString: typeof data === "string" })) as StdSignature; + } + + async verifyArbitrary(chainId: string, signer: string, data: string | Uint8Array, signature: StdSignature): Promise { + const bytes = typeof data === "string" ? enc.encode(data) : data; + return (await this.#request(chainId, COSMOS_INJECTED.verifyArbitrary, { signer, data: b64FromBytes(bytes), signature })) === true; + } + + /** Keplr sendTx: broadcasts signed tx bytes; resolves to the tx hash bytes. */ + async sendTx(chainId: string, tx: Uint8Array, mode: BroadcastMode): Promise { + if (!(tx instanceof Uint8Array)) throw plainError("Expected the signed transaction bytes."); + const res = (await this.#request(chainId, COSMOS_INJECTED.sendTx, { tx: b64FromBytes(tx), mode })) as { txhash: string }; + return bytesOfHex(res.txhash); + } + + getOfflineSigner(chainId: string, signOptions?: KeplrSignOptions) { + return new ClipOfflineSigner(this, chainId, signOptions); + } + + getOfflineSignerOnlyAmino(chainId: string, signOptions?: KeplrSignOptions) { + return new ClipOfflineSignerOnlyAmino(this, chainId, signOptions); + } + + /** Clip has no Ledger accounts here, so this is always the direct + amino signer (Keplr returns it unless isNanoLedger). */ + async getOfflineSignerAuto(chainId: string, signOptions?: KeplrSignOptions) { + await this.getKey(chainId); + return new ClipOfflineSigner(this, chainId, signOptions); + } + + /** Chains this wallet ships, without endpoints (Keplr ChainInfoWithoutEndpoints). */ + async getChainInfosWithoutEndpoints(): Promise { + return this.#networks.flatMap((n) => { + const info = chainInfoWithoutEndpoints(n.id.slice("cosmos:".length)); + return info ? [info] : []; + }); + } + + async getChainInfoWithoutEndpoints(chainId: string): Promise { + this.#familyOf(chainId); + return chainInfoWithoutEndpoints(chainId)!; + } + + /** Clip only works with the chains it ships: a known chain resolves, anything else is refused in plain words. */ + async experimentalSuggestChain(chainInfo: { chainId?: unknown }): Promise { + const id = chainInfo && typeof chainInfo.chainId === "string" ? chainInfo.chainId : ""; + if (id && this.#networks.some((n) => n.id === `cosmos:${id}`)) return; + throw plainError(`${this.#identity.name} doesn't support ${id || "this chain"} and can't add chains from websites.`, RpcErrorCode.UnsupportedMethod); + } + + on(event: "keystorechange", listener: () => void): () => void { + this.#events.on(event, listener); + return () => this.#events.removeListener(event, listener); + } + + off(event: "keystorechange", listener: () => void): void { + this.#events.removeListener(event, listener); + } +} + +/** cosmjs OfflineAminoSigner over the provider (Keplr CosmJSOfflineSignerOnlyAmino). */ +export class ClipOfflineSignerOnlyAmino { + constructor( + protected readonly provider: ClipCosmosProvider, + readonly chainId: string, + protected readonly signOptions?: KeplrSignOptions, + ) {} + + async getAccounts(): Promise { + const key = await this.provider.getKey(this.chainId); + return [{ address: key.bech32Address, algo: "secp256k1", pubkey: key.pubKey }]; + } + + async signAmino(signerAddress: string, signDoc: StdSignDoc) { + if (this.chainId !== signDoc.chain_id) throw new Error("Unmatched chain id with the offline signer"); + const key = await this.provider.getKey(signDoc.chain_id); + if (key.bech32Address !== signerAddress) throw new Error("Unknown signer address"); + return this.provider.signAmino(this.chainId, signerAddress, signDoc, this.signOptions); + } + + /** Legacy cosmjs name. */ + sign(signerAddress: string, signDoc: StdSignDoc) { + return this.signAmino(signerAddress, signDoc); + } +} + +/** cosmjs OfflineDirectSigner & OfflineAminoSigner (Keplr CosmJSOfflineSigner). */ +export class ClipOfflineSigner extends ClipOfflineSignerOnlyAmino { + async signDirect(signerAddress: string, signDoc: DirectSignDoc & { chainId: string }) { + if (this.chainId !== signDoc.chainId) throw new Error("Unmatched chain id with the offline signer"); + const key = await this.provider.getKey(signDoc.chainId); + if (key.bech32Address !== signerAddress) throw new Error("Unknown signer address"); + return this.provider.signDirect(this.chainId, signerAddress, signDoc, this.signOptions); + } +} + +export function installCosmosProvider( + win: Window, + identity: WalletIdentity, + networks: Network[], + transport: InpageTransport, + opts: InjectedOptions = {}, +): { provider: ClipCosmosProvider; stop(): void } { + const globalKey = opts.globalKey ?? DEFAULT_GLOBAL_KEY; + const fire = () => { + try { + const E = (win as unknown as { Event?: typeof Event }).Event ?? Event; + win.dispatchEvent(new E(`${globalKey}_keystorechange`)); + } catch { + /* no Event constructor (tests) */ + } + }; + const provider = new ClipCosmosProvider(identity, networks, transport, fire); + const stop = exposeOnGlobal(win, globalKey, "cosmos", provider, identity); + return { provider, stop }; +} diff --git a/packages/1mask/src/inpage/fuel.ts b/packages/1mask/src/inpage/fuel.ts new file mode 100644 index 00000000..1afb5615 --- /dev/null +++ b/packages/1mask/src/inpage/fuel.ts @@ -0,0 +1,326 @@ +import type { WalletIdentity } from "../shared/config.js"; +import { RpcErrorCode, rpcError } from "../shared/errors.js"; +import { FUEL_INJECTED, type FuelConnectorNetwork, type FuelWireMessage } from "../shared/fuel.js"; +import type { ExposedAccount } from "../shared/protocol.js"; +import { Emitter } from "./emitter.js"; +import { DEFAULT_GLOBAL_KEY, exposeOnGlobal, type InjectedOptions } from "./injected-base.js"; +import type { InpageTransport } from "./transport.js"; + +/** + * Fuel connector (the Fuel connector standard: fuels-ts `FuelConnector`, packages/account/src/connectors/ + * fuel-connector.ts and fuel.ts in github.com/FuelLabs/fuels-ts, v0.103). Dependency-free: fuels-ts is never bundled + * into the page; transactions cross to the background as plain JSON. + * + * Discovery (fuels-ts connectors/utils/dispatch-fuel-connector-event.ts and `Fuel.setupConnectorListener`): a wallet + * announces itself with `window.dispatchEvent(new CustomEvent("FuelConnector", { detail: connector }))`; every `Fuel` + * instance listening on window adds it (`Fuel.addConnector`), pings it, and selects it if none is selected. Fuel Wallet's + * own connector (FuelLabs/fuel-connectors packages/fuel-wallet, FuelWalletConnector.setupConnector) dispatches the same + * event once its ping succeeds. Since a `Fuel` created later misses an early event, this connector announces on + * install, on DOMContentLoaded and on load, and is also at `window.clipwallet.fuel` so a dapp can pass it in + * `new Fuel({ connectors: [window.clipwallet.fuel] })`. It announces the wallet's own name and icon (AGENTS rule 8), + * never "Fuel Wallet". + * + * What `Fuel` and `Account` use from a connector (checked in fuels-ts source): name, metadata, installed/connected + * (Fuel writes them, so the object stays extensible), on/off (Fuel.setupConnectorEvents), ping, isConnected, the + * FuelConnectorMethods it proxies, and `sendTransaction(address, request, params)` whose string result Account turns + * into a TransactionResponse with provider.getTransactionResponse(id). The request is a fuels-ts TransactionRequest + * instance: `JSON.stringify` gives the same JSON Fuel Wallet's connector sends (BN → 0x-hex, bytes → 0x-hex). + */ + +/** fuels-ts FuelConnectorEventTypes (connectors/types/connector-types.ts). */ +export const FUEL_CONNECTOR_EVENTS = { + connectors: "connectors", + currentConnector: "currentConnector", + connection: "connection", + accounts: "accounts", + currentAccount: "currentAccount", + networks: "networks", + currentNetwork: "currentNetwork", + assets: "assets", + abis: "abis", + consolidateCoins: "consolidateCoins", +} as const; + +/** The window event wallets announce with (fuels-ts `FuelConnectorEventType`). */ +export const FUEL_CONNECTOR_EVENT = "FuelConnector"; + +type Hashable = string | { personalSign: string | Uint8Array | ArrayLike }; + +const toHex = (b: ArrayLike) => `0x${Array.from(b, (x) => (x & 0xff).toString(16).padStart(2, "0")).join("")}`; + +/** HashableMessage (fuels-ts @fuel-ts/hasher) → the wire form. Bytes can't cross JSON, so they go as hex. */ +export function fuelWireMessage(m: unknown): FuelWireMessage { + if (typeof m === "string") { + if (!m.trim()) throw rpcError.invalidParams("Message is required."); + return { text: m }; + } + const p = m && typeof m === "object" ? (m as { personalSign?: unknown }).personalSign : undefined; + if (typeof p === "string") { + if (!p) throw rpcError.invalidParams("Message is required."); + return { personalSign: p }; + } + if (p instanceof Uint8Array || (Array.isArray(p) && p.every((x) => Number.isInteger(x) && x >= 0 && x < 256))) { + if (!(p as ArrayLike).length) throw rpcError.invalidParams("Message is required."); + return { personalSignHex: toHex(p as ArrayLike) }; + } + throw rpcError.invalidParams("Expected a message string or { personalSign }."); +} + +/** A TransactionRequest (instance or JSON) → plain JSON, as Fuel Wallet's connector sends it. */ +function plainTransaction(tx: unknown): unknown { + if (!tx || (typeof tx !== "object" && typeof tx !== "string")) throw rpcError.invalidParams("Transaction is required."); + if (typeof tx === "string") return tx; + return JSON.parse(JSON.stringify(tx)); +} + +interface SendTxParams { + onBeforeSend?: (req: unknown) => Promise; + provider?: { url?: string }; +} + +export interface FuelConnectorOptions extends InjectedOptions { + /** metadata.install.link: where people get this wallet (shown by pickers only when it isn't installed). */ + installLink?: string; + /** Announce on DOMContentLoaded and load too (default true). */ + reannounce?: boolean; +} + +export class ClipFuelConnector { + readonly isClipWallet = true; + name: string; + metadata: { image: string; install: { action: string; link: string; description: string } }; + connected = false; + installed = true; + external = true; + events = FUEL_CONNECTOR_EVENTS; + readonly #events = new Emitter(); + readonly #transport: InpageTransport; + #accounts: string[] = []; + + constructor(identity: WalletIdentity, transport: InpageTransport, opts: FuelConnectorOptions = {}) { + this.name = identity.name; + this.metadata = { + image: identity.icon, + install: { action: "Install", link: opts.installLink ?? "", description: `Install ${identity.name} to connect it.` }, + }; + this.#transport = transport; + transport.onEvent((family, event, data) => { + if (family !== "fuel") return; + if (event === "accountsChanged") this.#setAccounts(Array.isArray(data) ? (data as ExposedAccount[]) : []); + else if (event === "disconnect") this.#setAccounts([]); + else if (event === "chainChanged" && data && typeof data === "object") this.#emit(FUEL_CONNECTOR_EVENTS.currentNetwork, data); + }); + } + + #request(method: string, params?: unknown): Promise { + return this.#transport.request("fuel", method, params); + } + + #emit(event: string, ...args: unknown[]) { + try { + this.#events.emit(event, ...args); + } catch { + /* a dapp listener throwing must not break the connector */ + } + } + + #setAccounts(list: ExposedAccount[]) { + const next = list.filter((a) => a && typeof a.address === "string").map((a) => a.address); + const changed = next.length !== this.#accounts.length || next.some((a, i) => a !== this.#accounts[i]); + const wasConnected = this.connected; + this.#accounts = next; + this.connected = next.length > 0; + if (changed) { + this.#emit(FUEL_CONNECTOR_EVENTS.accounts, [...next]); + this.#emit(FUEL_CONNECTOR_EVENTS.currentAccount, next[0] ?? null); + } + if (wasConnected !== this.connected) this.#emit(FUEL_CONNECTOR_EVENTS.connection, this.connected); + } + + async #silentAccounts(): Promise { + const list = (await this.#request(FUEL_INJECTED.accounts)) as ExposedAccount[]; + this.#setAccounts(Array.isArray(list) ? list : []); + return [...this.#accounts]; + } + + /* ---------------------------------------------------------------- EventEmitter surface (Fuel uses on/off) */ + + on(event: string, listener: (...args: any[]) => void): this { + this.#events.on(event, listener); + return this; + } + addListener(event: string, listener: (...args: any[]) => void): this { + return this.on(event, listener); + } + once(event: string, listener: (...args: any[]) => void): this { + this.#events.once(event, listener); + return this; + } + off(event: string, listener: (...args: any[]) => void): this { + this.#events.removeListener(event, listener); + return this; + } + removeListener(event: string, listener: (...args: any[]) => void): this { + return this.off(event, listener); + } + removeAllListeners(event?: string): this { + this.#events.removeAllListeners(event); + return this; + } + listenerCount(event: string): number { + return this.#events.listenerCount(event); + } + emit(event: string, ...args: unknown[]): boolean { + this.#emit(event, ...args); + return true; + } + setMaxListeners(_n: number): this { + return this; + } + + /* ---------------------------------------------------------------- FuelConnector methods */ + + /** Answered in the page: the connector is here, so it's alive. */ + async ping(): Promise { + return true; + } + + async version(): Promise<{ app: string; network: string }> { + return { app: "1.0.0", network: ">=0.48.0" }; + } + + async isConnected(): Promise { + try { + return (await this.#silentAccounts()).length > 0; + } catch { + return false; + } + } + + async accounts(): Promise { + return this.#silentAccounts(); + } + + async currentAccount(): Promise { + return (await this.#silentAccounts())[0] ?? null; + } + + /** True once the person approves; false when they decline (FuelConnector.connect contract). */ + async connect(): Promise { + try { + const list = (await this.#request(FUEL_INJECTED.connect, {})) as ExposedAccount[]; + this.#setAccounts(Array.isArray(list) ? list : []); + return this.connected; + } catch (e) { + if ((e as { code?: number })?.code === RpcErrorCode.UserRejected) return false; + throw e; + } + } + + /** Returns the connection status after disconnecting (false). */ + async disconnect(): Promise { + await this.#request(FUEL_INJECTED.disconnect); + this.#setAccounts([]); + return false; + } + + async signMessage(address: string, message: Hashable): Promise { + return (await this.#request(FUEL_INJECTED.signMessage, { address, message: fuelWireMessage(message) })) as string; + } + + async #tx(method: string, address: string, transaction: unknown, params?: SendTxParams): Promise { + let tx = transaction; + if (params?.onBeforeSend) tx = await params.onBeforeSend(tx); + const url = params?.provider?.url; + return this.#request(method, { address, transaction: plainTransaction(tx), ...(typeof url === "string" ? { provider: { url } } : {}) }); + } + + /** Signs and submits; resolves with the transaction id (Account turns it into a TransactionResponse). */ + async sendTransaction(address: string, transaction: unknown, params?: SendTxParams): Promise { + return (await this.#tx(FUEL_INJECTED.sendTransaction, address, transaction, params)) as string; + } + + /** Signs only; resolves with the signed TransactionRequest as JSON (`transactionRequestify` reads it), like Fuel Wallet. */ + async signTransaction(address: string, transaction: unknown, params?: SendTxParams): Promise { + return this.#tx(FUEL_INJECTED.signTransaction, address, transaction, params); + } + + async currentNetwork(): Promise { + return (await this.#request(FUEL_INJECTED.currentNetwork)) as FuelConnectorNetwork; + } + + async networks(): Promise { + return (await this.#request(FUEL_INJECTED.networks)) as FuelConnectorNetwork[]; + } + + async selectNetwork(network: { chainId?: number; url?: string }): Promise { + return (await this.#request(FUEL_INJECTED.selectNetwork, { chainId: network?.chainId, url: network?.url })) as boolean; + } + + /** Only the networks the wallet ships with. */ + async addNetwork(_networkUrl: string): Promise { + throw rpcError.userRejected(`${this.name} only connects to the networks it ships with.`); + } + + /** The wallet's token list is curated; a site can't add to it. */ + async assets(): Promise { + return []; + } + async addAsset(_asset: unknown): Promise { + return false; + } + async addAssets(_assets: unknown[]): Promise { + return false; + } + + /** ABIs aren't used: transactions are explained from their coin flows and a dry run. */ + async addABI(_contractId: string, _abi: unknown): Promise { + return false; + } + async getABI(_contractId: string): Promise { + return null; + } + async hasABI(_contractId: string): Promise { + return false; + } + + async startConsolidation(_opts: unknown): Promise { + throw rpcError.unsupportedMethod("startConsolidation"); + } +} + +/** Installs the connector at window[globalKey].fuel and announces it with the FuelConnector event. */ +export function installFuelConnector( + win: Window, + identity: WalletIdentity, + transport: InpageTransport, + opts: FuelConnectorOptions = {}, +): { connector: ClipFuelConnector; announce(): void; stop(): void } { + const connector = new ClipFuelConnector(identity, transport, opts); + const unexpose = exposeOnGlobal(win, opts.globalKey ?? DEFAULT_GLOBAL_KEY, "fuel", connector, identity); + const announce = () => { + try { + win.dispatchEvent(new (win as unknown as { CustomEvent: typeof CustomEvent }).CustomEvent(FUEL_CONNECTOR_EVENT, { detail: connector })); + } catch { + /* no CustomEvent: the global is still there */ + } + }; + announce(); + const again = () => announce(); + const reannounce = opts.reannounce !== false; + if (reannounce) { + win.addEventListener("DOMContentLoaded", again, { once: true }); + win.addEventListener("load", again, { once: true }); + } + return { + connector, + announce, + stop: () => { + unexpose(); + if (reannounce) { + win.removeEventListener("DOMContentLoaded", again); + win.removeEventListener("load", again); + } + }, + }; +} diff --git a/packages/1mask/src/inpage/index.ts b/packages/1mask/src/inpage/index.ts index 49548acf..cfee508d 100644 --- a/packages/1mask/src/inpage/index.ts +++ b/packages/1mask/src/inpage/index.ts @@ -13,6 +13,7 @@ import { resolveChannel, resolveIdentity, type InpageConfig, type WalletIdentity import { ClipBitcoinWallet } from "./bitcoin.js"; import { ClipEthereumProvider, announceEip6963, claimWindowEthereum, type EIP6963ProviderDetail } from "./evm.js"; import { installP2Providers, type InstalledP2 } from "./p2.js"; +import { installN87Providers, type InstalledN87 } from "./n87.js"; import { ClipSolanaWallet } from "./solana.js"; import { ClipSuiWallet } from "./sui.js"; import { ClipAptosWallet } from "./aptos.js"; @@ -37,6 +38,8 @@ export interface InstalledOneMask { ton?: ClipTonConnectBridge; /** NEAR (window.clipwallet.near + NEAR Connect), Stellar (SEP-43), Algorand, Tezos (Beacon relay). */ p2?: InstalledP2; + /** networks87: TRON (TIP-6963), Stacks (WBIP-004), Fuel (FuelConnector). */ + n87?: InstalledN87; /** Hedera extension discovery (DAppConnector / HashConnect): the id dApps address the wallet by. */ hedera?: { extensionId: string }; destroy(): void; @@ -114,6 +117,12 @@ export function installOneMask(config: InpageConfig, win: Window = window): Inst }); stops.push(p2.stop); out.p2 = p2; + const n87 = installN87Providers(win, identity, config.networks, transport, { + want: { cosmos: want.cosmos ?? true, tron: want.tron ?? true, stacks: want.stacks ?? true, fuel: want.fuel ?? true, xrpl: want.xrpl ?? true, multiversx: want.multiversx ?? true }, + ...(config.globalKey ? { globalKey: config.globalKey } : {}), + }); + stops.push(n87.stop); + out.n87 = n87; return out; } @@ -127,6 +136,7 @@ export { ClipAptosWallet, APTOS_FEATURES, METHOD_APTOS_NETWORK, aptosChain, toWi export * from "./bitcoin-features.js"; export { createInpageTransport, type InpageTransport } from "./transport.js"; export * from "./p2.js"; +export * from "./n87.js"; export { ClipCardanoWallet, Cip30Error, CIP30_METHODS, APIErrorCode, TxSignErrorCode, DataSignErrorCode, TxSendErrorCode, cardanoWalletKey, installCardano, toCip30Error } from "./cardano.js"; export { ClipSubstrateProvider, SUBSTRATE_INPAGE_METHODS, installSubstrate, substrateExtensionName, caip2FromGenesis } from "./substrate.js"; export { ClipStarknetWallet, StarknetWalletError, STARKNET_ERRORS, injectStarknet, starknetWalletId, toStarknetError } from "./starknet.js"; diff --git a/packages/1mask/src/inpage/multiversx.ts b/packages/1mask/src/inpage/multiversx.ts new file mode 100644 index 00000000..448f3808 --- /dev/null +++ b/packages/1mask/src/inpage/multiversx.ts @@ -0,0 +1,241 @@ +import type { Network } from "@clip-wallet/core"; +import { hexToBytes } from "../shared/bytes.js"; +import type { WalletIdentity } from "../shared/config.js"; +import { ProviderRpcError, RpcErrorCode, rpcError } from "../shared/errors.js"; +import { MULTIVERSX_INJECTED, multiversxNetworkIdOf } from "../shared/multiversx-methods.js"; +import type { ExposedAccount } from "../shared/protocol.js"; +import { DEFAULT_GLOBAL_KEY, type InjectedOptions, exposeOnGlobal } from "./injected-base.js"; +import type { InpageTransport } from "./transport.js"; + +/** + * MultiversX provider: BEST EFFORT, through an UNDOCUMENTED hook. + * + * MultiversX has no wallet-discovery standard for browser extensions. @multiversx/sdk-dapp 5.x's `initApp` merges + * `window.multiversx?.providers` with the dapp's own `customProviders` (deduplicated by `type`) into + * `ProviderFactory.customProviders` (out/methods/initApp/initApp.mjs, 5.7.3), and `ProviderFactory.create({ type })` + * calls the matching entry's `constructor({ address, anchor })`, then `provider.init()` + * (out/providers/ProviderFactory.mjs). The entry shape is `ICustomProvider` { name, type, iconUrl?, constructor } + * (out/providers/types/providerFactory.types.d.ts). sdk-dapp's README presents `window.multiversx.providers` as + * something the DAPP sets; nothing documents it as a channel for wallets. So this works only on sdk-dapp dapps + * that call initApp after the page script ran and don't replace `window.multiversx` (MultiversX's template dapp does + * replace it), and only for as long as sdk-dapp keeps reading it. + * + * Identity: the entry is listed under the wallet's own name, icon and type (the global key, "clipwallet"), never as + * "extension" or the MultiversX DeFi Wallet. Nothing here answers the DeFi Wallet's `erdw-*` postMessage protocol + * or sets `window.elrondWallet` / `window.multiversxWallet`. + * + * The provider implements sdk-dapp's `IProvider` (init, login, logout, getType, getAddress, plus sdk-dapp-utils 3.1 + * IDAppProviderBase: getAccount, setAccount, isInitialized, isConnected, signTransaction(s), signMessage). + * - Transactions arrive as the dapp's sdk-core `Transaction` objects. They're read duck-typed (`toPlainObject()`), + * sent as plain JSON, and the same objects come back with `signature` set (guardian fields untouched). + * mx-sdk-js-extension-provider returns `Transaction.newFromPlainObject(...)` copies instead; sdk-dapp only reads + * the returned objects (signTransactionsWithProvider), so mutating its own instances is equivalent and needs no + * sdk-core in the page. + * - `login({ token })` connects, then signs `address + token` as a MultiversX message (sdk-core MessageComputer). + * That is what sdk-native-auth-server checks: `signedMessage = \`${decoded.address}${decoded.body}\`` verified over + * `MessageComputer.computeBytesForSigning(new Message({ address, data }))` + * (mx-sdk-js-native-auth-server src/native.auth.server.ts `validate` / `verifySignature`), with `body` the login + * token sdk-dapp passed in (sdk-native-auth-client `getToken(address, token, signature)`). The background refuses a + * token whose origin isn't this site. + */ + +export const MULTIVERSX_DEFAULT_TYPE = DEFAULT_GLOBAL_KEY; + +/** What sdk-dapp's ProviderFactory reads from `window.multiversx.providers` (ICustomProvider). */ +export interface MultiversXProviderEntry { + readonly name: string; + readonly type: string; + readonly iconUrl?: string; + constructor: (options?: { address?: string; anchor?: unknown }) => Promise; +} + +/** The parts of an sdk-core Transaction this provider uses. */ +interface TransactionLike { + toPlainObject(): Record; + signature?: Uint8Array; +} + +/** The parts of an sdk-core Message this provider uses. */ +interface MessageLike { + data: Uint8Array; + signature?: Uint8Array; + address?: unknown; + signer?: string; +} + +const bytesOf = (h: unknown): Uint8Array => { + if (typeof h !== "string" || !/^[0-9a-f]{128}$/i.test(h)) throw rpcError.internal("The wallet returned no signature."); + return hexToBytes(h); +}; + +export class ClipMultiversXProvider { + readonly isClipWallet = true; + readonly #transport: InpageTransport; + readonly #type: string; + readonly #name: string; + readonly #defaultNetwork: string; + #account: { address: string } = { address: "" }; + #initialized = false; + + constructor(networks: Network[], transport: InpageTransport, type: string, name: string) { + this.#transport = transport; + this.#type = type; + this.#name = name; + this.#defaultNetwork = networks.find((n) => n.family === "multiversx")?.id ?? "mvx:D"; + transport.onEvent((family, event, data) => { + if (family !== "multiversx") return; + if (event === "disconnect") this.#account = { address: "" }; + if (event === "accountsChanged") { + const list = Array.isArray(data) ? (data as ExposedAccount[]) : []; + if (!list.some((a) => a?.address === this.#account.address)) this.#account = { address: list[0]?.address ?? "" }; + } + }); + } + + #call(method: string, params: unknown, chain = this.#defaultNetwork): Promise { + return this.#transport.request("multiversx", method, params, chain); + } + + async init(): Promise { + this.#initialized = true; + return true; + } + + isInitialized(): boolean { + return this.#initialized; + } + + isConnected(): boolean { + return !!this.#account.address; + } + + getType(): string { + return this.#type; + } + + async getAddress(): Promise { + return this.#account.address || undefined; + } + + getAccount(): { address: string } | null { + return this.#account.address ? { ...this.#account } : null; + } + + setAccount(account: { address?: string } | null | undefined): void { + this.#account = { address: typeof account?.address === "string" ? account.address : "" }; + } + + /** Connects (approval on first use per site); with a native-auth `token`, also signs `address + token`. */ + async login(options: { token?: string; callbackUrl?: string } = {}): Promise<{ address: string; signature: string }> { + let list = (await this.#call(MULTIVERSX_INJECTED.accounts, undefined)) as ExposedAccount[]; + if (!Array.isArray(list) || !list.length) list = (await this.#call(MULTIVERSX_INJECTED.connect, {})) as ExposedAccount[]; + const address = Array.isArray(list) ? list[0]?.address : undefined; + if (!address) throw new ProviderRpcError(RpcErrorCode.Unauthorized, "No MultiversX account is available."); + this.#account = { address }; + if (!options.token) return { address, signature: "" }; + const r = (await this.#call(MULTIVERSX_INJECTED.signMessage, { message: `${address}${options.token}`, address })) as { signature?: unknown }; + bytesOf(r?.signature); + return { address, signature: String(r.signature) }; + } + + async logout(): Promise { + try { + await this.#call(MULTIVERSX_INJECTED.disconnect, undefined); + } finally { + this.#account = { address: "" }; + } + return true; + } + + cancelLogin(): void { + /* the approval window has its own cancel */ + } + + #ensureConnected(): string { + if (!this.#account.address) throw rpcError.unauthorized("Connect first."); + return this.#account.address; + } + + async signTransaction(transaction: T): Promise { + const [signed] = await this.signTransactions([transaction]); + if (!signed) throw rpcError.internal("The wallet returned no signature."); + return signed; + } + + async signTransactions(transactions: T[]): Promise { + const address = this.#ensureConnected(); + if (!Array.isArray(transactions) || !transactions.length) throw rpcError.invalidParams("Expected a list of transactions."); + const plain = transactions.map((t) => { + if (!t || typeof t.toPlainObject !== "function") throw rpcError.invalidParams("Expected MultiversX Transaction objects."); + // JSON round trip: only plain data crosses to the background. + return JSON.parse(JSON.stringify(t.toPlainObject())) as Record; + }); + const chains = new Set(plain.map((p) => multiversxNetworkIdOf(p.chainID))); + if (chains.size !== 1 || chains.has(null)) throw rpcError.invalidParams("All transactions must name the same MultiversX chain."); + const r = (await this.#call(MULTIVERSX_INJECTED.signTransactions, { transactions: plain, address }, [...chains][0]!)) as { signatures?: { signature?: unknown }[] }; + const sigs = Array.isArray(r?.signatures) ? r.signatures : []; + if (sigs.length !== transactions.length) throw rpcError.internal("The wallet returned the wrong number of signatures."); + transactions.forEach((t, i) => { + t.signature = bytesOf(sigs[i]?.signature); + }); + return transactions; + } + + async signMessage(message: M): Promise { + const address = this.#ensureConnected(); + if (!message || !(message.data instanceof Uint8Array)) throw rpcError.invalidParams("Expected a MultiversX Message."); + let text: string; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(message.data); + } catch { + throw rpcError.invalidParams("Clip Wallet signs text messages only."); + } + const r = (await this.#call(MULTIVERSX_INJECTED.signMessage, { message: text, address })) as { signature?: unknown }; + message.signature = bytesOf(r?.signature); + message.signer = this.#name; + return message; + } +} + +/** + * Adds the wallet's entry to `window.multiversx.providers` (creating the object/array only if absent; other wallets' + * entries and the array itself are kept) and exposes the provider at `window[globalKey].multiversx`. + */ +export function installMultiversXProvider( + win: Window, + identity: WalletIdentity, + networks: Network[], + transport: InpageTransport, + opts: InjectedOptions = {}, +): { provider: ClipMultiversXProvider; entry: MultiversXProviderEntry; stop(): void } { + const type = opts.globalKey ?? DEFAULT_GLOBAL_KEY; + const provider = new ClipMultiversXProvider(networks, transport, type, identity.name); + const entry: MultiversXProviderEntry = Object.freeze({ + name: identity.name, + type, + iconUrl: identity.icon, + // sdk-dapp passes the address it stored at login (session restore); the background still checks every signer. + constructor: async (options?: { address?: string }) => { + if (options?.address && !provider.isConnected()) provider.setAccount({ address: options.address }); + return provider; + }, + }); + const w = win as unknown as { multiversx?: { providers?: unknown } }; + if (!w.multiversx || typeof w.multiversx !== "object") w.multiversx = {}; + if (!Array.isArray(w.multiversx.providers)) w.multiversx.providers = []; + const list = w.multiversx.providers as unknown[]; + if (!list.some((e) => (e as { type?: unknown })?.type === type)) list.push(entry); + const unexpose = exposeOnGlobal(win, type, "multiversx", provider, identity); + return { + provider, + entry, + stop: () => { + unexpose(); + const now = (win as unknown as { multiversx?: { providers?: unknown } }).multiversx?.providers; + if (Array.isArray(now)) { + const i = now.indexOf(entry); + if (i >= 0) now.splice(i, 1); + } + }, + }; +} diff --git a/packages/1mask/src/inpage/n87.ts b/packages/1mask/src/inpage/n87.ts new file mode 100644 index 00000000..3229158c --- /dev/null +++ b/packages/1mask/src/inpage/n87.ts @@ -0,0 +1,88 @@ +/** + * networks87 injected sides, each under the wallet's own identity on window[globalKey]. plus the + * ecosystem's own discovery: Cosmos SDK chains (a Keplr-compatible API at window[globalKey].cosmos, never window.keplr, + * for Osmosis, dYdX, ZIGChain, Provenance, THORChain and Initia), TRON (TIP-1193 provider announced with TIP-6963), Stacks (SIP-030 provider registered on + * window.wbip_providers, WBIP-004), Fuel (a FuelConnector announced with the FuelConnector event) + * the XRP Ledger (XLS-72d: Wallet Standard with xrpl:signTransaction / xrpl:signAndSubmitTransaction) and, best + * effort, MultiversX (an entry on sdk-dapp's UNDOCUMENTED window.multiversx.providers custom-provider hook). + * Families without a verifiable third-party wallet standard (ICP, Antelope; Bitcoin Cash is reached over WalletConnect wc2-bch-bcr) are send and + * receive only (see docs/r1/networks87.md). + */ +import type { Network } from "@clip-wallet/core"; +import type { WalletIdentity } from "../shared/config.js"; +import type { InjectedOptions } from "./injected-base.js"; +import { installCosmosProvider, type ClipCosmosProvider } from "./cosmos.js"; +import { installFuelConnector, type ClipFuelConnector } from "./fuel.js"; +import { installStacksProvider, type ClipStacksProvider } from "./stacks.js"; +import { installTronProvider, type ClipTronProvider } from "./tron.js"; +import { installXrpl, type ClipXrplWallet } from "./xrpl.js"; +import { installMultiversXProvider, type ClipMultiversXProvider } from "./multiversx.js"; +import type { InpageTransport } from "./transport.js"; + +export interface InstalledN87 { + cosmos?: ClipCosmosProvider; + tron?: ClipTronProvider; + stacks?: ClipStacksProvider; + fuel?: ClipFuelConnector; + xrpl?: ClipXrplWallet; + multiversx?: ClipMultiversXProvider; + stop(): void; +} + +export type N87ProviderFamily = "cosmos" | "tron" | "stacks" | "fuel" | "xrpl" | "multiversx"; + +/** Installs the providers for every networks87 family present in `networks`. */ +export function installN87Providers( + win: Window, + identity: WalletIdentity, + networks: Network[], + transport: InpageTransport, + opts: InjectedOptions & { want?: Partial> } = {}, +): InstalledN87 { + const want: Record = { cosmos: true, tron: true, stacks: true, fuel: true, xrpl: true, multiversx: true, ...opts.want }; + const has = (f: string) => networks.some((n) => n.family === f); + const g = opts.globalKey ? { globalKey: opts.globalKey } : {}; + const stops: (() => void)[] = []; + const out: InstalledN87 = { stop: () => stops.forEach((s) => s()) }; + if (want.cosmos && ["cosmos", "provenance", "thorchain", "initia"].some(has)) { + const r = installCosmosProvider(win, identity, networks, transport, g); + out.cosmos = r.provider; + stops.push(r.stop); + } + if (want.tron && has("tron")) { + const r = installTronProvider(win, identity, networks, transport, g); + out.tron = r.provider; + stops.push(r.stop); + } + if (want.stacks && has("stacks")) { + const r = installStacksProvider(win, identity, networks, transport, g); + out.stacks = r.provider; + stops.push(r.stop); + } + if (want.fuel && has("fuel")) { + const r = installFuelConnector(win, identity, transport, g); + out.fuel = r.connector; + stops.push(r.stop); + } + if (want.xrpl && has("xrpl")) { + const r = installXrpl(win, identity, networks, transport, g); + if (r) { + out.xrpl = r.wallet; + stops.push(r.stop); + } + } + if (want.multiversx && has("multiversx")) { + // Best effort: sdk-dapp's window.multiversx.providers hook is undocumented and only works on dapps that don't reset it. + const r = installMultiversXProvider(win, identity, networks, transport, g); + out.multiversx = r.provider; + stops.push(r.stop); + } + return out; +} + +export * from "./cosmos.js"; +export * from "./tron.js"; +export * from "./stacks.js"; +export * from "./fuel.js"; +export * from "./xrpl.js"; +export * from "./multiversx.js"; diff --git a/packages/1mask/src/inpage/stacks.ts b/packages/1mask/src/inpage/stacks.ts new file mode 100644 index 00000000..14687757 --- /dev/null +++ b/packages/1mask/src/inpage/stacks.ts @@ -0,0 +1,170 @@ +import type { Network } from "@clip-wallet/core"; +import type { WalletIdentity } from "../shared/config.js"; +import { RpcErrorCode, toRpcErrorShape } from "../shared/errors.js"; +import type { ExposedAccount } from "../shared/protocol.js"; +import { STACKS_INJECTED, STACKS_SIGNING_METHODS, stacksChainHint } from "../shared/stacks.js"; +import { Emitter } from "./emitter.js"; +import { DEFAULT_GLOBAL_KEY, exposeOnGlobal, type InjectedOptions } from "./injected-base.js"; +import type { InpageTransport } from "./transport.js"; + +/** + * Injected Stacks provider at `window.clipwallet.stacks` (or `window[globalKey].stacks`), implementing SIP-030 + * (https://github.com/stacksgov/sips/blob/main/sips/sip-030/sip-030-wallet-interface.md): `request(method, params)` + * answers with a JSON-RPC 2.0 response `{ jsonrpc, id, result }` and rejects with `{ jsonrpc, id, error }` using the + * SIP-030 codes (-32000 user rejection, -32002 access denied, -32601 method not found, -32602 invalid params). + * + * Discovery is WBIP-004 (https://wbips.netlify.app/wbips/WBIP004): the wallet PUSHES `{ id, name, icon, methods }` + * onto `window.wbip_providers`, where `id` is the provider's path on `window`. @stacks/connect v8 reads that array + * for its wallet list (connect-ui `getRegisteredProviders`) and resolves the chosen id with + * `id.split(".").reduce((o, k) => o?.[k], window)` (`getProviderFromId`), so the id "clipwallet.stacks" finds this + * object. https://github.com/stx-labs/connect/blob/main/packages/connect-ui/src/providers.ts + * + * SIP-030 discourages a shared global ("Using a shared global object between multiple wallets is discouraged"), so + * this never sets `window.StacksProvider`, `window.LeatherProvider` or `window.XverseProviders`. Holds no secrets. + */ + +export type StacksJsonRpcResponse = { jsonrpc: "2.0"; id: string | number | null } & ({ result: unknown } | { error: { code: number; message: string; data?: unknown } }); + +/** SIP-030 error codes. */ +export const SIP030_ERRORS = { userRejection: -32000, addressMismatch: -32001, accessDenied: -32002, methodNotFound: -32601, invalidParams: -32602, internal: -32603 } as const; + +export function toSip030Error(err: unknown): { code: number; message: string } { + const e = toRpcErrorShape(err); + const code = + e.code === RpcErrorCode.UserRejected + ? SIP030_ERRORS.userRejection + : e.code === RpcErrorCode.Unauthorized + ? SIP030_ERRORS.accessDenied + : e.code === RpcErrorCode.UnsupportedMethod || e.code === RpcErrorCode.MethodNotFound + ? SIP030_ERRORS.methodNotFound + : e.code === RpcErrorCode.InvalidParams || e.code === RpcErrorCode.ChainDisconnected || e.code === RpcErrorCode.UnrecognizedChain + ? SIP030_ERRORS.invalidParams + : SIP030_ERRORS.internal; + return { code, message: e.message }; +} + +/** WBIP-004 provider entry. */ +export interface WbipProvider { + id: string; + name: string; + icon: string; + webUrl?: string; + methods?: string[]; +} + +const ADDRESS_METHODS = new Set(["getAddresses", "stx_getAddresses", "stx_requestAccounts"]); +export const STACKS_PROVIDER_METHODS = ["getAddresses", "stx_getAddresses", "stx_getNetworks", ...STACKS_SIGNING_METHODS]; + +export class ClipStacksProvider { + readonly isClipWallet = true; + readonly name: string; + readonly icon: string; + readonly #events = new Emitter(); + readonly #transport: InpageTransport; + #accounts: ExposedAccount[] = []; + #nextId = 1; + + constructor(identity: WalletIdentity, _networks: Network[], transport: InpageTransport) { + this.name = identity.name; + this.icon = identity.icon; + this.#transport = transport; + transport.onEvent((f, event, data) => { + if (f !== "stacks") return; + if (event === "accountsChanged") this.#setAccounts(Array.isArray(data) ? (data as ExposedAccount[]) : []); + if (event === "disconnect") { + this.#setAccounts([]); + this.#events.emit("disconnect"); + } + }); + } + + #setAccounts(next: ExposedAccount[]): void { + const clean = next.filter((a) => a && typeof a.address === "string"); + const changed = clean.length !== this.#accounts.length || clean.some((a, i) => a.address !== this.#accounts[i]?.address); + this.#accounts = clean; + if (changed) this.#events.emit("accountsChanged", this.#accounts.map((a) => ({ address: a.address, publicKey: a.publicKey ?? "" }))); + } + + #send(method: string, params?: unknown, chain?: string): Promise { + return this.#transport.request("stacks", method, params, chain); + } + + async #call(method: string, params: unknown): Promise { + const p = params && typeof params === "object" && !Array.isArray(params) ? (params as Record) : {}; + const chain = stacksChainHint(p.network); + if (p.network !== undefined && !chain) { + throw Object.assign(new Error(`Clip Wallet doesn't support the Stacks network “${String(p.network)}”.`), { code: RpcErrorCode.InvalidParams }); + } + if (ADDRESS_METHODS.has(method)) { + const list = (await this.#send(STACKS_INJECTED.connect, p, chain)) as ExposedAccount[]; + this.#setAccounts(Array.isArray(list) ? list : []); + return { addresses: this.#accounts.map((a) => ({ symbol: "STX", address: a.address, publicKey: a.publicKey ?? "" })) }; + } + if (method === STACKS_INJECTED.getNetworks) return this.#send(method, undefined, chain); + if (STACKS_SIGNING_METHODS.includes(method)) return this.#send(method, params ?? {}, chain); + throw Object.assign(new Error(`Clip Wallet does not support ${method}.`), { code: RpcErrorCode.MethodNotFound }); + } + + /** SIP-030 request. Resolves `{ jsonrpc, id, result }`; rejects with `{ jsonrpc, id, error }`. */ + async request(method: string, params?: unknown): Promise { + const id = this.#nextId++; + if (typeof method !== "string") throw { jsonrpc: "2.0", id, error: { code: SIP030_ERRORS.invalidParams, message: "Expected a method name." } }; + try { + return { jsonrpc: "2.0", id, result: await this.#call(method, params) }; + } catch (err) { + throw { jsonrpc: "2.0", id, error: toSip030Error(err) }; + } + } + + /** SIP-030 listeners: "stx_accountChange" (accounts), also "accountsChanged" and "disconnect". Returns unlisten. */ + listen(event: string, listener: (...args: any[]) => void): () => void { + const name = event === "stx_accountChange" ? "accountsChanged" : event; + this.#events.on(name, listener); + return () => this.#events.removeListener(name, listener); + } + + /** Accounts this site sees right now (silent, no prompt). */ + async accounts(): Promise<{ address: string; publicKey: string }[]> { + const list = (await this.#send(STACKS_INJECTED.accounts)) as ExposedAccount[]; + this.#setAccounts(Array.isArray(list) ? list : []); + return this.#accounts.map((a) => ({ address: a.address, publicKey: a.publicKey ?? "" })); + } + + async disconnect(): Promise { + await this.#send(STACKS_INJECTED.disconnect); + this.#setAccounts([]); + } +} + +export function installStacksProvider( + win: Window, + identity: WalletIdentity, + networks: Network[], + transport: InpageTransport, + opts: InjectedOptions & { webUrl?: string } = {}, +): { provider: ClipStacksProvider; entry: WbipProvider; stop(): void } { + const provider = new ClipStacksProvider(identity, networks, transport); + const key = opts.globalKey ?? DEFAULT_GLOBAL_KEY; + const unexpose = exposeOnGlobal(win, key, "stacks", provider, identity); + const entry: WbipProvider = Object.freeze({ + id: `${key}.stacks`, + name: identity.name, + icon: identity.icon, + ...(opts.webUrl ? { webUrl: opts.webUrl } : {}), + methods: [...STACKS_PROVIDER_METHODS], + }); + // WBIP-004: make sure the array exists, then push; never replace it (other wallets' entries stay). + const w = win as unknown as { wbip_providers?: unknown }; + if (!Array.isArray(w.wbip_providers)) w.wbip_providers = []; + const list = w.wbip_providers as WbipProvider[]; + if (!list.some((p) => p && p.id === entry.id)) list.push(entry); + return { + provider, + entry, + stop: () => { + unexpose(); + const i = list.indexOf(entry); + if (i >= 0) list.splice(i, 1); + }, + }; +} diff --git a/packages/1mask/src/inpage/tron.ts b/packages/1mask/src/inpage/tron.ts new file mode 100644 index 00000000..f0c2a6f3 --- /dev/null +++ b/packages/1mask/src/inpage/tron.ts @@ -0,0 +1,314 @@ +import type { Network } from "@clip-wallet/core"; +import { uuidV4 } from "../shared/bytes.js"; +import type { WalletIdentity } from "../shared/config.js"; +import { ProviderRpcError, RpcErrorCode, rpcError } from "../shared/errors.js"; +import type { ExposedAccount } from "../shared/protocol.js"; +import { TRON_INJECTED, tronChainIdOf, tronNetworkIdOf } from "../shared/tron-methods.js"; +import { Emitter } from "./emitter.js"; +import { DEFAULT_GLOBAL_KEY, type InjectedOptions, exposeOnGlobal } from "./injected-base.js"; +import type { InpageTransport } from "./transport.js"; + +/** + * Injected TRON provider at `window.clipwallet.tron`, announced with TIP-6963 under the wallet's own identity. + * + * - TIP-1193 "TRON Provider JavaScript API" (Final): `request({ method, params })`, events `connect` / + * `disconnect` / `chainChanged` / `accountsChanged` via `on` / `removeListener` (Node EventEmitter API, return + * `this`), errors 4001 / 4100 / 4200 / 4900 / 4901. https://github.com/tronprotocol/tips/blob/master/tip-1193.md + * - TIP-1102 "Opt-in account exposure" (Final): `eth_requestAccounts` → `[address]`. + * https://github.com/tronprotocol/tips/blob/master/tip-1102.md + * - TIP-3326 "Wallet Switch TRON Chain Method" (Final): `wallet_switchEthereumChain` `[{ chainId }]` → null, 4902 for + * an unknown chain. https://github.com/tronprotocol/tips/blob/master/tip-3326.md + * - `eth_chainId`: TIP-1193 / TIP-3326 take the chain id from "TRON's eth_chainId" ("0x" + last 4 bytes of the genesis + * block id, e.g. 0x2b6653dc); answered from the page's current chain. + * - TIP-6963 "Multi Injected Provider Discovery" (Final): `TIP6963:announceProvider` with a frozen + * `{ info: { uuid, name, icon, rdns }, provider }`, re-announced on `TIP6963:requestProvider`. + * https://github.com/tronprotocol/tips/blob/master/tip-6963.md + * + * TIP-1193 defines no signing methods: it says the provider exposes an instantiated TronWeb as `tron.tronWeb` and + * dapps sign through it (`tronWeb.trx.sign`, `tronWeb.trx.signMessageV2`; that's also what @tronweb3/tronwallet-adapters + * calls). Clip doesn't ship TronWeb in the page; `tronWeb` here is only the documented subset dapps use for signing: + * `defaultAddress`, `ready`, `fullNode/solidityNode/eventServer.host`, `trx.sign(transaction)` and + * `trx.signMessageV2(message)`. Everything else on TronWeb (building transactions, reading the chain) a dapp does with + * its own TronWeb instance. Legacy `trx.sign(hexString)` message signing, `multiSign` and `_signTypedData` are refused + * with 4200. The provider never takes a private key, never sets `isTronLink`, and never touches `window.tronLink`, + * `window.tronWeb` or (unless `claimWindowTron` and nothing owns it) `window.tron`. + */ + +export interface TronRequestArguments { + readonly method: string; + readonly params?: readonly unknown[] | object; +} + +export interface TIP6963ProviderInfo { + uuid: string; + name: string; + icon: string; + rdns: string; +} + +export interface TIP6963ProviderDetail { + info: TIP6963ProviderInfo; + provider: ClipTronProvider; +} + +const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +/** "41…" hex of a base58 TRON address (checksum not re-checked: the address comes from the wallet itself). */ +function hex41(address: string): string | false { + let n = 0n; + for (const c of address) { + const i = B58.indexOf(c); + if (i < 0) return false; + n = n * 58n + BigInt(i); + } + const h = n.toString(16).padStart(50, "0"); + return h.startsWith("41") ? h.slice(0, 42) : false; +} + +const toHex = (b: ArrayLike) => Array.from(b, (x) => (x & 0xff).toString(16).padStart(2, "0")).join(""); + +export class ClipTronProvider { + readonly isClipWallet = true; + /** The documented signing subset of TronWeb (see the file comment). */ + readonly tronWeb: ClipTronWeb; + readonly #events = new Emitter(); + readonly #transport: InpageTransport; + readonly #networks: Network[]; + #accounts: string[] = []; + #chainId: string; + #connected = false; + + constructor(networks: Network[], transport: InpageTransport) { + this.#transport = transport; + this.#networks = networks.filter((n) => n.family === "tron" && tronChainIdOf(n.id)); + this.#chainId = tronChainIdOf(this.#networks[0]?.id ?? "") ?? "0x00000000"; + this.tronWeb = new ClipTronWeb(this); + transport.onEvent((family, event, data) => { + if (family !== "tron") return; + if (event === "accountsChanged") this.#setAccounts(Array.isArray(data) ? (data as ExposedAccount[]) : []); + if (event === "disconnect") { + this.#setAccounts([]); + if (this.#connected) { + this.#connected = false; + this.#events.emit("disconnect", rpcError.disconnected()); + } + } + }); + } + + get chainId(): string { + return this.#chainId; + } + + get selectedAddress(): string | undefined { + return this.#accounts[0]; + } + + /** The current network's CAIP-2 id (what the background validates). */ + get networkId(): string { + return `tron:${this.#chainId}`; + } + + get rpcUrl(): string { + return this.#networks.find((n) => n.id === this.networkId)?.rpcUrls[0] ?? ""; + } + + #setAccounts(list: ExposedAccount[]): void { + const next = list.filter((a) => a && typeof a.address === "string").map((a) => a.address); + const changed = next.length !== this.#accounts.length || next.some((a, i) => a !== this.#accounts[i]); + this.#accounts = next; + if (changed) this.#events.emit("accountsChanged", [...next]); + } + + /** Background call on the current chain. */ + call(method: string, params?: unknown): Promise { + return this.#transport.request("tron", method, params, this.networkId); + } + + async request(args: TronRequestArguments): Promise { + if (!args || typeof args !== "object" || typeof args.method !== "string") throw rpcError.invalidParams("Expected { method, params }."); + switch (args.method) { + case "eth_requestAccounts": { + let list = (await this.call(TRON_INJECTED.accounts)) as ExposedAccount[]; + if (!Array.isArray(list) || !list.length) list = (await this.call(TRON_INJECTED.connect, {})) as ExposedAccount[]; + this.#setAccounts(Array.isArray(list) ? list : []); + if (!this.#accounts.length) throw new ProviderRpcError(RpcErrorCode.Unauthorized, "No TRON account is available."); + if (!this.#connected) { + this.#connected = true; + this.#events.emit("connect", { chainId: this.#chainId }); + } + return [...this.#accounts]; + } + case "eth_chainId": + return this.#chainId; + case "wallet_switchEthereumChain": { + const p = Array.isArray(args.params) ? args.params[0] : args.params; + const chainId = p && typeof p === "object" ? (p as { chainId?: unknown }).chainId : undefined; + if (typeof chainId !== "string") throw rpcError.invalidParams("Expected [{ chainId }]."); + const id = tronNetworkIdOf(chainId); + if (!id || !this.#networks.some((n) => n.id === id)) throw rpcError.unrecognizedChain(chainId); + const next = tronChainIdOf(id)!; + if (next !== this.#chainId) { + this.#chainId = next; + this.#events.emit("chainChanged", { chainId: next }); + } + return null; + } + default: + throw rpcError.unsupportedMethod(args.method); + } + } + + /** Signs a TronWeb transaction object; resolves to it with `signature` added (TronWeb `trx.sign`). */ + async signTransaction(transaction: unknown): Promise { + if (!transaction || typeof transaction !== "object") throw rpcError.invalidParams("Expected a TronWeb transaction object."); + const address = this.#requireAccount(); + return this.call(TRON_INJECTED.signTransaction, { address, transaction }); + } + + /** TronWeb `trx.signMessageV2`: a string (UTF-8) or bytes; resolves to the "0x…" signature. */ + async signMessageV2(message: unknown): Promise { + const address = this.#requireAccount(); + let params: { address: string; message: string; encoding?: "hex" }; + if (typeof message === "string") params = { address, message }; + else if (message instanceof Uint8Array || (Array.isArray(message) && message.every((x) => Number.isInteger(x) && x >= 0 && x < 256))) params = { address, message: toHex(message as ArrayLike), encoding: "hex" }; + else throw rpcError.invalidParams("Expected a message string or bytes."); + const res = (await this.call(TRON_INJECTED.signMessage, params)) as { signature?: string }; + if (typeof res?.signature !== "string") throw rpcError.internal(); + return res.signature; + } + + #requireAccount(): string { + const a = this.#accounts[0]; + if (!a) throw rpcError.unauthorized("Connect Clip Wallet to this site first (eth_requestAccounts)."); + return a; + } + + on(event: string, listener: (...args: any[]) => void): this { + this.#events.on(event, listener); + return this; + } + + once(event: string, listener: (...args: any[]) => void): this { + this.#events.once(event, listener); + return this; + } + + removeListener(event: string, listener: (...args: any[]) => void): this { + this.#events.removeListener(event, listener); + return this; + } + + off(event: string, listener: (...args: any[]) => void): this { + return this.removeListener(event, listener); + } + + /** Not in TIP-1193: forgets this site's connection in the wallet. */ + async disconnect(): Promise { + await this.call(TRON_INJECTED.disconnect); + this.#setAccounts([]); + } +} + +/** The signing subset of TronWeb a TIP-1193 provider exposes as `tron.tronWeb` (see the file comment). */ +export class ClipTronWeb { + readonly #p: ClipTronProvider; + readonly trx: { + sign(transaction: unknown, privateKey?: unknown, useTronHeader?: unknown, multisig?: unknown): Promise; + signMessageV2(message: unknown, privateKey?: unknown): Promise; + multiSign(): Promise; + _signTypedData(): Promise; + }; + + constructor(p: ClipTronProvider) { + this.#p = p; + const noKeys = (k: unknown) => { + if (k !== undefined && k !== false && k !== null) throw rpcError.invalidParams("Clip Wallet signs with its own keys; don't pass a private key."); + }; + this.trx = Object.freeze({ + sign: async (transaction: unknown, privateKey?: unknown, _useTronHeader?: unknown, multisig?: unknown) => { + noKeys(privateKey); + if (typeof transaction === "string") throw new ProviderRpcError(RpcErrorCode.UnsupportedMethod, "Clip Wallet signs messages with signMessageV2 only."); + if (multisig) throw rpcError.unsupportedMethod("multiSign"); + return p.signTransaction(transaction); + }, + signMessageV2: async (message: unknown, privateKey?: unknown) => { + noKeys(privateKey); + return p.signMessageV2(message); + }, + multiSign: async () => { + throw rpcError.unsupportedMethod("multiSign"); + }, + _signTypedData: async () => { + throw rpcError.unsupportedMethod("_signTypedData"); + }, + }); + } + + get ready(): boolean { + return !!this.#p.selectedAddress; + } + + get defaultAddress(): { base58: string | false; hex: string | false } { + const a = this.#p.selectedAddress; + return { base58: a ?? false, hex: a ? hex41(a) : false }; + } + + get fullNode(): { host: string } { + return { host: this.#p.rpcUrl }; + } + + get solidityNode(): { host: string } { + return { host: this.#p.rpcUrl }; + } + + get eventServer(): { host: string } { + return { host: this.#p.rpcUrl }; + } +} + +/** TIP-6963: announce now and on every `TIP6963:requestProvider`. One v4 uuid per page session. */ +export function announceTip6963(win: Window, identity: WalletIdentity, provider: ClipTronProvider): { detail: TIP6963ProviderDetail; stop(): void } { + const info: TIP6963ProviderInfo = Object.freeze({ uuid: uuidV4(), name: identity.name, icon: identity.icon, rdns: identity.rdns }); + const detail: TIP6963ProviderDetail = Object.freeze({ info, provider }); + const announce = () => win.dispatchEvent(new CustomEvent("TIP6963:announceProvider", { detail })); + win.addEventListener("TIP6963:requestProvider", announce); + announce(); + return { detail, stop: () => win.removeEventListener("TIP6963:requestProvider", announce) }; +} + +export interface TronInjectedOptions extends InjectedOptions { + /** + * Also set `window.tron` (TIP-1193's suggested global) when nothing owns it. Default false: TIP-6963 discovery and + * `window.clipwallet.tron` only. Never overrides another wallet's `window.tron`. + */ + claimWindowTron?: boolean; +} + +export function installTronProvider( + win: Window, + identity: WalletIdentity, + networks: Network[], + transport: InpageTransport, + opts: TronInjectedOptions = {}, +): { provider: ClipTronProvider; detail: TIP6963ProviderDetail; claimedWindowTron: boolean; stop(): void } { + const provider = new ClipTronProvider(networks, transport); + const unexpose = exposeOnGlobal(win, opts.globalKey ?? DEFAULT_GLOBAL_KEY, "tron", provider, identity); + const { detail, stop } = announceTip6963(win, identity, provider); + let claimed = false; + const w = win as unknown as { tron?: unknown }; + if (opts.claimWindowTron && w.tron === undefined) { + Object.defineProperty(win, "tron", { value: provider, configurable: true, enumerable: false, writable: false }); + claimed = true; + } + return { + provider, + detail, + claimedWindowTron: claimed, + stop: () => { + unexpose(); + stop(); + if (claimed) delete (win as unknown as { tron?: unknown }).tron; + }, + }; +} diff --git a/packages/1mask/src/inpage/xrpl.ts b/packages/1mask/src/inpage/xrpl.ts new file mode 100644 index 00000000..35d26413 --- /dev/null +++ b/packages/1mask/src/inpage/xrpl.ts @@ -0,0 +1,134 @@ +import type { Network } from "@clip-wallet/core"; +import type { IdentifierArray, Wallet, WalletAccount } from "@wallet-standard/base"; +import { + StandardConnect, + StandardDisconnect, + StandardEvents, + type StandardConnectFeature, + type StandardConnectInput, + type StandardConnectOutput, + type StandardDisconnectFeature, + type StandardEventsFeature, +} from "@wallet-standard/features"; +import { registerWallet } from "@wallet-standard/wallet"; +import type { WalletIdentity } from "../shared/config.js"; +import { rpcError } from "../shared/errors.js"; +import type { ExposedAccount } from "../shared/protocol.js"; +import { XRPL_INJECTED, XRPL_SIGN_AND_SUBMIT_TRANSACTION, XRPL_SIGN_TRANSACTION, xrplChainId } from "../shared/xrpl.js"; +import { DEFAULT_GLOBAL_KEY, type InjectedOptions, exposeOnGlobal } from "./injected-base.js"; +import { StandardWalletBase } from "./standard-base.js"; +import type { InpageTransport } from "./transport.js"; + +/** + * XLS-72d XRPL wallet on Wallet Standard (see ../shared/xrpl.ts for the spec links). Types are written out here + * from @xrpl-wallet-standard/core 0.1.4 instead of importing it: that package pulls in xrpl.js and an alpha + * Xahau fork just for transaction types. The wallet registers under the kit wallet's own name and icon + * (`registerWallet`), so dapps list it as itself; it never claims to be GemWallet, Crossmark or Xaman. + */ + +export interface XrplSignTransactionInput { + tx_json: Record; + account: WalletAccount; + /** `xrpl:` or an alias (`xrpl:testnet`). */ + network: string; + options?: { autofill?: boolean; multisig?: boolean }; +} +export interface XrplSignTransactionOutput { + signed_tx_blob: string; +} +export interface XrplSignAndSubmitTransactionOutput { + tx_hash: string; + tx_json: Record; +} + +export type XrplSignTransactionFeature = { + [XRPL_SIGN_TRANSACTION]: { version: "1.0.0"; signTransaction(input: XrplSignTransactionInput): Promise }; +}; +export type XrplSignAndSubmitTransactionFeature = { + [XRPL_SIGN_AND_SUBMIT_TRANSACTION]: { version: "1.0.0"; signAndSubmitTransaction(input: XrplSignTransactionInput): Promise }; +}; + +export type ClipXrplFeatures = StandardConnectFeature & StandardDisconnectFeature & StandardEventsFeature & XrplSignTransactionFeature & XrplSignAndSubmitTransactionFeature; + +/** @xrpl-wallet-standard/core REQUIRED_FEATURES, plus standard:disconnect. */ +export const XRPL_FEATURES = [StandardConnect, StandardDisconnect, StandardEvents, XRPL_SIGN_TRANSACTION, XRPL_SIGN_AND_SUBMIT_TRANSACTION] as const; + +/** The registry's XRPL networks as XLS-72d chains (their CAIP-2 ids already are: "xrpl:0", "xrpl:1", "xrpl:2"). */ +export function xrplChains(networks: Network[]): `xrpl:${string}`[] { + return networks.filter((n) => n.family === "xrpl" && /^xrpl:\d+$/.test(n.id)).map((n) => n.id as `xrpl:${string}`); +} + +export class ClipXrplWallet extends StandardWalletBase { + protected readonly family = "xrpl" as const; + protected readonly accountFeatures: IdentifierArray = [XRPL_SIGN_TRANSACTION, XRPL_SIGN_AND_SUBMIT_TRANSACTION]; + + constructor(identity: WalletIdentity, networks: Network[], transport: InpageTransport) { + super(identity, xrplChains(networks), transport); + this.listen(); + } + + get features(): ClipXrplFeatures { + return { + [StandardConnect]: { version: "1.0.0", connect: this.#connect }, + [StandardDisconnect]: { version: "1.0.0", disconnect: this.#disconnect }, + [StandardEvents]: { version: "1.0.0", on: this.on }, + [XRPL_SIGN_TRANSACTION]: { version: "1.0.0", signTransaction: this.#signTransaction }, + [XRPL_SIGN_AND_SUBMIT_TRANSACTION]: { version: "1.0.0", signAndSubmitTransaction: this.#signAndSubmitTransaction }, + }; + } + + #connect = async (input?: StandardConnectInput): Promise => { + if (input?.silent) return { accounts: await this.silentAccounts() }; + const res = await this.transport.request("xrpl", XRPL_INJECTED.connect); + return { accounts: this.setAccounts(Array.isArray(res) ? (res as ExposedAccount[]) : []) }; + }; + + #disconnect = async (): Promise => { + await this.transport.request("xrpl", XRPL_INJECTED.disconnect); + this.setAccounts([]); + }; + + #wire(input: XrplSignTransactionInput): { params: Record; chain: string } { + if (!input || typeof input !== "object" || !input.tx_json || typeof input.tx_json !== "object") throw rpcError.invalidParams("Expected { tx_json, account, network }."); + const chain = xrplChainId(input.network); + if (!chain || !this.chains.includes(chain as `xrpl:${string}`)) throw rpcError.chainDisconnected(`This wallet does not support ${String(input.network)}.`); + const account = this.ownAccount(input.account).address; + const params: Record = { tx_json: input.tx_json, account, network: chain }; + if (input.options && typeof input.options === "object") { + params.options = { ...(input.options.autofill !== undefined ? { autofill: !!input.options.autofill } : {}), ...(input.options.multisig !== undefined ? { multisig: !!input.options.multisig } : {}) }; + } + return { params, chain }; + } + + #signTransaction = async (input: XrplSignTransactionInput): Promise => { + const { params, chain } = this.#wire(input); + const res = (await this.transport.request("xrpl", XRPL_SIGN_TRANSACTION, params, chain)) as XrplSignTransactionOutput; + return { signed_tx_blob: res.signed_tx_blob }; + }; + + #signAndSubmitTransaction = async (input: XrplSignTransactionInput): Promise => { + const { params, chain } = this.#wire(input); + const res = (await this.transport.request("xrpl", XRPL_SIGN_AND_SUBMIT_TRANSACTION, params, chain)) as XrplSignAndSubmitTransactionOutput; + return { tx_hash: res.tx_hash, tx_json: res.tx_json }; + }; +} + +/** + * Registers the wallet with Wallet Standard (how XLS-72d dapps find wallets: @wallet-standard/app getWallets(), + * @xrpl-wallet-standard/app getRegisterdXRPLWallets()) and exposes it at `window.clipwallet.xrpl` (or the kit + * wallet's own global key). Returns undefined when the registry has no XRPL network. + */ +export function installXrpl( + win: Window, + identity: WalletIdentity, + networks: Network[], + transport: InpageTransport, + opts: InjectedOptions & { register?: (w: Wallet) => void } = {}, +): { wallet: ClipXrplWallet; stop(): void } | undefined { + if (!xrplChains(networks).length) return undefined; + const wallet = new ClipXrplWallet(identity, networks, transport); + // Wallet Standard has no unregister for wallets (the other Standard wallets in index.ts don't have one either). + (opts.register ?? registerWallet)(wallet); + const unexpose = exposeOnGlobal(win, opts.globalKey ?? DEFAULT_GLOBAL_KEY, "xrpl", wallet, identity); + return { wallet, stop: unexpose }; +} diff --git a/packages/1mask/src/shared/bitcoincash.ts b/packages/1mask/src/shared/bitcoincash.ts new file mode 100644 index 00000000..30d13c8c --- /dev/null +++ b/packages/1mask/src/shared/bitcoincash.ts @@ -0,0 +1,83 @@ +import type { NetworkId } from "@clip-wallet/core"; + +/** + * Bitcoin Cash dapp connectivity. BCH has no injected-provider standard; dapps reach wallets over WalletConnect v2 + * with the community spec wc2-bch-bcr (https://github.com/mainnet-pat/wc2-bch-bcr, used by Cashonize, Paytaca, Zapit + * and apps such as TapSwap): namespace "bch", chains "bch:bitcoincash" / "bch:bchtest", methods bch_getAddresses, + * bch_signTransaction, bch_signMessage, event "addressesChanged". The signing methods are chains-bitcoincash's own + * DappRequest methods, so the background hands them over unchanged. + */ +export const BCH_WC = { + namespace: "bch", + getAddresses: "bch_getAddresses", + signTransaction: "bch_signTransaction", + signMessage: "bch_signMessage", + events: ["addressesChanged"], +} as const; + +export const BCH_SIGNING_METHODS: readonly string[] = [BCH_WC.signTransaction, BCH_WC.signMessage]; + +/** + * wc2-bch-bcr chain ids ↔ the wallet's CAIP-2 network ids (chains-bitcoincash BCH_NETS: bip122 + first own block). + * "bch:bchtest" names any test network; chipnet (where BCH upgrades are tested) is the default one. + */ +export const BCH_WC_CHAINS: Record = { + "bch:bitcoincash": "bip122:000000000000000000651ef99cb9fcbe", + "bch:bchtest": "bip122:00000000040ba9641ba98a37b2e5ceea", +}; + +const BCH_NETWORK_PREFIX: Record = { + "bip122:000000000000000000651ef99cb9fcbe": "bitcoincash", + "bip122:00000000040ba9641ba98a37b2e5ceea": "bchtest", + "bip122:000000001dd410c49a788668ce267517": "bchtest", +}; + +export function bchNetworkForWcChain(chain: string | undefined): NetworkId | undefined { + if (!chain) return undefined; + return BCH_WC_CHAINS[chain] ?? (BCH_NETWORK_PREFIX[chain] ? chain : undefined); +} + +export function bchWcChainFor(networkId: NetworkId): string | undefined { + const p = BCH_NETWORK_PREFIX[networkId]; + return p === "bitcoincash" ? "bch:bitcoincash" : p ? "bch:bchtest" : undefined; +} + +/* ------------------------------------------------------------------ CashAddr re-spelling (checksum only, no hashing) */ + +const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + +function polymod(values: number[]): bigint { + const GEN = [0x98f2bc8e61n, 0x79b76d99e2n, 0xf33e5fb3c4n, 0xae2eabe2a8n, 0x1e4f43e470n]; + let c = 1n; + for (const d of values) { + const c0 = c >> 35n; + c = ((c & 0x07ffffffffn) << 5n) ^ BigInt(d); + for (let i = 0; i < 5; i++) if ((c0 >> BigInt(i)) & 1n) c ^= GEN[i]!; + } + return c ^ 1n; +} + +const words = (prefix: string) => [...prefix].map((ch) => ch.charCodeAt(0) & 31); + +/** + * The same account's CashAddr for `networkId` ("bitcoincash:q…" → "bchtest:q…"): the payload stays, only the + * prefix-bound checksum changes. Verifies the input checksum; undefined for anything that isn't a CashAddr. + */ +export function bchAddressOn(address: string, networkId: NetworkId): string | undefined { + const target = BCH_NETWORK_PREFIX[networkId]; + const a = address.trim().toLowerCase(); + const i = a.indexOf(":"); + if (!target || i <= 0) return undefined; + const payload: number[] = []; + for (const ch of a.slice(i + 1)) { + const w = CHARSET.indexOf(ch); + if (w < 0) return undefined; + payload.push(w); + } + if (payload.length < 42 || polymod([...words(a.slice(0, i)), 0, ...payload]) !== 0n) return undefined; + const data = payload.slice(0, -8); + const mod = polymod([...words(target), 0, ...data, 0, 0, 0, 0, 0, 0, 0, 0]); + const check: number[] = []; + for (let k = 0; k < 8; k++) check.push(Number((mod >> BigInt(5 * (7 - k))) & 31n)); + return `${target}:${[...data, ...check].map((w) => CHARSET[w]).join("")}`; +} diff --git a/packages/1mask/src/shared/config.ts b/packages/1mask/src/shared/config.ts index f427dff8..3e9b25ae 100644 --- a/packages/1mask/src/shared/config.ts +++ b/packages/1mask/src/shared/config.ts @@ -46,6 +46,13 @@ export interface InpageConfig { stellar?: boolean; tezos?: boolean; algorand?: boolean; + /** networks87: Cosmos SDK (Keplr-compatible), TRON (TIP-1193/TIP-6963), Stacks (SIP-030/WBIP-004), Fuel (FuelConnector). */ + cosmos?: boolean; + tron?: boolean; + stacks?: boolean; + fuel?: boolean; + xrpl?: boolean; + multiversx?: boolean; cardano?: boolean; substrate?: boolean; starknet?: boolean; diff --git a/packages/1mask/src/shared/cosmos-bech32.ts b/packages/1mask/src/shared/cosmos-bech32.ts new file mode 100644 index 00000000..a159ee7b --- /dev/null +++ b/packages/1mask/src/shared/cosmos-bech32.ts @@ -0,0 +1,80 @@ +/** + * BIP-173 bech32 (not bech32m), dependency-free so the page bundle and the background dispatcher can re-spell a + * Cosmos account address with another chain's prefix (the 20 address bytes are the same on every chain of a key + * family). Spec and reference: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + */ + +const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; +const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + +function polymod(values: number[]): number { + let chk = 1; + for (const v of values) { + const b = chk >>> 25; + chk = ((chk & 0x1ffffff) << 5) ^ v; + for (let i = 0; i < 5; i++) if ((b >>> i) & 1) chk ^= GEN[i]!; + } + return chk; +} + +function hrpExpand(hrp: string): number[] { + const out: number[] = []; + for (let i = 0; i < hrp.length; i++) out.push(hrp.charCodeAt(i) >>> 5); + out.push(0); + for (let i = 0; i < hrp.length; i++) out.push(hrp.charCodeAt(i) & 31); + return out; +} + +function convert(data: ArrayLike, from: number, to: number, pad: boolean): number[] | null { + let acc = 0; + let bits = 0; + const out: number[] = []; + const max = (1 << to) - 1; + for (let i = 0; i < data.length; i++) { + const v = data[i]!; + if (v < 0 || v >> from) return null; + acc = (acc << from) | v; + bits += from; + while (bits >= to) { + bits -= to; + out.push((acc >>> bits) & max); + } + } + if (pad) { + if (bits > 0) out.push((acc << (to - bits)) & max); + } else if (bits >= from || (acc << (to - bits)) & max) return null; + return out; +} + +export function bech32Encode(hrp: string, data: Uint8Array): string { + const words = convert(data, 8, 5, true)!; + const values = [...hrpExpand(hrp), ...words, 0, 0, 0, 0, 0, 0]; + const mod = polymod(values) ^ 1; + const checksum = Array.from({ length: 6 }, (_, i) => (mod >>> (5 * (5 - i))) & 31); + return `${hrp}1${[...words, ...checksum].map((w) => CHARSET[w]).join("")}`; +} + +/** { prefix, data } for a valid lower-case bech32 string, else null. */ +export function bech32Decode(value: string): { prefix: string; data: Uint8Array } | null { + if (typeof value !== "string" || value.length > 90 || value !== value.toLowerCase()) return null; + const sep = value.lastIndexOf("1"); + if (sep < 1 || sep + 7 > value.length) return null; + const hrp = value.slice(0, sep); + const words: number[] = []; + for (const ch of value.slice(sep + 1)) { + const w = CHARSET.indexOf(ch); + if (w < 0) return null; + words.push(w); + } + if (polymod([...hrpExpand(hrp), ...words]) !== 1) return null; + const bytes = convert(words.slice(0, -6), 5, 8, false); + return bytes ? { prefix: hrp, data: Uint8Array.from(bytes) } : null; +} + +export const hexOf = (b: Uint8Array): string => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); + +export function bytesOfHex(h: string): Uint8Array { + const clean = h.replace(/^0x/, ""); + if (clean.length % 2 || !/^[0-9a-fA-F]*$/.test(clean)) throw new Error("not hex"); + return Uint8Array.from(clean.match(/../g) ?? [], (x) => parseInt(x, 16)); +} diff --git a/packages/1mask/src/shared/cosmos.ts b/packages/1mask/src/shared/cosmos.ts new file mode 100644 index 00000000..c361faf4 --- /dev/null +++ b/packages/1mask/src/shared/cosmos.ts @@ -0,0 +1,151 @@ +import type { Family } from "@clip-wallet/core"; +import { METHOD_WS_STATE } from "./protocol.js"; + +/** + * Wire methods and public chain facts for the Keplr-compatible Cosmos provider (inpage/cosmos.ts, + * background/cosmos.ts). The signing method names are chains-cosmos `COSMOS_METHODS`, so the background hands them + * to the module unchanged. The chain table repeats what @clip-wallet/chains-cosmos/networks ships (1Mask doesn't + * depend on chain packages); chains-cosmos's tests check the two agree. + */ + +export type CosmosFamily = "cosmos" | "provenance" | "thorchain" | "initia"; +export const COSMOS_FAMILIES = ["cosmos", "provenance", "thorchain", "initia"] as const satisfies readonly Family[]; + +export const COSMOS_INJECTED = { + accounts: METHOD_WS_STATE, + enable: "cosmos:enable", + disable: "cosmos:disable", + getKey: "cosmos:getKey", + signDirect: "cosmos_signDirect", + signAmino: "cosmos_signAmino", + signArbitrary: "cosmos_signArbitrary", + sendTx: "cosmos_sendTx", + verifyArbitrary: "cosmos_verifyArbitrary", +} as const; + +/** Connect method: the extension's connector treats it as a connect approval. */ +export const COSMOS_CONNECT_METHODS = [COSMOS_INJECTED.enable] as const; + +/** Keplr Key over the wire (bytes as hex). */ +export interface CosmosKeyWire { + algo: string; + pubKey: string; + address: string; + bech32Address: string; + ethereumHexAddress: string; +} + +export interface CosmosCurrency { + coinDenom: string; + coinMinimalDenom: string; + coinDecimals: number; +} + +/** Keplr ChainInfoWithoutEndpoints (@keplr-wallet/types src/chain-info.ts): ChainInfo minus rest/rpc/nodeProvider. */ +export interface CosmosChainInfoWithoutEndpoints { + readonly rest: undefined; + readonly rpc: undefined; + readonly nodeProvider: undefined; + readonly chainId: string; + readonly chainName: string; + readonly stakeCurrency?: CosmosCurrency; + readonly bip44: { coinType: number }; + readonly bech32Config: { + bech32PrefixAccAddr: string; + bech32PrefixAccPub: string; + bech32PrefixValAddr: string; + bech32PrefixValPub: string; + bech32PrefixConsAddr: string; + bech32PrefixConsPub: string; + }; + readonly currencies: CosmosCurrency[]; + readonly feeCurrencies: (CosmosCurrency & { gasPriceStep?: { low: number; average: number; high: number } })[]; + readonly features: string[]; + readonly isTestnet: boolean; +} + +export interface CosmosChainFacts { + chainId: string; + family: CosmosFamily; + chainName: string; + prefix: string; + coinType: number; + /** Keplr Key.algo. */ + algo: "secp256k1" | "ethsecp256k1"; + native: CosmosCurrency; + /** Native gas price step (chain-registry). */ + gas: { low: number; average: number; high: number }; + usdc?: string; + /** THORChain has no x/staking: no stake currency. */ + staking: boolean; + features: string[]; + testnet: boolean; +} + +const cur = (coinDenom: string, coinMinimalDenom: string, coinDecimals: number): CosmosCurrency => ({ coinDenom, coinMinimalDenom, coinDecimals }); +const OSMO = cur("OSMO", "uosmo", 6); +const ZIG = cur("ZIG", "azig", 18); +const HASH = cur("HASH", "nhash", 9); +const INIT = cur("INIT", "uinit", 6); +const DYDX_USDC = "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5"; + +export const COSMOS_CHAIN_FACTS: readonly CosmosChainFacts[] = [ + { chainId: "osmo-test-5", family: "cosmos", chainName: "Osmosis Testnet", prefix: "osmo", coinType: 118, algo: "secp256k1", native: OSMO, gas: { low: 0.0025, average: 0.025, high: 0.04 }, usdc: "ibc/DE6792CF9E521F6AD6E9A4BDF6225C9571A3B74ACC0A529F92BC5122A39D2E58", staking: true, features: ["cosmwasm", "osmosis-txfees"], testnet: true }, + { chainId: "osmosis-1", family: "cosmos", chainName: "Osmosis", prefix: "osmo", coinType: 118, algo: "secp256k1", native: OSMO, gas: { low: 0.03, average: 0.1, high: 0.16 }, usdc: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", staking: true, features: ["cosmwasm", "osmosis-txfees"], testnet: false }, + { chainId: "dydx-testnet-4", family: "cosmos", chainName: "dYdX Testnet", prefix: "dydx", coinType: 118, algo: "secp256k1", native: cur("DV4TNT", "adv4tnt", 18), gas: { low: 25000000000, average: 25000000000, high: 50000000000 }, usdc: DYDX_USDC, staking: true, features: [], testnet: true }, + { chainId: "dydx-mainnet-1", family: "cosmos", chainName: "dYdX", prefix: "dydx", coinType: 118, algo: "secp256k1", native: cur("DYDX", "adydx", 18), gas: { low: 12500000000, average: 12500000000, high: 20000000000 }, usdc: DYDX_USDC, staking: true, features: [], testnet: false }, + { chainId: "zig-test-2", family: "cosmos", chainName: "ZIGChain Testnet", prefix: "zig", coinType: 118, algo: "secp256k1", native: ZIG, gas: { low: 2500000000, average: 25000000000, high: 50000000000 }, staking: true, features: ["cosmwasm"], testnet: true }, + { chainId: "zigchain-1", family: "cosmos", chainName: "ZIGChain", prefix: "zig", coinType: 118, algo: "secp256k1", native: ZIG, gas: { low: 2500000000, average: 25000000000, high: 50000000000 }, staking: true, features: ["cosmwasm"], testnet: false }, + { chainId: "pio-testnet-1", family: "provenance", chainName: "Provenance Testnet", prefix: "tp", coinType: 505, algo: "secp256k1", native: HASH, gas: { low: 1, average: 1, high: 1 }, staking: true, features: ["cosmwasm"], testnet: true }, + { chainId: "pio-mainnet-1", family: "provenance", chainName: "Provenance", prefix: "pb", coinType: 505, algo: "secp256k1", native: HASH, gas: { low: 1, average: 1, high: 1 }, staking: true, features: ["cosmwasm"], testnet: false }, + { chainId: "thorchain-1", family: "thorchain", chainName: "THORChain", prefix: "thor", coinType: 931, algo: "secp256k1", native: cur("RUNE", "rune", 8), gas: { low: 0, average: 0, high: 0 }, staking: false, features: [], testnet: false }, + { chainId: "initiation-2", family: "initia", chainName: "Initia Testnet", prefix: "init", coinType: 60, algo: "ethsecp256k1", native: INIT, gas: { low: 0.015, average: 0.015, high: 0.04 }, staking: true, features: ["eth-address-gen", "eth-key-sign", "eth-secp256k1-initia"], testnet: true }, + { chainId: "interwoven-1", family: "initia", chainName: "Initia", prefix: "init", coinType: 60, algo: "ethsecp256k1", native: INIT, gas: { low: 0.015, average: 0.015, high: 0.04 }, staking: true, features: ["eth-address-gen", "eth-key-sign", "eth-secp256k1-initia"], testnet: false }, +]; + +export function cosmosChainOf(chainId: string): CosmosChainFacts | undefined { + return COSMOS_CHAIN_FACTS.find((c) => c.chainId === chainId); +} + +export function chainInfoWithoutEndpoints(chainId: string): CosmosChainInfoWithoutEndpoints | undefined { + const c = cosmosChainOf(chainId); + if (!c) return undefined; + const p = c.prefix; + const usdc = c.usdc ? [cur("USDC", c.usdc, 6)] : []; + return { + rest: undefined, + rpc: undefined, + nodeProvider: undefined, + chainId: c.chainId, + chainName: c.chainName, + ...(c.staking ? { stakeCurrency: c.native } : {}), + bip44: { coinType: c.coinType }, + bech32Config: { + bech32PrefixAccAddr: p, + bech32PrefixAccPub: `${p}pub`, + bech32PrefixValAddr: `${p}valoper`, + bech32PrefixValPub: `${p}valoperpub`, + bech32PrefixConsAddr: `${p}valcons`, + bech32PrefixConsPub: `${p}valconspub`, + }, + currencies: [c.native, ...usdc], + feeCurrencies: [{ ...c.native, gasPriceStep: c.gas }], + features: [...c.features], + isTestnet: c.testnet, + }; +} + +/* ------------------------------------------------------------------ base64 without Buffer (page bundle) */ + +export function b64FromBytes(bytes: Uint8Array): string { + let s = ""; + for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + return btoa(s); +} + +export function bytesFromB64(b64: string): Uint8Array { + const s = atob(b64); + const out = new Uint8Array(s.length); + for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); + return out; +} diff --git a/packages/1mask/src/shared/fuel.ts b/packages/1mask/src/shared/fuel.ts new file mode 100644 index 00000000..17ad8324 --- /dev/null +++ b/packages/1mask/src/shared/fuel.ts @@ -0,0 +1,40 @@ +import type { Network, NetworkId } from "@clip-wallet/core"; +import { METHOD_WS_STATE } from "./protocol.js"; + +/** + * Wire methods of the Fuel connector (inpage/fuel.ts ↔ background/fuel.ts). The signing names are chains-fuel's own + * DappRequest methods (FUEL_METHODS), so the background hands them to the module unchanged. + */ +export const FUEL_INJECTED = { + /** Silent account read (no prompt). */ + accounts: METHOD_WS_STATE, + connect: "fuel:connect", + disconnect: "fuel:disconnect", + currentNetwork: "fuel:currentNetwork", + networks: "fuel:networks", + selectNetwork: "fuel:selectNetwork", + sendTransaction: "fuel_sendTransaction", + signTransaction: "fuel_signTransaction", + signMessage: "fuel_signMessage", +} as const; + +export const FUEL_CONNECT_METHODS = [FUEL_INJECTED.connect] as const; + +/** The Fuel connector's network shape (fuels-ts connectors/types `Network`). */ +export interface FuelConnectorNetwork { + url: string; + chainId: number; +} + +/** "fuel:" (chains-fuel networks) → the connector's numeric chain id. */ +export function fuelChainId(networkId: NetworkId): number | undefined { + const m = /^fuel:(\d+)$/.exec(networkId); + return m ? Number(m[1]) : undefined; +} + +export function fuelConnectorNetwork(net: Network): FuelConnectorNetwork { + return { url: net.rpcUrls[0] ?? "", chainId: fuelChainId(net.id) ?? -1 }; +} + +/** Message forms on the wire (chains-fuel `FuelMessage`): a plain string, personalSign text, or personalSign bytes. */ +export type FuelWireMessage = { text: string } | { personalSign: string } | { personalSignHex: string }; diff --git a/packages/1mask/src/shared/multiversx-methods.ts b/packages/1mask/src/shared/multiversx-methods.ts new file mode 100644 index 00000000..1b3f3812 --- /dev/null +++ b/packages/1mask/src/shared/multiversx-methods.ts @@ -0,0 +1,42 @@ +import { METHOD_WS_STATE } from "./protocol.js"; + +/** + * Wire methods of the MultiversX provider (inpage/multiversx.ts ↔ background/multiversx.ts). The signing methods are + * chains-multiversx's own DappRequest methods (MultiversX's WalletConnect method names), handed to the module + * unchanged: + * - mvx_signTransactions { transactions: sdk-core plain objects, address? } → { signatures: [{ signature }], transactions } + * - mvx_signMessage { message: UTF-8 text, address } → { signature: hex, address } + */ +export const MULTIVERSX_INJECTED = { + accounts: METHOD_WS_STATE, + connect: "mvx:connect", + disconnect: "mvx:disconnect", + signTransactions: "mvx_signTransactions", + signMessage: "mvx_signMessage", +} as const; + +/** "mvx:" + the transaction chainID ("1", "D", "T"): the ChainAgnostic / WalletConnect id chains-multiversx uses. */ +export function multiversxNetworkIdOf(chainID: unknown): string | null { + return typeof chainID === "string" && /^[0-9A-Za-z]{1,8}$/.test(chainID) ? `mvx:${chainID}` : null; +} + +const fromB64Url = (s: string): string | null => { + try { + const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4)); + return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(b, (c) => c.charCodeAt(0))); + } catch { + return null; + } +}; + +/** + * The site a MultiversX native-auth login token is for, or null when `token` isn't one. Token format + * (sdk-native-auth-client `initialize`): base64url(origin) "." blockHash "." ttl "." base64url(extraInfo JSON). + * https://github.com/multiversx/mx-sdk-js-native-auth-client/blob/main/src/native.auth.client.ts + */ +export function nativeAuthOrigin(token: string): string | null { + const parts = token.split("."); + if (parts.length !== 4 || !/^[0-9a-f]{64}$/i.test(parts[1]!) || !/^\d{1,10}$/.test(parts[2]!)) return null; + const origin = fromB64Url(parts[0]!); + return origin && origin.length <= 512 ? origin : null; +} diff --git a/packages/1mask/src/shared/protocol.ts b/packages/1mask/src/shared/protocol.ts index e11f91b8..e59d5487 100644 --- a/packages/1mask/src/shared/protocol.ts +++ b/packages/1mask/src/shared/protocol.ts @@ -15,6 +15,7 @@ export const DEFAULT_CHANNEL = "clip-wallet-1mask"; export const FAMILIES = [ "evm", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", + "cosmos", "provenance", "thorchain", "initia", "tron", "xrpl", "antelope", "multiversx", "icp", "stacks", "fuel", "bitcoincash", ] as const satisfies readonly Family[]; export const familySchema = z.enum(FAMILIES); diff --git a/packages/1mask/src/shared/stacks.ts b/packages/1mask/src/shared/stacks.ts new file mode 100644 index 00000000..2e32db15 --- /dev/null +++ b/packages/1mask/src/shared/stacks.ts @@ -0,0 +1,115 @@ +import type { NetworkId } from "@clip-wallet/core"; +import { METHOD_WS_STATE } from "./protocol.js"; + +/** + * Stacks wire methods for 1Mask. The signing methods are SIP-030's own names (= chains-stacks STACKS_METHODS), so + * the background hands them to the module unchanged. Sources: + * - SIP-030 (Recommended) https://github.com/stacksgov/sips/blob/main/sips/sip-030/sip-030-wallet-interface.md + * - @stacks/connect v8 `request()` / `MethodParams` https://github.com/stx-labs/connect/blob/main/packages/connect/src/methods.ts + */ +export const STACKS_INJECTED = { + accounts: METHOD_WS_STATE, + /** "Connect": SIP-030 stx_getAddresses (and @stacks/connect's getAddresses) prompt on first use. */ + connect: "stacks:connect", + disconnect: "stacks:disconnect", + /** SIP-030 stx_getNetworks (answered by the background, no prompt). */ + getNetworks: "stx_getNetworks", + transferStx: "stx_transferStx", + transferSip10Ft: "stx_transferSip10Ft", + transferSip9Nft: "stx_transferSip9Nft", + callContract: "stx_callContract", + deployContract: "stx_deployContract", + signTransaction: "stx_signTransaction", + signMessage: "stx_signMessage", + signStructuredMessage: "stx_signStructuredMessage", +} as const; + +export const STACKS_SIGNING_METHODS: readonly string[] = [ + STACKS_INJECTED.transferStx, + STACKS_INJECTED.transferSip10Ft, + STACKS_INJECTED.transferSip9Nft, + STACKS_INJECTED.callContract, + STACKS_INJECTED.deployContract, + STACKS_INJECTED.signTransaction, + STACKS_INJECTED.signMessage, + STACKS_INJECTED.signStructuredMessage, +]; + +/** ChainAgnostic stacks namespace (CAIP-2 = "stacks:" + chain id) ↔ SIP-030 network names. */ +export const STACKS_CHAINS: Record<"mainnet" | "testnet", { caip2: NetworkId; chainId: number; transactionVersion: number }> = { + mainnet: { caip2: "stacks:1", chainId: 1, transactionVersion: 0x00 }, + testnet: { caip2: "stacks:2147483648", chainId: 0x80000000, transactionVersion: 0x80 }, +}; + +export function stacksNetworkName(networkId: NetworkId): "mainnet" | "testnet" | undefined { + return networkId === STACKS_CHAINS.mainnet.caip2 ? "mainnet" : networkId === STACKS_CHAINS.testnet.caip2 ? "testnet" : undefined; +} + +/** SIP-030 `network` param ("mainnet" | "testnet" | CAIP-2) → CAIP-2 chain hint; undefined for anything else. */ +export function stacksChainHint(network: unknown): NetworkId | undefined { + if (typeof network !== "string") return undefined; + const n = network.trim().toLowerCase(); + if (n === "mainnet" || n === STACKS_CHAINS.mainnet.caip2) return STACKS_CHAINS.mainnet.caip2; + if (n === "testnet" || n === STACKS_CHAINS.testnet.caip2) return STACKS_CHAINS.testnet.caip2; + return undefined; +} + +/* ------------------------------------------------------------------ c32check re-spelling (WebCrypto only) */ + +const C32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +/** Single-sig versions: 22 "SP" mainnet, 26 "ST" testnet; multisig 20 "SM" / 21 "SN". */ +const SINGLE = { mainnet: 22, testnet: 26 } as const; +const MULTI = { mainnet: 20, testnet: 21 } as const; + +function c32encode(data: Uint8Array): string { + let n = 0n; + for (const b of data) n = (n << 8n) | BigInt(b); + let out = ""; + while (n > 0n) { + out = C32[Number(n & 31n)]! + out; + n >>= 5n; + } + let zeros = 0; + while (zeros < data.length && data[zeros] === 0) zeros++; + return "0".repeat(zeros) + out; +} + +function c32decode24(text: string): Uint8Array | undefined { + let n = 0n; + for (const ch of text) { + const v = C32.indexOf(ch); + if (v < 0) return undefined; + n = (n << 5n) | BigInt(v); + } + const out = new Uint8Array(24); + for (let i = 23; i >= 0; i--) { + out[i] = Number(n & 0xffn); + n >>= 8n; + } + return n === 0n ? out : undefined; +} + +async function checksum(version: number, h: Uint8Array): Promise { + const d1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([version, ...h]))); + return new Uint8Array(await crypto.subtle.digest("SHA-256", d1)).subarray(0, 4); +} + +/** + * The same account's address spelled for `networkId` (Account.address is the mainnet "SP…" form; testnet is + * "ST…": same hash160, version 26). Verifies the input's c32check checksum; undefined when it isn't a Stacks address. + */ +export async function stacksAddressOn(address: string, networkId: NetworkId): Promise { + const net = stacksNetworkName(networkId); + const a = address.trim().toUpperCase(); + if (!net || !/^S[0-9A-Z]{3,42}$/.test(a)) return undefined; + const version = C32.indexOf(a[1]!); + const body = c32decode24(a.slice(2)); + if (!body || version < 0) return undefined; + const h = body.subarray(0, 20); + const sum = await checksum(version, h); + if (sum.some((b, i) => b !== body[20 + i])) return undefined; + const multi = version === MULTI.mainnet || version === MULTI.testnet; + const target = multi ? MULTI[net] : SINGLE[net]; + if (!multi && version !== SINGLE.mainnet && version !== SINGLE.testnet) return undefined; + return "S" + C32[target] + c32encode(new Uint8Array([...h, ...(await checksum(target, h))])); +} diff --git a/packages/1mask/src/shared/tron-methods.ts b/packages/1mask/src/shared/tron-methods.ts new file mode 100644 index 00000000..6b070aba --- /dev/null +++ b/packages/1mask/src/shared/tron-methods.ts @@ -0,0 +1,38 @@ +import type { NetworkId } from "@clip-wallet/core"; +import { METHOD_WS_STATE } from "./protocol.js"; + +/** + * Wire methods of the injected TRON provider (inpage/tron.ts ↔ background/tron.ts). The signing methods are + * chains-tron's own DappRequest methods (the WalletConnect / Reown TRON method names), so the background hands them + * to the module unchanged: + * - tron_signTransaction { address, transaction } → the TronWeb transaction with `signature: [hex]` + * - tron_signMessage { address, message, encoding?: "hex" } → { signature: "0x…" } (TronWeb signMessageV2 format) + */ +export const TRON_INJECTED = { + accounts: METHOD_WS_STATE, + connect: "tron:connect", + disconnect: "tron:disconnect", + signTransaction: "tron_signTransaction", + signMessage: "tron_signMessage", +} as const; + +/** TRON chain ids as `eth_chainId` / TIP-1193 report them ("0x" + last 4 bytes of the genesis block id). */ +export const TRON_CHAIN_IDS = { + mainnet: "0x2b6653dc", + shasta: "0x94a9059e", + nile: "0xcd8690dc", +} as const; + +/** CAIP-2 id (WalletConnect / Reown form, "tron:0x…") for a TIP-1193 chain id. */ +export function tronNetworkIdOf(chainId: string): NetworkId | null { + const c = chainId.trim().toLowerCase(); + if (/^0x[0-9a-f]{1,8}$/.test(c)) return `tron:0x${c.slice(2).padStart(8, "0")}`; + if (/^\d{1,10}$/.test(c)) return `tron:0x${Number(c).toString(16).padStart(8, "0")}`; + return null; +} + +/** TIP-1193 chain id ("0x…") of a CAIP-2 TRON network id. */ +export function tronChainIdOf(networkId: NetworkId): string | null { + const m = /^tron:(0x[0-9a-f]{8})$/i.exec(networkId); + return m ? m[1]!.toLowerCase() : null; +} diff --git a/packages/1mask/src/shared/xrpl.ts b/packages/1mask/src/shared/xrpl.ts new file mode 100644 index 00000000..ae7953fd --- /dev/null +++ b/packages/1mask/src/shared/xrpl.ts @@ -0,0 +1,51 @@ +/** + * XRP Ledger wire names for 1Mask, plain strings so the background never imports the in-page wallet. + * + * The standard is XLS-72d "Browser Wallet Standard" (https://github.com/XRPLF/XRPL-Standards/discussions/206, by + * tequ; the discussion was closed for inactivity in March 2026 without being replaced), built on Wallet Standard + * (https://github.com/wallet-standard/wallet-standard). Its reference types are @xrpl-wallet-standard/core 0.1.4 + * (https://github.com/tequdev/xrpl-wallet-standard, packages/core/src/features/*.ts, networks.ts, utils.ts): + * - features `xrpl:signTransaction` 1.0.0 ({ tx_json, account, network, options? } → { signed_tx_blob }) and + * `xrpl:signAndSubmitTransaction` 1.0.0 (→ { tx_hash, tx_json }), plus standard:connect / standard:events + * (REQUIRED_FEATURES) and standard:disconnect; + * - chains `xrpl:` (`xrpl:0` mainnet, `xrpl:1` testnet, `xrpl:2` devnet), with the aliases + * `xrpl:mainnet` / `xrpl:testnet` / `xrpl:devnet` accepted as input. + * There is no message-signing feature in XLS-72d, so 1Mask has none for the XRPL. + */ +import { METHOD_WS_STATE } from "./protocol.js"; + +export const XRPL_SIGN_TRANSACTION = "xrpl:signTransaction" as const; +export const XRPL_SIGN_AND_SUBMIT_TRANSACTION = "xrpl:signAndSubmitTransaction" as const; + +export const XRPL_INJECTED = { + /** Silent account read (no prompt). */ + state: METHOD_WS_STATE, + connect: "standard:connect", + disconnect: "standard:disconnect", + signTransaction: XRPL_SIGN_TRANSACTION, + signAndSubmitTransaction: XRPL_SIGN_AND_SUBMIT_TRANSACTION, +} as const; + +export const XRPL_METHODS_ALLOWED = { + local: [XRPL_INJECTED.state, XRPL_INJECTED.disconnect], + connect: [XRPL_INJECTED.connect], + signing: [XRPL_INJECTED.signTransaction, XRPL_INJECTED.signAndSubmitTransaction], +} as const; + +/** For methods.ts `injectedAllowlist("xrpl")`. */ +export function xrplAllowlist(): ReadonlySet { + return new Set([...XRPL_METHODS_ALLOWED.local, ...XRPL_METHODS_ALLOWED.connect, ...XRPL_METHODS_ALLOWED.signing]); +} + +const ALIASES: Record = { "xrpl:mainnet": "xrpl:0", "xrpl:testnet": "xrpl:1", "xrpl:devnet": "xrpl:2" }; + +/** + * An XLS-72d network identifier as the CAIP-2 id the registry uses ("xrpl:testnet" → "xrpl:1"), or undefined when it + * isn't an XRPL identifier at all. Xahau's `xrpl:21337` / `xrpl:21338` pass through and are refused later as networks + * this wallet doesn't have. + */ +export function xrplChainId(network: unknown): string | undefined { + if (typeof network !== "string") return undefined; + if (ALIASES[network]) return ALIASES[network]; + return /^xrpl:\d{1,10}$/.test(network) ? network : undefined; +} diff --git a/packages/1mask/src/walletconnect/namespaces.ts b/packages/1mask/src/walletconnect/namespaces.ts index 7ea02bc2..724bca8b 100644 --- a/packages/1mask/src/walletconnect/namespaces.ts +++ b/packages/1mask/src/walletconnect/namespaces.ts @@ -1,5 +1,6 @@ import type { Family, Network } from "@clip-wallet/core"; import { HEDERA_METHODS } from "../background/methods.js"; +import { BCH_WC, bchAddressOn, bchNetworkForWcChain } from "../shared/bitcoincash.js"; import { P2_WC_NAMESPACE_FAMILY, P2_WC_SUPPORTED_EVENTS, P2_WC_SUPPORTED_METHODS, type P2WcNamespaceKey } from "./p2-namespaces.js"; /** @@ -14,13 +15,14 @@ import { P2_WC_NAMESPACE_FAMILY, P2_WC_SUPPORTED_EVENTS, P2_WC_SUPPORTED_METHODS * - Optional methods/events: intersection with what we support. */ -export type WcNamespaceKey = "eip155" | "solana" | "bip122" | "hedera" | P2WcNamespaceKey; +export type WcNamespaceKey = "eip155" | "solana" | "bip122" | "hedera" | "bch" | P2WcNamespaceKey; export const WC_NAMESPACE_FAMILY: Record = { eip155: "evm", solana: "solana", bip122: "bitcoin", hedera: "hedera", + bch: "bitcoincash", ...P2_WC_NAMESPACE_FAMILY, }; @@ -49,6 +51,8 @@ export const WC_SUPPORTED_METHODS: Record = { bip122: ["getAccountAddresses", "signMessage", "signPsbt", "sendTransfer"], // hashgraph/hedera-wallet-connect HederaJsonRpcMethod hedera: HEDERA_METHODS, + // wc2-bch-bcr (https://github.com/mainnet-pat/wc2-bch-bcr#pairing): Cashonize, Paytaca, Zapit + bch: [BCH_WC.getAddresses, BCH_WC.signTransaction, BCH_WC.signMessage], ...P2_WC_SUPPORTED_METHODS, }; @@ -58,9 +62,30 @@ export const WC_SUPPORTED_EVENTS: Record = { bip122: ["bip122_addressesChanged"], // hashgraph/hedera-wallet-connect HederaSessionEvent hedera: ["accountsChanged", "chainChanged"], + bch: [...BCH_WC.events], ...P2_WC_SUPPORTED_EVENTS, }; +/** + * Chain aliases: a WalletConnect chain id whose wallet network has another id. Only wc2-bch-bcr needs one: its chains + * are "bch:bitcoincash" / "bch:bchtest", the wallet's Bitcoin Cash networks are bip122 ids (chains-bitcoincash). Every + * other namespace's chain id IS the network id. + */ +export function networkIdForWcChain(chain: string): string { + return namespaceOf(chain) === "bch" ? (bchNetworkForWcChain(chain) ?? chain) : chain; +} + +/** + * CAIP-10 session account for `address` on WC `chain`. wc2-bch-bcr wallets put the CashAddr after "bch:" (Cashonize + * src/stores/walletconnectStore.ts: `bch:${addr}` → "bch:bchtest:qq…"), i.e. the chain id plus the CashAddr payload, + * spelled for that chain's prefix. + */ +export function wcAccount(chain: string, address: string): string { + if (namespaceOf(chain) !== "bch") return `${chain}:${address}`; + const spelled = bchAddressOn(address, networkIdForWcChain(chain)) ?? address; + return `${chain}:${spelled.slice(spelled.indexOf(":") + 1)}`; +} + export interface ProposalNamespace { chains?: string[]; methods: string[]; @@ -134,8 +159,9 @@ export function mapProposalNamespaces(proposal: ProposalLike, input: NamespaceMa const registry = new Set(input.networks.map((n) => n.id)); const servable = (chain: string, key: WcNamespaceKey): string[] | undefined => { - if (!registry.has(chain)) return undefined; - const addrs = input.addressesFor(chain, WC_NAMESPACE_FAMILY[key]); + const networkId = networkIdForWcChain(chain); + if (!registry.has(networkId)) return undefined; + const addrs = input.addressesFor(networkId, WC_NAMESPACE_FAMILY[key]); return addrs.length > 0 ? addrs : undefined; }; @@ -179,7 +205,7 @@ export function mapProposalNamespaces(proposal: ProposalLike, input: NamespaceMa continue; } chains.push(chain); - for (const a of addrs) accounts.push(`${chain}:${a}`); + for (const a of addrs) accounts.push(wcAccount(chain, a)); } if (chains.length === 0) continue; diff --git a/packages/1mask/src/walletconnect/wallet.ts b/packages/1mask/src/walletconnect/wallet.ts index 7c12156e..f1064d9d 100644 --- a/packages/1mask/src/walletconnect/wallet.ts +++ b/packages/1mask/src/walletconnect/wallet.ts @@ -8,6 +8,8 @@ import { isServedMethod, mapProposalNamespaces, namespaceOf, + networkIdForWcChain, + wcAccount, type NamespaceMapping, type ProposalNamespace, type SessionNamespace, @@ -18,6 +20,7 @@ import { assessVerify, type Verification, type VerifyContextLike } from "./verif import { namedAccounts, sessionHasAccount } from "./accounts.js"; import { CALLS_METHODS, CallsErrorCode, callsError, chainCapabilities, isCallsMethod, parseSendCalls, type CallsHost, type Hex, type SendCallsParams } from "../shared/calls.js"; import { rpcError } from "../shared/errors.js"; +import { BCH_WC, bchWcChainFor } from "../shared/bitcoincash.js"; /* ------------------------------------------------------------------ SDK error codes */ @@ -220,7 +223,9 @@ export async function createWalletConnectWallet(opts: WalletConnectWalletOptions metadata: opts.metadata, ...(opts.coreOptions ? { coreOptions: opts.coreOptions } : {}), }); - const familyFor = opts.familyForChain ?? defaultFamilyForChain; + const baseFamilyFor = opts.familyForChain ?? defaultFamilyForChain; + /** Chain aliases (wc2-bch-bcr): a Bitcoin Cash bip122 network id is "bitcoincash", never the bip122 namespace's "bitcoin". */ + const familyFor = (chain: string): Family => (bchWcChainFor(chain) ? "bitcoincash" : baseFamilyFor(chain)); const newId = opts.newId ?? randomId; const registry = new Set(opts.networks.map((n) => n.id)); const inflight = new Map(); @@ -229,7 +234,7 @@ export async function createWalletConnectWallet(opts: WalletConnectWalletOptions const mapProposal: WalletConnectWallet["mapProposal"] = (p) => mapProposalNamespaces(p, { networks: opts.networks, - addressesFor: (chain) => opts.addressesFor(chain, familyFor(chain)), + addressesFor: (networkId) => opts.addressesFor(networkId, familyFor(networkId)), ...(extraMethods ? { extraMethods } : {}), }); @@ -255,7 +260,7 @@ export async function createWalletConnectWallet(opts: WalletConnectWalletOptions const approvedChains = Object.values(mapping.namespaces).flatMap((n) => n.chains); // Audit WC-04: the connect screen names one network and address; say plainly when the app also gets the // user's addresses of other kinds of account (other namespaces have other addresses). - const names = [...new Set(approvedChains.map((c) => opts.networks.find((n) => n.id === c)?.name ?? c))]; + const names = [...new Set(approvedChains.map((c) => opts.networks.find((n) => n.id === networkIdForWcChain(c))?.name ?? c))]; const shared: Warning[] = Object.keys(mapping.namespaces).length > 1 ? [{ level: "info", code: "network-matters", message: `This app gets your addresses on ${names.length} networks: ${names.join(", ")}.` }] @@ -323,6 +328,10 @@ export async function createWalletConnectWallet(opts: WalletConnectWalletOptions if (namespaceOf(chainId) === "near" && method === "near_getAccounts") { return { result: sessionAccounts(session, chainId).map((accountId) => ({ accountId })) }; } + if (namespaceOf(chainId) === "bch" && method === BCH_WC.getAddresses) { + // wc2-bch-bcr: full CashAddrs ("bchtest:qq…"); the session account is "bch:bchtest:qq…". + return { result: sessionAccounts(session, chainId).map((payload) => `${chainId.slice("bch:".length)}:${payload}`) }; + } return undefined; }; @@ -389,7 +398,7 @@ export async function createWalletConnectWallet(opts: WalletConnectWalletOptions if (!session) throw { code: RpcErrorCode.Unauthorized, message: "Unknown session." }; const nsKey = namespaceOf(chainId); const ns = session.namespaces[nsKey]; - if (!ns || !ns.chains.includes(chainId) || !registry.has(chainId)) { + if (!ns || !ns.chains.includes(chainId) || !registry.has(networkIdForWcChain(chainId))) { throw { code: RpcErrorCode.ChainDisconnected, message: `Clip Wallet is not connected to ${chainId}.` }; } if (!ns.methods.includes(request.method)) { @@ -415,7 +424,7 @@ export async function createWalletConnectWallet(opts: WalletConnectWalletOptions const verify = assessVerify(ev.verifyContext, peer.url, opts.isKnownScam); let method = request.method; let reqParams = request.params; - let networkId = chainId; + let networkId = networkIdForWcChain(chainId); if (opts.calls && nsKey === "eip155" && isCallsMethod(method)) { const answered = await answerCalls(session, chainId, verify.origin, method, request.params); if ("result" in answered) return void (await respond(topic, id, answered)); @@ -518,15 +527,17 @@ export async function createWalletConnectWallet(opts: WalletConnectWalletOptions for (const s of Object.values(kit.getActiveSessions())) { const namespaces: Record = {}; for (const [key, ns] of Object.entries(s.namespaces)) { - const accounts = ns.chains.flatMap((c) => opts.addressesFor(c, familyFor(c)).map((a) => `${c}:${a}`)); + const accounts = ns.chains.flatMap((c) => opts.addressesFor(networkIdForWcChain(c), familyFor(networkIdForWcChain(c))).map((a) => wcAccount(c, a))); namespaces[key] = { ...ns, accounts }; } await kit.updateSession({ topic: s.topic, namespaces }); for (const [key, ns] of Object.entries(namespaces)) { - const event = key === "bip122" ? "bip122_addressesChanged" : "accountsChanged"; + const event = key === "bip122" ? "bip122_addressesChanged" : key === "bch" ? "addressesChanged" : "accountsChanged"; if (!ns.events.includes(event)) continue; for (const c of ns.chains) { - const data = ns.accounts.filter((a) => a.startsWith(`${c}:`)).map((a) => (key === "eip155" ? a.slice(c.length + 1) : a)); + const data = ns.accounts + .filter((a) => a.startsWith(`${c}:`)) + .map((a) => (key === "eip155" ? a.slice(c.length + 1) : key === "bch" ? `${c.slice("bch:".length)}:${a.slice(c.length + 1)}` : a)); await kit.emitSessionEvent({ topic: s.topic, event: { name: event, data }, chainId: c }); } } diff --git a/packages/1mask/test/bitcoincash-wc.test.ts b/packages/1mask/test/bitcoincash-wc.test.ts new file mode 100644 index 00000000..b2bd74ff --- /dev/null +++ b/packages/1mask/test/bitcoincash-wc.test.ts @@ -0,0 +1,46 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { createBitcoinCashDispatcher } from "../src/background/bitcoincash.js"; +import { rpcError } from "../src/shared/errors.js"; +import { bchAddressOn, bchNetworkForWcChain, bchWcChainFor } from "../src/shared/bitcoincash.js"; + +/** The public "abandon … about" BCH account 0 (chains-bitcoincash test/signatures.ts). */ +const MAIN = "bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6"; +const TEST = "bchtest:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnqseeszx8x"; +const asset = (networkId: string) => ({ key: "bch", symbol: "BCH", name: "Bitcoin Cash", decimals: 8, networkId }); +const CHIPNET: Network = { id: "bip122:00000000040ba9641ba98a37b2e5ceea", family: "bitcoincash", name: "Chipnet", nativeAsset: asset("x"), testnet: true, rpcUrls: [], explorerUrl: "" }; +const MAINNET: Network = { id: "bip122:000000000000000000651ef99cb9fcbe", family: "bitcoincash", name: "Bitcoin Cash", nativeAsset: asset("y"), testnet: false, rpcUrls: [], explorerUrl: "" }; + +describe("Bitcoin Cash over WalletConnect (wc2-bch-bcr)", () => { + it("re-spells CashAddr per network and maps the spec's chain ids", () => { + expect(bchAddressOn(MAIN, CHIPNET.id)).toBe(TEST); + expect(bchAddressOn(TEST, MAINNET.id)).toBe(MAIN); + expect(bchAddressOn(MAIN.slice(0, -1) + "7", CHIPNET.id)).toBeUndefined(); + expect(bchNetworkForWcChain("bch:bchtest")).toBe(CHIPNET.id); + expect(bchNetworkForWcChain("bch:bitcoincash")).toBe(MAINNET.id); + expect(bchNetworkForWcChain("bch:bchreg")).toBeUndefined(); + expect(bchWcChainFor(CHIPNET.id)).toBe("bch:bchtest"); + }); + + it("answers bch_getAddresses for the session's network and passes signing through after permission", async () => { + let permitted = false; + const approved: DappRequest[] = []; + const d = createBitcoinCashDispatcher({ + permitted: async () => permitted, + requirePermission: async () => { + if (!permitted) throw rpcError.unauthorized(); + }, + accounts: async () => [{ address: MAIN }], + approve: async (req) => (approved.push(req), { signedTransaction: "00", signedTransactionHash: "11" }), + makeReq: (origin: string, family: Family, net: Network, method: string, params: unknown) => ({ id: "1", origin, via: "walletconnect" as const, family, networkId: net.id, method, params }), + requireNetwork: (_f, _o, chain) => (chain === MAINNET.id ? MAINNET : CHIPNET), + }); + await expect(d.dispatch("https://app", "bitcoincash", "bch_getAddresses", {}, "bch:bchtest")).rejects.toMatchObject({ code: 4100 }); + permitted = true; + expect(await d.dispatch("https://app", "bitcoincash", "bch_getAddresses", {}, "bch:bchtest")).toEqual([TEST]); + expect(await d.dispatch("https://app", "bitcoincash", "bch_getAddresses", {}, "bch:bitcoincash")).toEqual([MAIN]); + await d.dispatch("https://app", "bitcoincash", "bch_signMessage", { message: "hi" }, "bch:bchtest"); + expect(approved[0]).toMatchObject({ family: "bitcoincash", networkId: CHIPNET.id, method: "bch_signMessage" }); + await expect(d.dispatch("https://app", "bitcoincash", "bch_sendTransaction", {}, "bch:bchtest")).rejects.toMatchObject({ code: 4200 }); + }); +}); diff --git a/packages/1mask/test/content.test.ts b/packages/1mask/test/content.test.ts index 1b6cf3bd..0ca300a3 100644 --- a/packages/1mask/test/content.test.ts +++ b/packages/1mask/test/content.test.ts @@ -79,7 +79,7 @@ describe("content bridge security", () => { it("rejects schema-invalid requests (bad family, missing method)", async () => { const s = setup(); - s.win.postMessage(s.req({ family: "cosmos" }), "*"); + s.win.postMessage(s.req({ family: "dogecoin" }), "*"); s.win.postMessage(s.req({ id: "r2", method: "" }), "*"); await vi.waitFor(() => expect(s.toPage.map((m) => m.error?.code)).toEqual([-32602, -32602])); expect(s.received).toEqual([]); diff --git a/packages/1mask/test/cosmos.test.ts b/packages/1mask/test/cosmos.test.ts new file mode 100644 index 00000000..e38d6e00 --- /dev/null +++ b/packages/1mask/test/cosmos.test.ts @@ -0,0 +1,233 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { COSMOS_METHODS_ALLOWED, cosmosInjectedAllowlist, createCosmosDispatcher, type CosmosRouterInternals } from "../src/background/cosmos.js"; +import { installCosmosProvider } from "../src/inpage/cosmos.js"; +import type { InpageTransport } from "../src/inpage/transport.js"; +import { DEFAULT_IDENTITY } from "../src/shared/config.js"; +import { COSMOS_CHAIN_FACTS, COSMOS_INJECTED, chainInfoWithoutEndpoints } from "../src/shared/cosmos.js"; +import { bech32Decode, bech32Encode, hexOf } from "../src/shared/cosmos-bech32.js"; +import { fromRpcErrorShape, toRpcErrorShape } from "../src/shared/errors.js"; +import type { ExposedAccount } from "../src/shared/protocol.js"; +import { newWindow } from "./helpers.js"; + +/** + * The Keplr-compatible provider (window.clipwallet.cosmos) over the background dispatcher, with the router's + * internals faked: permissions, accounts, approvals and reads are recorded. Account values are the public + * "abandon … about" account 0 of each family (addresses and public keys only). + */ + +const ORIGIN = "https://app.osmosis.example"; +const asset = (networkId: string) => ({ key: "x", symbol: "X", name: "X", decimals: 6, networkId }); +const NETS: Network[] = COSMOS_CHAIN_FACTS.map((c) => ({ id: `cosmos:${c.chainId}`, family: c.family, name: c.chainName, nativeAsset: asset(`cosmos:${c.chainId}`), testnet: c.testnet, rpcUrls: [], explorerUrl: "" })); + +const ACCOUNTS: Partial> = { + cosmos: [{ address: "cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4", publicKey: "024f4e2ad99c34d60b9ba6283c9431a8418af8673212961f97a77b6377fcd05b62" }], + provenance: [{ address: "pb1fpwxdyscnu2dzjwghpstx7l9xtvsap48l5gnrm", publicKey: "02b46c78777309c65fb5c2308574fb9a76853d7c0e91d1a1bd1f79f6ccb64ef6da" }], + thorchain: [{ address: "thor1gm00vwsfcp48enm4uv9e5dhm37jtd0ye27wrx0", publicKey: "02205c476a22d5fe10b74489db9479d0e36e25a32da393a771fcf12380136a451f" }], + initia: [{ address: "init1npvwllfr9dqr8erajqqr6s0vxnk2ak558xjc5c", publicKey: "0237b0bb7a8288d38ed49a524b5dc98cff3eb5ca824c9f9dc0dfdb3d9cd600f299" }], +}; +const OSMO = "osmo19rl4cm2hmr8afy4kldpxz3fka4jguq0a5m7df8"; +const OSMO_BOB = "osmo1jrkmdcwgq94uaamx6zax2luewlhf7u4k5r4pqs"; + +function harness(opts: { read?: boolean; result?: (r: DappRequest) => unknown } = {}) { + const granted = new Set(); + const approved: DappRequest[] = []; + const reads: DappRequest[] = []; + const connects: { family: Family; params: unknown }[] = []; + const internals: CosmosRouterInternals = { + permitted: async (o, f) => granted.has(`${o}|${f}`), + requirePermission: async (o, f) => { + if (!granted.has(`${o}|${f}`)) throw Object.assign(new Error("Connect Clip Wallet to this site first."), { code: 4100 }); + }, + accounts: async (_o, f) => ACCOUNTS[f] ?? [], + connect: async (o, f, _net, _m, params) => { + connects.push({ family: f, params }); + granted.add(`${o}|${f}`); + return ACCOUNTS[f] ?? []; + }, + approve: async (req) => { + approved.push(req); + return opts.result ? opts.result(req) : null; + }, + makeReq: (origin, family, net, method, params) => ({ id: String(approved.length + reads.length), origin, via: "injected", family, networkId: net.id, method, params }), + requireNetwork: (family, _o, chain) => { + const n = NETS.find((x) => x.family === family && x.id === chain); + if (!n) throw Object.assign(new Error(`Clip Wallet does not support ${chain}.`), { code: 4901 }); + return n; + }, + revoke: async (o, f) => void granted.delete(`${o}|${f}`), + ...(opts.read + ? { + read: async (req: DappRequest) => { + reads.push(req); + return req.method === COSMOS_INJECTED.sendTx ? { txhash: "AABBCC" } : true; + }, + } + : {}), + }; + const dispatcher = createCosmosDispatcher(internals); + const listeners = new Set<(family: Family, event: "accountsChanged" | "disconnect", data: unknown) => void>(); + const transport: InpageTransport = { + request: async (family, method, params, chain) => { + try { + // JSON round trip, as across postMessage and the runtime port. + const p = params === undefined ? undefined : JSON.parse(JSON.stringify(params)); + return JSON.parse(JSON.stringify((await dispatcher.dispatch(ORIGIN, family, method, p, chain)) ?? null)); + } catch (e) { + throw fromRpcErrorShape(toRpcErrorShape(e)); + } + }, + onEvent: (l) => (listeners.add(l as never), () => listeners.delete(l as never)), + destroy: () => undefined, + }; + const win = newWindow(); + const { provider, stop } = installCosmosProvider(win, DEFAULT_IDENTITY, NETS, transport); + const emit = (family: Family, event: "accountsChanged" | "disconnect", data: unknown) => listeners.forEach((l) => l(family, event, data)); + return { provider, stop, win, approved, reads, connects, granted, emit, dispatcher }; +} + +describe("window.clipwallet.cosmos (Keplr API)", () => { + it("is exposed under Clip's own global, never window.keplr", () => { + const h = harness(); + const w = h.win as unknown as Record | undefined>; + expect(w.clipwallet!.cosmos).toBe(h.provider); + expect(w.keplr).toBeUndefined(); + expect((w.clipwallet!.info as { name: string }).name).toBe("Clip Wallet"); + expect(h.provider.mode).toBe("extension"); + h.stop(); + expect(w.clipwallet!.cosmos).toBeUndefined(); + }); + + it("enable prompts once per key family; getKey spells the account per chain", async () => { + const h = harness(); + await expect(h.provider.getKey("osmo-test-5")).rejects.toMatchObject({ code: 4100 }); + await h.provider.enable(["osmo-test-5", "dydx-testnet-4", "initiation-2"]); + expect(h.connects).toEqual([ + { family: "cosmos", params: { chainIds: ["osmo-test-5", "dydx-testnet-4"] } }, + { family: "initia", params: { chainIds: ["initiation-2"] } }, + ]); + await h.provider.enable("osmo-test-5"); + expect(h.connects.length).toBe(2); + + const osmo = await h.provider.getKey("osmo-test-5"); + expect(osmo).toMatchObject({ name: "Clip Wallet", algo: "secp256k1", bech32Address: OSMO, isNanoLedger: false, isKeystone: false }); + expect(hexOf(osmo.pubKey)).toBe(ACCOUNTS.cosmos![0]!.publicKey); + expect(hexOf(osmo.address)).toBe(hexOf(bech32Decode(OSMO)!.data)); + expect(osmo.ethereumHexAddress).toBe(`0x${hexOf(osmo.address)}`); + expect((await h.provider.getKey("dydx-testnet-4")).bech32Address).toBe("dydx19rl4cm2hmr8afy4kldpxz3fka4jguq0a4erelz"); + const init = await h.provider.getKey("initiation-2"); + expect(init).toMatchObject({ algo: "ethsecp256k1", bech32Address: "init1npvwllfr9dqr8erajqqr6s0vxnk2ak558xjc5c" }); + expect(init.ethereumHexAddress).toBe("0x9858effd232b4033e47d90003d41ec34ecaeda94"); + // Provenance isn't connected yet. + await expect(h.provider.getKey("pio-testnet-1")).rejects.toMatchObject({ code: 4100 }); + }); + + it("refuses chains Clip doesn't ship, in plain words", async () => { + const h = harness(); + await expect(h.provider.enable("cosmoshub-4")).rejects.toThrow("There is no chain info for cosmoshub-4"); + await expect(h.provider.enable([])).rejects.toThrow("chain id not set"); + await expect(h.provider.experimentalSuggestChain({ chainId: "evil-1" })).rejects.toThrow("Clip Wallet doesn't support evil-1 and can't add chains from websites."); + await expect(h.provider.experimentalSuggestChain({ chainId: "osmo-test-5" })).resolves.toBeUndefined(); + const infos = await h.provider.getChainInfosWithoutEndpoints(); + expect(infos.map((i) => i.chainId)).toEqual(COSMOS_CHAIN_FACTS.map((c) => c.chainId)); + expect(infos[0]).toMatchObject({ rest: undefined, rpc: undefined, bip44: { coinType: 118 }, bech32Config: { bech32PrefixAccAddr: "osmo", bech32PrefixValAddr: "osmovaloper" } }); + expect(chainInfoWithoutEndpoints("thorchain-1")!.stakeCurrency).toBeUndefined(); + }); + + it("offline signer: getAccounts, signDirect through an approval, account number type kept", async () => { + const h = harness({ + result: (r) => { + const p = r.params as { signDoc: unknown }; + return { signed: p.signDoc, signature: { pub_key: { type: "tendermint/PubKeySecp256k1", value: "AA==" }, signature: "c2ln" } }; + }, + }); + await h.provider.enable("osmo-test-5"); + const signer = h.provider.getOfflineSigner("osmo-test-5"); + const [acc] = await signer.getAccounts(); + expect(acc).toMatchObject({ address: OSMO, algo: "secp256k1" }); + const doc = { bodyBytes: new Uint8Array([1, 2]), authInfoBytes: new Uint8Array([3]), chainId: "osmo-test-5", accountNumber: 4242n }; + const res = await signer.signDirect(OSMO, doc); + expect(res.signed.bodyBytes).toEqual(new Uint8Array([1, 2])); + expect(res.signed.accountNumber).toBe(4242n); + expect(res.signature.signature).toBe("c2ln"); + expect(h.approved.at(-1)).toMatchObject({ + family: "cosmos", + networkId: "cosmos:osmo-test-5", + method: "cosmos_signDirect", + origin: ORIGIN, + params: { signerAddress: OSMO, signDoc: { bodyBytes: "AQI=", authInfoBytes: "Aw==", chainId: "osmo-test-5", accountNumber: "4242" } }, + }); + await expect(signer.signDirect(OSMO, { ...doc, chainId: "osmosis-1" })).rejects.toThrow("Unmatched chain id with the offline signer"); + await expect(signer.signDirect(OSMO_BOB, doc)).rejects.toThrow("Unknown signer address"); + const amino = h.provider.getOfflineSignerOnlyAmino("osmo-test-5"); + expect("signDirect" in amino).toBe(false); + const auto = await h.provider.getOfflineSignerAuto("osmo-test-5"); + expect("signDirect" in auto).toBe(true); + }); + + it("the background checks the signer and the sign doc's chain itself", async () => { + const h = harness(); + await h.provider.enable("osmo-test-5"); + const wire = { bodyBytes: "AQI=", authInfoBytes: "Aw==", chainId: "osmo-test-5", accountNumber: "1" }; + const call = (method: string, params: unknown, chain = "cosmos:osmo-test-5") => h.dispatcher.dispatch(ORIGIN, "cosmos", method, params, chain); + await expect(call("cosmos_signDirect", { signerAddress: OSMO_BOB, signDoc: wire })).rejects.toMatchObject({ code: 4100, message: "Signer mismatched" }); + await expect(call("cosmos_signDirect", { signerAddress: "dydx19rl4cm2hmr8afy4kldpxz3fka4jguq0a4erelz", signDoc: wire })).rejects.toMatchObject({ code: 4100 }); + await expect(call("cosmos_signDirect", { signerAddress: OSMO, signDoc: { ...wire, chainId: "dydx-testnet-4" } })).rejects.toMatchObject({ code: -32602 }); + await expect(call("cosmos_signAmino", { signerAddress: OSMO, signDoc: { chain_id: "osmosis-1" } })).rejects.toMatchObject({ code: -32602 }); + await expect(call("cosmos_signArbitrary", { signer: OSMO, data: "" })).rejects.toMatchObject({ code: -32602 }); + // Another family's network can't be named through this family. + await expect(call("cosmos_signDirect", { signerAddress: OSMO, signDoc: wire }, "cosmos:initiation-2")).rejects.toMatchObject({ code: 4901 }); + await expect(call("cosmos_signEverything", {})).rejects.toMatchObject({ code: 4200 }); + expect(h.approved).toEqual([]); + // Same 20 bytes, this chain's prefix: allowed. + await call("cosmos_signDirect", { signerAddress: OSMO, signDoc: wire }); + expect(h.approved.length).toBe(1); + }); + + it("signAmino, signArbitrary (ADR-36 string → UTF-8 → base64), verifyArbitrary and sendTx", async () => { + const h = harness({ read: true, result: (r) => (r.method === "cosmos_signArbitrary" ? { pub_key: { type: "tendermint/PubKeySecp256k1", value: "AA==" }, signature: "c2ln" } : { signed: (r.params as { signDoc: unknown }).signDoc, signature: {} }) }); + await h.provider.enable("osmo-test-5"); + const doc = { chain_id: "osmo-test-5", account_number: "1", sequence: "0", fee: { amount: [], gas: "1" }, msgs: [], memo: "" }; + expect((await h.provider.signAmino("osmo-test-5", OSMO, doc)).signed).toEqual(doc); + expect(await h.provider.signArbitrary("osmo-test-5", OSMO, "héllo")).toMatchObject({ signature: "c2ln" }); + expect(h.approved.at(-1)).toMatchObject({ method: "cosmos_signArbitrary", params: { signer: OSMO, data: btoa(String.fromCharCode(...new TextEncoder().encode("héllo"))), isString: true } }); + expect(await h.provider.verifyArbitrary("osmo-test-5", OSMO, "x", { pub_key: { type: "t", value: "" }, signature: "" })).toBe(true); + expect(await h.provider.sendTx("osmo-test-5", new Uint8Array([1, 2, 3]), "sync")).toEqual(new Uint8Array([0xaa, 0xbb, 0xcc])); + expect(h.reads.map((r) => r.method)).toEqual(["cosmos_verifyArbitrary", "cosmos_sendTx"]); + await expect(h.dispatcher.dispatch(ORIGIN, "cosmos", "cosmos_sendTx", { tx: "AQ==", mode: "block!" }, "cosmos:osmo-test-5")).rejects.toMatchObject({ code: -32602 }); + }); + + it("without a read hook sendTx is unsupported (4200), not approved", async () => { + const h = harness(); + await h.provider.enable("osmo-test-5"); + await expect(h.provider.sendTx("osmo-test-5", new Uint8Array([1]), "sync")).rejects.toMatchObject({ code: 4200 }); + expect(h.approved).toEqual([]); + }); + + it("account changes fire keystorechange and the namespaced window event; disable forgets the site", async () => { + const h = harness(); + await h.provider.enable(["osmo-test-5", "pio-testnet-1"]); + let n = 0; + let w = 0; + h.provider.on("keystorechange", () => n++); + h.win.addEventListener("clipwallet_keystorechange", () => w++); + h.emit("cosmos", "accountsChanged", []); + h.emit("evm", "accountsChanged", []); + expect(n).toBe(1); + expect(w).toBe(1); + await h.provider.disable("pio-testnet-1"); + expect([...h.granted]).toEqual([`${ORIGIN}|cosmos`]); + await h.provider.disable(); + expect([...h.granted]).toEqual([]); + }); + + it("allowlist and table", () => { + expect([...cosmosInjectedAllowlist("initia")]).toEqual(Object.values(COSMOS_METHODS_ALLOWED).flat()); + expect(cosmosInjectedAllowlist("evm").size).toBe(0); + for (const c of COSMOS_CHAIN_FACTS) { + const data = new Uint8Array(20).fill(7); + expect(bech32Decode(bech32Encode(c.prefix, data))).toEqual({ prefix: c.prefix, data }); + } + expect(bech32Decode(OSMO.slice(0, -1) + "q")).toBeNull(); + }); +}); diff --git a/packages/1mask/test/fuel.test.ts b/packages/1mask/test/fuel.test.ts new file mode 100644 index 00000000..58a9bc37 --- /dev/null +++ b/packages/1mask/test/fuel.test.ts @@ -0,0 +1,208 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { createFuelDispatcher, fuelInjectedAllowlist, type FuelRouterInternals } from "../src/background/fuel.js"; +import { ClipFuelConnector, FUEL_CONNECTOR_EVENT, fuelWireMessage, installFuelConnector } from "../src/inpage/fuel.js"; +import type { InpageTransport } from "../src/inpage/transport.js"; +import { DEFAULT_IDENTITY } from "../src/shared/config.js"; +import { ProviderRpcError, RpcErrorCode } from "../src/shared/errors.js"; +import { FUEL_INJECTED } from "../src/shared/fuel.js"; +import type { ExposedAccount, OneMaskEvent } from "../src/shared/protocol.js"; + +const asset = (networkId: string) => ({ key: "eth", symbol: "ETH", name: "Ether", decimals: 9, networkId }); +const net = (id: string, url: string): Network => ({ id, family: "fuel", name: id, nativeAsset: asset(id), testnet: id === "fuel:0", rpcUrls: [url], explorerUrl: "" }); +const TESTNET = net("fuel:0", "https://testnet.fuel.network/v1/graphql"); +const MAINNET = net("fuel:9889", "https://mainnet.fuel.network/v1/graphql"); +const ME: ExposedAccount = { address: "0x806EC69A1bC1398877A7327c467EE09511B3f26DcfB55b305D8ea2cd2c285b89", publicKey: "026760754232b8ae531b05039f630cf2549027d47dfe6af603f7504616602dfcd2" }; +const ORIGIN = "https://app.example"; + +/** A router stand-in: one origin, one permission flag, approvals recorded. */ +function fakeRouter(opts: { permitted?: boolean; reject?: boolean } = {}) { + let permitted = opts.permitted ?? false; + const selected = new Map(); + const approved: DappRequest[] = []; + const events: { event: OneMaskEvent; data: unknown }[] = []; + const h: FuelRouterInternals = { + permitted: async () => permitted, + accounts: async () => (permitted ? [ME] : []), + connect: async () => { + if (opts.reject) throw new ProviderRpcError(RpcErrorCode.UserRejected, "The user rejected the request."); + permitted = true; + return [ME]; + }, + approve: async (r) => (approved.push(r), r.method === FUEL_INJECTED.sendTransaction ? "0xabc" : r.method === FUEL_INJECTED.signMessage ? "0xsig" : { signed: true }), + makeReq: (origin, family, n, method, params) => ({ id: "x", origin, via: "injected", family, networkId: n.id, method, params }), + selectedNetwork: (origin) => [TESTNET, MAINNET].find((n) => n.id === selected.get(origin)) ?? TESTNET, + setSelected: (origin, _f, id) => void selected.set(origin, id), + candidates: (f: Family) => (f === "fuel" ? [TESTNET, MAINNET] : []), + emit: (_o, _f, event, data) => void events.push({ event, data }), + revoke: async () => void (permitted = false), + }; + return { h, approved, events, dispatcher: createFuelDispatcher(h) }; +} + +/** Page ↔ background without the content script: requests go straight to the dispatcher. */ +function directTransport(r: ReturnType) { + const listeners = new Set<(f: Family, e: OneMaskEvent, d: unknown) => void>(); + const sent: { method: string; params: unknown }[] = []; + const t: InpageTransport & { push(e: OneMaskEvent, d: unknown): void; sent: typeof sent } = { + request: async (family, method, params) => { + expect(family).toBe("fuel"); + sent.push({ method, params }); + // Round-trip through JSON like the real transport (postMessage + runtime port). + const p = params === undefined ? undefined : JSON.parse(JSON.stringify(params)); + return JSON.parse(JSON.stringify((await r.dispatcher.dispatch(ORIGIN, method, p)) ?? null)); + }, + onEvent: (l) => (listeners.add(l), () => listeners.delete(l)), + destroy: () => listeners.clear(), + push: (e, d) => listeners.forEach((l) => l("fuel", e, d)), + sent, + }; + return t; +} + +describe("Fuel dispatcher (background)", () => { + it("allowlists the connector's methods only", () => { + expect([...fuelInjectedAllowlist()].sort()).toEqual( + ["1mask_getAccounts", "fuel:connect", "fuel:currentNetwork", "fuel:disconnect", "fuel:networks", "fuel:selectNetwork", "fuel_sendTransaction", "fuel_signMessage", "fuel_signTransaction"].sort(), + ); + }); + + it("reads accounts silently, connects once, and refuses signing before connect", async () => { + const r = fakeRouter(); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.accounts, undefined)).toEqual([]); + await expect(r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.signMessage, { address: ME.address, message: { text: "hi" } })).rejects.toMatchObject({ code: 4100 }); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.connect, {})).toEqual([ME]); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.accounts, undefined)).toEqual([ME]); + expect(r.approved).toEqual([]); + }); + + it("sends signing requests for the connected account to approval, on the site's network", async () => { + const r = fakeRouter({ permitted: true }); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.sendTransaction, { address: ME.address.toLowerCase(), transaction: { type: 0 } })).toBe("0xabc"); + expect(r.approved[0]).toMatchObject({ family: "fuel", networkId: "fuel:0", method: "fuel_sendTransaction", origin: ORIGIN }); + await expect(r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.signTransaction, { address: `0x${"11".repeat(32)}`, transaction: { type: 0 } })).rejects.toMatchObject({ code: 4100 }); + await expect(r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.signMessage, { address: ME.address })).rejects.toMatchObject({ code: -32602 }); + await expect(r.dispatcher.dispatch(ORIGIN, "fuel_signTypedData", {})).rejects.toMatchObject({ code: 4200 }); + }); + + it("lists and switches only the wallet's networks, telling the page", async () => { + const r = fakeRouter(); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.currentNetwork, undefined)).toEqual({ url: TESTNET.rpcUrls[0], chainId: 0 }); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.networks, undefined)).toEqual([ + { url: TESTNET.rpcUrls[0], chainId: 0 }, + { url: MAINNET.rpcUrls[0], chainId: 9889 }, + ]); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.selectNetwork, { chainId: 9889 })).toBe(true); + expect(r.events).toEqual([{ event: "chainChanged", data: { url: MAINNET.rpcUrls[0], chainId: 9889 } }]); + expect(await r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.selectNetwork, { url: "https://mainnet.fuel.network/v1/graphql/" })).toBe(true); + expect(r.events).toHaveLength(1); + await expect(r.dispatcher.dispatch(ORIGIN, FUEL_INJECTED.selectNetwork, { chainId: 1119889111 })).rejects.toMatchObject({ code: 4902 }); + }); +}); + +describe("Fuel connector (inpage)", () => { + it("announces itself with the FuelConnector event under the wallet's own identity", () => { + const seen: unknown[] = []; + const onAnnounce = (e: Event) => seen.push((e as CustomEvent).detail); + window.addEventListener(FUEL_CONNECTOR_EVENT, onAnnounce); + const identity = { ...DEFAULT_IDENTITY, name: "Kit Wallet", rdns: "com.example.kit" }; + const { connector, announce, stop } = installFuelConnector(window, identity, directTransport(fakeRouter()), { reannounce: false }); + expect(seen).toEqual([connector]); + expect(connector.name).toBe("Kit Wallet"); + expect(connector.name).not.toMatch(/fuel wallet/i); + expect(connector.metadata.image).toBe(identity.icon); + expect((window as unknown as { clipwallet: { fuel: unknown } }).clipwallet.fuel).toBe(connector); + announce(); + expect(seen).toHaveLength(2); + // Fuel writes these on the connector, so it must stay writable. + connector.installed = true; + (connector as unknown as { _latestUpdate: number })._latestUpdate = 1; + stop(); + window.removeEventListener(FUEL_CONNECTOR_EVENT, onAnnounce); + }); + + it("connects, follows account and network events, and disconnects", async () => { + const r = fakeRouter(); + const t = directTransport(r); + const c = new ClipFuelConnector(DEFAULT_IDENTITY, t); + const log: [string, unknown][] = []; + for (const e of ["accounts", "currentAccount", "connection", "currentNetwork"]) c.on(e, (d: unknown) => log.push([e, d])); + expect(await c.ping()).toBe(true); + expect(await c.isConnected()).toBe(false); + expect(await c.connect()).toBe(true); + expect(await c.accounts()).toEqual([ME.address]); + expect(await c.currentAccount()).toBe(ME.address); + expect(log).toEqual([ + ["accounts", [ME.address]], + ["currentAccount", ME.address], + ["connection", true], + ]); + t.push("chainChanged", { url: MAINNET.rpcUrls[0], chainId: 9889 }); + expect(log.at(-1)).toEqual(["currentNetwork", { url: MAINNET.rpcUrls[0], chainId: 9889 }]); + expect(await c.disconnect()).toBe(false); + expect(log.slice(-3)).toEqual([ + ["accounts", []], + ["currentAccount", null], + ["connection", false], + ]); + expect(await c.isConnected()).toBe(false); + }); + + it("answers false when the person declines to connect", async () => { + const c = new ClipFuelConnector(DEFAULT_IDENTITY, directTransport(fakeRouter({ reject: true }))); + expect(await c.connect()).toBe(false); + }); + + it("sends messages and transactions as plain JSON", async () => { + const r = fakeRouter({ permitted: true }); + const t = directTransport(r); + const c = new ClipFuelConnector(DEFAULT_IDENTITY, t); + expect(await c.signMessage(ME.address, "hello")).toBe("0xsig"); + await c.signMessage(ME.address, { personalSign: new Uint8Array([0xde, 0xad, 0xbe, 0xef]) }); + expect(r.approved.map((a) => (a.params as { message: unknown }).message)).toEqual([{ text: "hello" }, { personalSignHex: "0xdeadbeef" }]); + + // A fuels-ts-like request instance: BN fields serialize through toJSON, functions and prototypes don't cross. + class Bn { + constructor(private v: number) {} + toJSON() { + return `0x${this.v.toString(16)}`; + } + } + class Req { + type = 0; + gasLimit = new Bn(64); + maxFee = new Bn(168); + inputs = []; + outputs = []; + witnesses = []; + helper() { + return 1; + } + } + const id = await c.sendTransaction(ME.address, new Req(), { + provider: { url: TESTNET.rpcUrls[0]! }, + onBeforeSend: async (x) => Object.assign(x as object, { tip: new Bn(1) }), + }); + expect(id).toBe("0xabc"); + expect(r.approved.at(-1)!.params).toEqual({ + address: ME.address, + transaction: { type: 0, gasLimit: "0x40", maxFee: "0xa8", inputs: [], outputs: [], witnesses: [], tip: "0x1" }, + provider: { url: TESTNET.rpcUrls[0] }, + }); + expect(await c.signTransaction(ME.address, { type: 0 })).toEqual({ signed: true }); + expect(r.approved.at(-1)!.method).toBe("fuel_signTransaction"); + }); + + it("checks message shapes in the page and doesn't let sites add networks or assets", async () => { + expect(fuelWireMessage({ personalSign: "hi" })).toEqual({ personalSign: "hi" }); + expect(() => fuelWireMessage(" ")).toThrow(); + expect(() => fuelWireMessage({ nope: 1 })).toThrow(); + const c = new ClipFuelConnector(DEFAULT_IDENTITY, directTransport(fakeRouter())); + await expect(c.addNetwork("https://evil.example/v1/graphql")).rejects.toMatchObject({ code: 4001 }); + expect(await c.addAssets([{}])).toBe(false); + expect(await c.assets()).toEqual([]); + expect(await c.hasABI("0x00")).toBe(false); + expect(await c.selectNetwork({ chainId: 9889 })).toBe(true); + expect(await c.currentNetwork()).toEqual({ url: MAINNET.rpcUrls[0], chainId: 9889 }); + }); +}); diff --git a/packages/1mask/test/multiversx.test.ts b/packages/1mask/test/multiversx.test.ts new file mode 100644 index 00000000..3ef6149c --- /dev/null +++ b/packages/1mask/test/multiversx.test.ts @@ -0,0 +1,228 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { createMemoryPermissionStore } from "../src/background/index.js"; +import { MULTIVERSX_CONNECT_METHODS, createMultiversXDispatcher, multiversxInjectedAllowlist } from "../src/background/multiversx.js"; +import { type MultiversXProviderEntry, installMultiversXProvider } from "../src/inpage/multiversx.js"; +import type { EventListener, InpageTransport } from "../src/inpage/transport.js"; +import { resolveIdentity } from "../src/shared/config.js"; +import { ProviderRpcError } from "../src/shared/errors.js"; +import { MULTIVERSX_INJECTED, multiversxNetworkIdOf, nativeAuthOrigin } from "../src/shared/multiversx-methods.js"; +import type { ExposedAccount, OneMaskEvent } from "../src/shared/protocol.js"; +import { newWindow } from "./helpers.js"; + +const asset = (networkId: string) => ({ key: "egld", symbol: "EGLD", name: "MultiversX eGold", decimals: 18, networkId }); +const net = (id: string, name: string): Network => ({ id, family: "multiversx", name, nativeAsset: asset(id), testnet: id !== "mvx:1", rpcUrls: ["https://devnet-gateway.multiversx.com"], explorerUrl: "https://devnet-explorer.multiversx.com" }); +const DEVNET = net("mvx:D", "MultiversX Devnet"); +const MAINNET = net("mvx:1", "MultiversX"); +const NETWORKS: Network[] = [DEVNET, MAINNET]; +const ME = "erd1sqhjrtmsn5yjk6w85099p8v0ly0g8z9pxeqe5dvu5rlf2n7vq3vqytny9g"; +const BOB = "erd1sxmr0k8u6trd5c6eu6trzyapzux7090ykujmsng7pdx0m8k93n5s48kwzk"; +const ORIGIN = "https://dapp.example"; +const SIG = "ab".repeat(64); + +const b64url = (t: string) => btoa(t).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +const loginToken = (origin: string) => `${b64url(origin)}.${"cd".repeat(32)}.86400.${b64url("{}")}`; + +/** Router stand-in lending the same closures createOneMaskRouter lends the dispatchers. */ +function harness(handleImpl: (r: DappRequest) => unknown = () => ({ signature: SIG })) { + const permissions = createMemoryPermissionStore(); + const handled: DappRequest[] = []; + const listeners = new Set(); + const accounts: ExposedAccount[] = [{ address: ME, publicKey: "80".repeat(32) }]; + const emit = (family: Family, event: OneMaskEvent, data?: unknown) => listeners.forEach((l) => l(family, event, data)); + const d = createMultiversXDispatcher({ + permitted: async (o, f) => !!(await permissions.has(o, f)), + requirePermission: async (o, f) => { + if (!(await permissions.has(o, f))) throw new ProviderRpcError(4100, "Connect first."); + }, + accounts: async () => accounts, + approve: async (req) => { + handled.push(req); + return handleImpl(req); + }, + connect: async (o, f, n, method, params) => { + handled.push({ id: "c", origin: o, via: "injected", family: f, networkId: n.id, method, params }); + await permissions.grant(o, f); + emit(f, "accountsChanged", accounts); + return accounts; + }, + makeReq: (o, f, n, method, params) => ({ id: String(handled.length), origin: o, via: "injected", family: f, networkId: n.id, method, params }), + requireNetwork: (_f, _o, chain) => { + if (chain === undefined) return DEVNET; + const n = NETWORKS.find((x) => x.id === chain); + if (!n) throw new ProviderRpcError(4901, "Not connected to that network."); + return n; + }, + revoke: async (o, f) => { + await permissions.revoke(o, f); + emit(f, "accountsChanged", []); + emit(f, "disconnect"); + }, + }); + const transport: InpageTransport & { calls: { method: string; params: unknown; chain?: string }[] } = { + calls: [], + async request(family, method, params, chain) { + transport.calls.push({ method, params, ...(chain !== undefined ? { chain } : {}) }); + expect(family).toBe("multiversx"); + // The page→background hop is JSON (ports serialise). + return JSON.parse(JSON.stringify((await d.dispatch(ORIGIN, method, params === undefined ? undefined : JSON.parse(JSON.stringify(params)), chain)) ?? null)); + }, + onEvent(l) { + listeners.add(l); + return () => listeners.delete(l); + }, + destroy() {}, + }; + return { d, handled, permissions, transport }; +} + +/** A stand-in for an sdk-core Transaction (the provider only uses toPlainObject() and `signature`). */ +class FakeTransaction { + signature = new Uint8Array(); + guardianSignature = new Uint8Array([9]); + constructor(readonly plain: Record) {} + toPlainObject() { + return { ...this.plain }; + } +} +const plainTx = (over: Record = {}) => ({ nonce: 7, value: "1000000000000000000", receiver: BOB, sender: ME, gasPrice: 1000000000, gasLimit: 50000, chainID: "D", version: 2, ...over }); + +describe("MultiversX background dispatcher", () => { + it("allows only the MultiversX wire methods", async () => { + expect([...multiversxInjectedAllowlist()].sort()).toEqual([MULTIVERSX_INJECTED.accounts, MULTIVERSX_INJECTED.connect, MULTIVERSX_INJECTED.disconnect, "mvx_signMessage", "mvx_signTransactions"].sort()); + expect(MULTIVERSX_CONNECT_METHODS).toEqual(["mvx:connect"]); + const { d } = harness(); + await expect(d.dispatch(ORIGIN, "mvx_signAndSendTransactions", {}, undefined)).rejects.toMatchObject({ code: 4200 }); + }); + + it("needs a connection before signing; accounts are silent and empty until then", async () => { + const { d, handled } = harness(); + expect(await d.dispatch(ORIGIN, MULTIVERSX_INJECTED.accounts, undefined, undefined)).toEqual([]); + await expect(d.dispatch(ORIGIN, "mvx_signMessage", { message: "hi", address: ME }, "mvx:D")).rejects.toMatchObject({ code: 4100 }); + expect(handled).toHaveLength(0); + }); + + it("checks that every signer is this site's account", async () => { + const { d, handled } = harness(); + await d.dispatch(ORIGIN, MULTIVERSX_INJECTED.connect, {}, "mvx:D"); + await expect(d.dispatch(ORIGIN, "mvx_signMessage", { message: "hi", address: BOB }, "mvx:D")).rejects.toMatchObject({ code: 4100 }); + await expect(d.dispatch(ORIGIN, "mvx_signTransactions", { transactions: [plainTx({ sender: BOB })] }, "mvx:D")).rejects.toMatchObject({ code: 4100 }); + await expect(d.dispatch(ORIGIN, "mvx_signTransactions", { transactions: [] }, "mvx:D")).rejects.toMatchObject({ code: -32602 }); + await expect(d.dispatch(ORIGIN, "mvx_signTransactions", { transactions: [plainTx()] }, "mvx:Z")).rejects.toMatchObject({ code: 4901 }); + expect(handled.map((h) => h.method)).toEqual(["mvx:connect"]); + await d.dispatch(ORIGIN, "mvx_signTransactions", { transactions: [plainTx()], address: ME }, "mvx:D"); + expect(handled.at(-1)).toMatchObject({ method: "mvx_signTransactions", networkId: "mvx:D", family: "multiversx" }); + }); + + it("refuses a native-auth login token made for another site", async () => { + const { d, handled } = harness(); + await d.dispatch(ORIGIN, MULTIVERSX_INJECTED.connect, {}, undefined); + expect(nativeAuthOrigin(loginToken("https://other.example"))).toBe("https://other.example"); + await expect(d.dispatch(ORIGIN, "mvx_signMessage", { message: ME + loginToken("https://other.example"), address: ME }, undefined)).rejects.toMatchObject({ code: 4100 }); + await d.dispatch(ORIGIN, "mvx_signMessage", { message: ME + loginToken(ORIGIN), address: ME }, undefined); + expect(handled.at(-1)?.method).toBe("mvx_signMessage"); + }); +}); + +describe("MultiversX provider on window.multiversx.providers (sdk-dapp custom provider hook)", () => { + const identity = resolveIdentity({ name: "Kit Wallet", rdns: "org.example.kit" }); + + it("adds its own entry without replacing the array or other wallets' entries", () => { + const win = newWindow(); + const other = { name: "Other", type: "other", constructor: async () => ({}) }; + const original = [other]; + (win as unknown as { multiversx: unknown }).multiversx = { providers: original, keep: 1 }; + const { transport } = harness(); + const { entry, provider, stop } = installMultiversXProvider(win, identity, NETWORKS, transport, { globalKey: "kitwallet" }); + const mvx = (win as unknown as { multiversx: { providers: MultiversXProviderEntry[]; keep: number } }).multiversx; + expect(mvx.providers).toBe(original); + expect(mvx.keep).toBe(1); + expect(mvx.providers.map((e) => e.type)).toEqual(["other", "kitwallet"]); + expect(entry).toMatchObject({ name: "Kit Wallet", type: "kitwallet", iconUrl: identity.icon }); + expect(Object.isFrozen(entry)).toBe(true); + expect((win as unknown as { kitwallet: { multiversx: unknown } }).kitwallet.multiversx).toBe(provider); + // Installing twice doesn't duplicate (sdk-dapp dedupes by type too). + installMultiversXProvider(win, identity, NETWORKS, transport, { globalKey: "kitwallet" }); + expect(mvx.providers.filter((e) => e.type === "kitwallet")).toHaveLength(1); + stop(); + expect(mvx.providers.map((e) => e.type)).toEqual(["other"]); + }); + + it("creates the hook when the page has none, never as the DeFi Wallet", () => { + const win = newWindow(); + const { transport } = harness(); + const { entry } = installMultiversXProvider(win, resolveIdentity(), NETWORKS, transport); + expect(entry.type).toBe("clipwallet"); + expect(entry.type).not.toBe("extension"); + const w = win as unknown as Record; + expect(w.elrondWallet).toBeUndefined(); + expect(w.multiversxWallet).toBeUndefined(); + }); + + it("ProviderFactory-style use: construct, init, login with a native-auth token, sign transactions and a message", async () => { + const win = newWindow(); + const { transport, handled } = harness((req) => { + if (req.method === "mvx_signTransactions") { + const txs = (req.params as { transactions: unknown[] }).transactions; + return { signatures: txs.map((_, i) => ({ signature: String(i + 1).repeat(128).slice(0, 128) })), transactions: txs }; + } + return { signature: SIG, address: ME }; + }); + const { entry } = installMultiversXProvider(win, identity, NETWORKS, transport); + // sdk-dapp ProviderFactory.create: `await entry.constructor({ address, anchor })`, then `provider.init()`. + const p = await entry.constructor({ address: undefined }); + expect(await p.init()).toBe(true); + expect(p.isInitialized() && !p.isConnected()).toBe(true); + expect(p.getType()).toBe("clipwallet"); + + const token = loginToken(ORIGIN); + const login = await p.login({ token }); + expect(login).toEqual({ address: ME, signature: SIG }); + expect(handled.map((h) => h.method)).toEqual(["mvx:connect", "mvx_signMessage"]); + // The native-auth message: address + token (sdk-native-auth-server `${address}${body}`). + expect(handled[1]!.params).toEqual({ message: `${ME}${token}`, address: ME }); + expect(await p.getAddress()).toBe(ME); + expect(p.getAccount()).toEqual({ address: ME }); + + const t1 = new FakeTransaction(plainTx()); + const t2 = new FakeTransaction(plainTx({ nonce: 8 })); + const signed = await p.signTransactions([t1, t2]); + expect(signed[0]).toBe(t1); + expect(Array.from(t1.signature.slice(0, 2))).toEqual([0x11, 0x11]); + expect(Array.from(t2.signature.slice(0, 2))).toEqual([0x22, 0x22]); + expect(t1.guardianSignature).toEqual(new Uint8Array([9])); + expect(handled.at(-1)).toMatchObject({ method: "mvx_signTransactions", networkId: "mvx:D", params: { transactions: [plainTx(), plainTx({ nonce: 8 })], address: ME } }); + expect(await p.signTransaction(new FakeTransaction(plainTx({ chainID: "1" })))).toBeInstanceOf(FakeTransaction); + expect(handled.at(-1)?.networkId).toBe("mvx:1"); + await expect(p.signTransactions([new FakeTransaction(plainTx()), new FakeTransaction(plainTx({ chainID: "1" }))])).rejects.toMatchObject({ code: -32602 }); + + const message = { data: new TextEncoder().encode("hello"), signer: "sdk-js" } as { data: Uint8Array; signature?: Uint8Array; signer: string }; + const sm = await p.signMessage(message); + expect(sm).toBe(message); + expect(sm.signature).toHaveLength(64); + expect(sm.signer).toBe("Kit Wallet"); + expect(handled.at(-1)?.params).toEqual({ message: "hello", address: ME }); + await expect(p.signMessage({ data: Uint8Array.of(0xff, 0xfe) })).rejects.toMatchObject({ code: -32602 }); + + expect(await p.logout()).toBe(true); + expect(p.isConnected()).toBe(false); + await expect(p.signMessage({ data: new TextEncoder().encode("x") })).rejects.toMatchObject({ code: 4100 }); + }); + + it("session restore: the constructor takes sdk-dapp's stored address; the background still checks it", async () => { + const win = newWindow(); + const { transport } = harness(); + const { entry } = installMultiversXProvider(win, identity, NETWORKS, transport); + const p = await entry.constructor({ address: BOB }); + expect(await p.getAddress()).toBe(BOB); + // Not permitted yet → 4100 from the background, nothing signed. + await expect(p.signMessage({ data: new TextEncoder().encode("x") })).rejects.toMatchObject({ code: 4100 }); + }); + + it("chain ids", () => { + expect(multiversxNetworkIdOf("D")).toBe("mvx:D"); + expect(multiversxNetworkIdOf("1")).toBe("mvx:1"); + expect(multiversxNetworkIdOf("")).toBeNull(); + expect(nativeAuthOrigin("not.a.token")).toBeNull(); + }); +}); diff --git a/packages/1mask/test/n87-account-events.test.ts b/packages/1mask/test/n87-account-events.test.ts new file mode 100644 index 00000000..c7416035 --- /dev/null +++ b/packages/1mask/test/n87-account-events.test.ts @@ -0,0 +1,68 @@ +import type { Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { createMemoryPermissionStore, createOneMaskRouter, type RouterPort } from "../src/background/index.js"; + +/** The public "abandon … about" accounts (chains-stacks / chains-bitcoincash test/signatures.ts), vault spelling. */ +const SP = "SPC5KHM41H6WHAST7MWWDD807YSPRQKJ69FSH54J"; +const ST = "STC5KHM41H6WHAST7MWWDD807YSPRQKJ68T330BQ"; +const BCH_MAIN = "bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6"; +const BCH_TEST = "bchtest:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnqseeszx8x"; +const ORIGIN = "https://dapp.example"; + +const asset = (key: string, networkId: string) => ({ key, symbol: key, name: key, decimals: 6, networkId }); +const net = (id: string, family: Network["family"], testnet = true): Network => ({ id, family, name: id, nativeAsset: asset(family, id), testnet, rpcUrls: [], explorerUrl: "" }); +const NETWORKS: Network[] = [ + net("stacks:2147483648", "stacks"), + net("stacks:1", "stacks", false), + net("bip122:00000000040ba9641ba98a37b2e5ceea", "bitcoincash"), + net("bip122:000000000000000000651ef99cb9fcbe", "bitcoincash", false), +]; + +function setup() { + const permissions = createMemoryPermissionStore(); + const router = createOneMaskRouter({ + networks: NETWORKS, + permissions, + accountsFor: (_o, f) => (f === "stacks" ? [{ address: SP, publicKey: "03d5" }] : f === "bitcoincash" ? [{ address: BCH_MAIN }] : []), + handle: async () => true, + }); + const events: { family: string; event: string; data: unknown }[] = []; + const port: RouterPort = { + postMessage: (m: unknown) => { + const e = m as { type: string; family: string; event: string; data: unknown }; + if (e.type === "event") events.push({ family: e.family, event: e.event, data: e.data }); + }, + onMessage: { addListener: () => {} }, + onDisconnect: { addListener: () => {} }, + } as unknown as RouterPort; + router.attachPort(port, { senderOrigin: ORIGIN }); + return { router, permissions, events }; +} + +describe("accountsChanged spelling for Stacks and Bitcoin Cash", () => { + it("connect and notifyAccountsChanged carry the site's network spelling, not the vault's mainnet form", async () => { + const { router, permissions, events } = setup(); + expect(await router.dispatch(ORIGIN, { family: "stacks", method: "stacks:connect", params: {} })).toEqual([{ address: ST, publicKey: "03d5" }]); + expect(events.at(-1)).toEqual({ family: "stacks", event: "accountsChanged", data: [{ address: ST, publicKey: "03d5" }] }); + + await permissions.grant(ORIGIN, "bitcoincash"); + await router.notifyAccountsChanged(); + expect(events.filter((e) => e.event === "accountsChanged").slice(-2)).toEqual([ + { family: "stacks", event: "accountsChanged", data: [{ address: ST, publicKey: "03d5" }] }, + { family: "bitcoincash", event: "accountsChanged", data: [{ address: BCH_TEST }] }, + ]); + }); + + it("follows the network a site selected (mainnet → SP / bitcoincash:)", async () => { + const { router, permissions, events } = setup(); + await router.dispatch(ORIGIN, { family: "stacks", method: "stacks:connect", params: {}, chain: "stacks:1" }); + expect(events.at(-1)?.data).toEqual([{ address: SP, publicKey: "03d5" }]); + await router.notifyAccountsChanged("stacks"); + expect(events.at(-1)?.data).toEqual([{ address: SP, publicKey: "03d5" }]); + await router.dispatch(ORIGIN, { family: "stacks", method: "stacks:disconnect" }); + await permissions.grant(ORIGIN, "bitcoincash"); + await router.notifyAccountsChanged("bitcoincash"); + expect(events.at(-1)?.data).toEqual([{ address: BCH_TEST }]); + + }); +}); diff --git a/packages/1mask/test/stacks-provider.test.ts b/packages/1mask/test/stacks-provider.test.ts new file mode 100644 index 00000000..9274ee30 --- /dev/null +++ b/packages/1mask/test/stacks-provider.test.ts @@ -0,0 +1,150 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { createStacksDispatcher, stacksInjectedAllowlist } from "../src/background/stacks.js"; +import { DEFAULT_IDENTITY } from "../src/shared/config.js"; +import { rpcError } from "../src/shared/errors.js"; +import type { ExposedAccount } from "../src/shared/protocol.js"; +import { STACKS_INJECTED, stacksAddressOn, stacksChainHint } from "../src/shared/stacks.js"; +import { ClipStacksProvider, installStacksProvider, toSip030Error } from "../src/inpage/stacks.js"; +import type { InpageTransport } from "../src/inpage/transport.js"; +import { newWindow } from "./helpers.js"; + +/** The public "abandon … about" Stacks account 0 (chains-stacks test/signatures.ts). */ +const SP = "SPC5KHM41H6WHAST7MWWDD807YSPRQKJ69FSH54J"; +const ST = "STC5KHM41H6WHAST7MWWDD807YSPRQKJ68T330BQ"; +const PUB = "03d5d038bce81b3965314dba54f636f093c7dbdd6617cded013a53474fbccb100c"; +const OTHER_ST = "ST3XHES5990FYDV5BHBZCJRFYFD2Z4X3FMEXRWMFR"; + +const asset = (networkId: string) => ({ key: "stx", symbol: "STX", name: "Stacks", decimals: 6, networkId }); +const TESTNET: Network = { id: "stacks:2147483648", family: "stacks", name: "Stacks Testnet", nativeAsset: asset("stacks:2147483648"), testnet: true, rpcUrls: [], explorerUrl: "" }; +const MAINNET: Network = { id: "stacks:1", family: "stacks", name: "Stacks", nativeAsset: asset("stacks:1"), testnet: false, rpcUrls: [], explorerUrl: "" }; + +function fakeRouter(opts: { permitted?: boolean; approve?: (r: DappRequest) => unknown } = {}) { + let permitted = opts.permitted ?? false; + const approved: DappRequest[] = []; + const accounts: ExposedAccount[] = [{ address: SP, publicKey: PUB }]; + const r = { + permitted: async () => permitted, + requirePermission: async () => { + if (!permitted) throw rpcError.unauthorized(); + }, + accounts: async () => accounts, + connect: async () => { + permitted = true; + return accounts; + }, + approve: async (req: DappRequest) => { + approved.push(req); + return opts.approve ? opts.approve(req) : { txid: "ab".repeat(32) }; + }, + makeReq: (origin: string, family: Family, net: Network, method: string, params: unknown): DappRequest => ({ id: "1", origin, via: "injected", family, networkId: net.id, method, params }), + requireNetwork: (_f: Family, _o: string, chain: string | undefined) => (chain === MAINNET.id ? MAINNET : TESTNET), + revoke: async () => { + permitted = false; + }, + networks: () => [TESTNET, MAINNET], + }; + return { r, approved, d: createStacksDispatcher(r) }; +} + +/** Inpage transport straight into the dispatcher (no content script), origin fixed. */ +function direct(d: ReturnType, origin = "https://app.example"): InpageTransport { + return { + request: (family, method, params, chain) => d.dispatch(origin, family, method, params, chain), + onEvent: () => () => {}, + destroy: () => {}, + }; +} + +describe("Stacks addresses per network", () => { + it("re-spells the mainnet SP address as ST on testnet (checksum verified)", async () => { + expect(await stacksAddressOn(SP, TESTNET.id)).toBe(ST); + expect(await stacksAddressOn(ST, MAINNET.id)).toBe(SP); + expect(await stacksAddressOn(SP.slice(0, -1) + "K", TESTNET.id)).toBeUndefined(); + expect(await stacksAddressOn("0x1234", TESTNET.id)).toBeUndefined(); + expect(await stacksAddressOn("SP000000000000000000002Q6VF78", TESTNET.id)).toBe("ST000000000000000000002AMW42H"); + expect(stacksChainHint("testnet")).toBe(TESTNET.id); + expect(stacksChainHint("devnet")).toBeUndefined(); + }); +}); + +describe("background dispatcher", () => { + it("connects with the testnet spelling and lets signing through only after connect", async () => { + const { d, approved } = fakeRouter(); + expect(await d.dispatch("https://app.example", "stacks", STACKS_INJECTED.accounts, undefined, undefined)).toEqual([]); + await expect(d.dispatch("https://app.example", "stacks", "stx_transferStx", { recipient: OTHER_ST, amount: "1" }, undefined)).rejects.toMatchObject({ code: 4100 }); + expect(await d.dispatch("https://app.example", "stacks", STACKS_INJECTED.connect, {}, undefined)).toEqual([{ address: ST, publicKey: PUB }]); + await d.dispatch("https://app.example", "stacks", "stx_transferStx", { recipient: OTHER_ST, amount: "1" }, undefined); + expect(approved[0]).toMatchObject({ family: "stacks", networkId: TESTNET.id, method: "stx_transferStx" }); + }); + + it("refuses a signer address that isn't connected, and unknown methods", async () => { + const { d } = fakeRouter({ permitted: true }); + await expect(d.dispatch("https://app.example", "stacks", "stx_signMessage", { message: "hi", address: OTHER_ST }, undefined)).rejects.toMatchObject({ code: 4100 }); + await d.dispatch("https://app.example", "stacks", "stx_signMessage", { message: "hi", address: ST }, undefined); + await expect(d.dispatch("https://app.example", "stacks", "stx_getAccounts", {}, undefined)).rejects.toMatchObject({ code: 4200 }); + expect(stacksInjectedAllowlist().has("stx_callContract")).toBe(true); + expect(stacksInjectedAllowlist().has("stx_updateProfile")).toBe(false); + }); + + it("answers stx_getNetworks", async () => { + const { d } = fakeRouter({ permitted: true }); + expect(await d.dispatch("https://app.example", "stacks", "stx_getNetworks", undefined, undefined)).toEqual({ + active: "testnet", + networks: [ + { id: "testnet", chainId: 2147483648, transactionVersion: 128 }, + { id: "mainnet", chainId: 1, transactionVersion: 0 }, + ], + }); + }); +}); + +describe("injected SIP-030 provider", () => { + it("registers on window.clipwallet.stacks and WBIP-004 without touching other wallets", () => { + const win = newWindow(); + const leather = { request: () => "leather" }; + const others = [{ id: "LeatherProvider", name: "Leather", icon: "data:," }]; + Object.assign(win, { LeatherProvider: leather, wbip_providers: others }); + const { provider, entry, stop } = installStacksProvider(win, DEFAULT_IDENTITY, [TESTNET], direct(fakeRouter().d)); + const w = win as unknown as Record; + expect(w.wbip_providers).toBe(others); + expect(w.wbip_providers.map((p: { id: string }) => p.id)).toEqual(["LeatherProvider", "clipwallet.stacks"]); + expect(entry).toMatchObject({ name: "Clip Wallet", icon: DEFAULT_IDENTITY.icon }); + expect(entry.methods).toContain("stx_signMessage"); + // @stacks/connect-ui getProviderFromId + expect(entry.id.split(".").reduce((o: any, k) => o?.[k], win)).toBe(provider); + expect(w.LeatherProvider).toBe(leather); + expect(w.StacksProvider).toBeUndefined(); + stop(); + expect(w.wbip_providers.map((p: { id: string }) => p.id)).toEqual(["LeatherProvider"]); + }); + + it("creates the registry when none exists and uses a kit wallet's own identity", () => { + const win = newWindow(); + const { entry } = installStacksProvider(win, { name: "Acme Wallet", icon: DEFAULT_IDENTITY.icon, rdns: "com.acme" }, [TESTNET], direct(fakeRouter().d), { globalKey: "acme" }); + expect((win as unknown as { wbip_providers: unknown[] }).wbip_providers).toEqual([entry]); + expect(entry).toMatchObject({ id: "acme.stacks", name: "Acme Wallet" }); + }); + + it("request() answers JSON-RPC responses the way @stacks/connect reads them", async () => { + const { d } = fakeRouter({ approve: (r) => (r.method === "stx_signMessage" ? { signature: "aa", publicKey: PUB } : { txid: "cd".repeat(32) }) }); + const p = new ClipStacksProvider(DEFAULT_IDENTITY, [TESTNET], direct(d)); + const seen: unknown[] = []; + const unlisten = p.listen("stx_accountChange", (a) => seen.push(a)); + const res = await p.request("getAddresses"); + expect(res).toMatchObject({ jsonrpc: "2.0", result: { addresses: [{ symbol: "STX", address: ST, publicKey: PUB }] } }); + expect(seen).toEqual([[{ address: ST, publicKey: PUB }]]); + unlisten(); + expect(await p.request("stx_signMessage", { message: "hello" })).toMatchObject({ result: { signature: "aa", publicKey: PUB } }); + expect(await p.request("stx_transferStx", { recipient: OTHER_ST, amount: "1", network: "testnet" })).toMatchObject({ result: { txid: "cd".repeat(32) } }); + await expect(p.request("stx_transferStx", { recipient: OTHER_ST, amount: "1", network: "devnet" })).rejects.toMatchObject({ jsonrpc: "2.0", error: { code: -32602 } }); + await expect(p.request("stx_updateProfile", {})).rejects.toMatchObject({ error: { code: -32601 } }); + }); + + it("maps rejections to SIP-030 codes", () => { + expect(toSip030Error(rpcError.userRejected()).code).toBe(-32000); + expect(toSip030Error(rpcError.unauthorized()).code).toBe(-32002); + expect(toSip030Error(rpcError.invalidParams("x")).code).toBe(-32602); + expect(toSip030Error(new Error("boom")).code).toBe(-32603); + }); +}); diff --git a/packages/1mask/test/tron.test.ts b/packages/1mask/test/tron.test.ts new file mode 100644 index 00000000..ebb48551 --- /dev/null +++ b/packages/1mask/test/tron.test.ts @@ -0,0 +1,210 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { createMemoryPermissionStore } from "../src/background/index.js"; +import { createTronDispatcher, tronInjectedAllowlist } from "../src/background/tron.js"; +import { type TIP6963ProviderDetail, installTronProvider } from "../src/inpage/tron.js"; +import type { EventListener, InpageTransport } from "../src/inpage/transport.js"; +import { resolveIdentity } from "../src/shared/config.js"; +import { ProviderRpcError } from "../src/shared/errors.js"; +import type { ExposedAccount, OneMaskEvent } from "../src/shared/protocol.js"; +import { TRON_INJECTED, tronChainIdOf, tronNetworkIdOf } from "../src/shared/tron-methods.js"; +import { newWindow } from "./helpers.js"; + +const asset = (networkId: string) => ({ key: "trx", symbol: "TRX", name: "TRON", decimals: 6, networkId }); +const net = (id: string, name: string, rpc: string): Network => ({ id, family: "tron", name, nativeAsset: asset(id), testnet: true, rpcUrls: [rpc], explorerUrl: "https://nile.tronscan.org" }); +const NILE = net("tron:0xcd8690dc", "TRON Nile Testnet", "https://nile.trongrid.io"); +const MAINNET = { ...net("tron:0x2b6653dc", "TRON", "https://api.trongrid.io"), testnet: false }; +const NETWORKS: Network[] = [NILE, MAINNET]; +const ME = "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH"; +const PUB = "03ff21f8e64d3a3c0198edfbb7afdc79be959432e92e2f8a1984bb436a414b8edc"; +const ORIGIN = "https://dapp.example"; + +/** Router stand-in lending the same closures createOneMaskRouter lends the dispatchers. */ +function harness(handleImpl: (r: DappRequest) => unknown = () => ({ ok: true })) { + const permissions = createMemoryPermissionStore(); + const handled: DappRequest[] = []; + const listeners = new Set(); + const accounts: ExposedAccount[] = [{ address: ME, publicKey: PUB }]; + const emit = (family: Family, event: OneMaskEvent, data?: unknown) => listeners.forEach((l) => l(family, event, data)); + const requireNetwork = (_f: Family, _o: string, chain: string | undefined) => { + if (chain === undefined) return NILE; + const n = NETWORKS.find((x) => x.id === chain); + if (!n) throw new ProviderRpcError(4901, "Not connected to that network."); + return n; + }; + const d = createTronDispatcher({ + permitted: async (o, f) => !!(await permissions.has(o, f)), + requirePermission: async (o, f) => { + if (!(await permissions.has(o, f))) throw new ProviderRpcError(4100, "Connect first."); + }, + accounts: async () => accounts, + approve: async (req) => { + handled.push(req); + return handleImpl(req); + }, + connect: async (o, f, n, method, params) => { + handled.push({ id: "c", origin: o, via: "injected", family: f, networkId: n.id, method, params }); + await permissions.grant(o, f); + emit(f, "accountsChanged", accounts); + return accounts; + }, + makeReq: (o, f, n, method, params) => ({ id: String(handled.length), origin: o, via: "injected", family: f, networkId: n.id, method, params }), + requireNetwork, + revoke: async (o, f) => { + await permissions.revoke(o, f); + emit(f, "accountsChanged", []); + emit(f, "disconnect"); + }, + }); + const transport: InpageTransport & { calls: { method: string; params: unknown; chain?: string }[] } = { + calls: [], + async request(family, method, params, chain) { + transport.calls.push({ method, params, ...(chain !== undefined ? { chain } : {}) }); + expect(family).toBe("tron"); + // The page→background hop is JSON (ports serialise). + return JSON.parse(JSON.stringify((await d.dispatch(ORIGIN, method, params === undefined ? undefined : JSON.parse(JSON.stringify(params)), chain)) ?? null)); + }, + onEvent(l) { + listeners.add(l); + return () => listeners.delete(l); + }, + destroy() {}, + }; + return { d, handled, permissions, transport }; +} + +describe("TRON background dispatcher", () => { + it("allows only the TRON wire methods", async () => { + expect([...tronInjectedAllowlist()].sort()).toEqual([TRON_INJECTED.accounts, TRON_INJECTED.connect, TRON_INJECTED.disconnect, "tron_signMessage", "tron_signTransaction"].sort()); + const { d } = harness(); + await expect(d.dispatch(ORIGIN, "tron_sendRawTransaction", {}, undefined)).rejects.toMatchObject({ code: 4200 }); + await expect(d.dispatch(ORIGIN, "tron_signAndSendTransaction", {}, undefined)).rejects.toMatchObject({ code: 4200 }); + }); + + it("reads accounts silently, connects through an approval, signs only for connected accounts", async () => { + const { d, handled } = harness(); + expect(await d.dispatch(ORIGIN, TRON_INJECTED.accounts, undefined, undefined)).toEqual([]); + await expect(d.dispatch(ORIGIN, "tron_signMessage", { address: ME, message: "hi" }, NILE.id)).rejects.toMatchObject({ code: 4100 }); + expect(await d.dispatch(ORIGIN, TRON_INJECTED.connect, {}, NILE.id)).toEqual([{ address: ME, publicKey: PUB }]); + expect(handled.map((h) => [h.method, h.networkId])).toEqual([[TRON_INJECTED.connect, NILE.id]]); + await expect(d.dispatch(ORIGIN, "tron_signMessage", { address: "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", message: "hi" }, NILE.id)).rejects.toMatchObject({ code: 4100 }); + await expect(d.dispatch(ORIGIN, "tron_signTransaction", { address: ME }, NILE.id)).rejects.toMatchObject({ code: -32602 }); + await expect(d.dispatch(ORIGIN, "tron_signMessage", { address: ME, message: "hi" }, "tron:0x94a9059e")).rejects.toMatchObject({ code: 4901 }); + await d.dispatch(ORIGIN, "tron_signTransaction", { address: ME, transaction: { raw_data_hex: "0a00" } }, MAINNET.id); + expect(handled.at(-1)).toMatchObject({ family: "tron", networkId: MAINNET.id, method: "tron_signTransaction", origin: ORIGIN }); + expect(await d.dispatch(ORIGIN, TRON_INJECTED.disconnect, undefined, undefined)).toBeNull(); + expect(await d.dispatch(ORIGIN, TRON_INJECTED.accounts, undefined, undefined)).toEqual([]); + }); +}); + +describe("TRON injected provider (TIP-1193 + TIP-6963)", () => { + const install = (handleImpl?: (r: DappRequest) => unknown, opts = {}) => { + const win = newWindow(); + const h = harness(handleImpl); + const seen: TIP6963ProviderDetail[] = []; + win.addEventListener("TIP6963:announceProvider", (e) => seen.push((e as CustomEvent).detail)); + const identity = resolveIdentity({}); + const r = installTronProvider(win, identity, NETWORKS, h.transport, opts); + return { win, h, seen, r, identity }; + }; + + it("announces its own identity (TIP-6963) and stays off other wallets' globals", () => { + const { win, seen, r, identity } = install(); + expect(seen).toHaveLength(1); + expect(seen[0]!.info).toMatchObject({ name: identity.name, icon: identity.icon, rdns: identity.rdns }); + expect(seen[0]!.info.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + expect(Object.isFrozen(seen[0])).toBe(true); + expect(seen[0]!.provider).toBe(r.provider); + win.dispatchEvent(new Event("TIP6963:requestProvider")); + expect(seen).toHaveLength(2); + expect(seen[1]!.info.uuid).toBe(seen[0]!.info.uuid); + const w = win as unknown as Record; + expect((w.clipwallet as Record).tron).toBe(r.provider); + expect(w.tron).toBeUndefined(); + expect(w.tronWeb).toBeUndefined(); + expect(w.tronLink).toBeUndefined(); + expect((r.provider as unknown as Record).isTronLink).toBeUndefined(); + r.stop(); + win.dispatchEvent(new Event("TIP6963:requestProvider")); + expect(seen).toHaveLength(2); + }); + + it("claims window.tron only when asked and nothing owns it", () => { + const a = install(undefined, { claimWindowTron: true }); + expect((a.win as unknown as { tron?: unknown }).tron).toBe(a.r.provider); + const win = newWindow(); + const other = { isTronLink: true }; + (win as unknown as { tron: unknown }).tron = other; + const r = installTronProvider(win, resolveIdentity({}), NETWORKS, harness().transport, { claimWindowTron: true }); + expect(r.claimedWindowTron).toBe(false); + expect((win as unknown as { tron: unknown }).tron).toBe(other); + }); + + it("eth_requestAccounts, eth_chainId, wallet_switchEthereumChain and the events", async () => { + const { r, h } = install(); + const p = r.provider; + const events: [string, unknown][] = []; + for (const e of ["connect", "accountsChanged", "chainChanged", "disconnect"]) expect(p.on(e, (x: unknown) => events.push([e, x]))).toBe(p); + expect(await p.request({ method: "eth_chainId" })).toBe("0xcd8690dc"); + expect(await p.request({ method: "eth_requestAccounts" })).toEqual([ME]); + expect(h.transport.calls.map((c) => [c.method, c.chain])).toEqual([ + [TRON_INJECTED.accounts, NILE.id], + [TRON_INJECTED.connect, NILE.id], + ]); + expect(events).toEqual([ + ["accountsChanged", [ME]], + ["connect", { chainId: "0xcd8690dc" }], + ]); + expect(await p.request({ method: "eth_requestAccounts" })).toEqual([ME]); // already connected: no second prompt + expect(h.handled.filter((x) => x.method === TRON_INJECTED.connect)).toHaveLength(1); + + expect(await p.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x2b6653dc" }] })).toBeNull(); + expect(events.at(-1)).toEqual(["chainChanged", { chainId: "0x2b6653dc" }]); + expect(await p.request({ method: "eth_chainId" })).toBe("0x2b6653dc"); + await expect(p.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x94a9059e" }] })).rejects.toMatchObject({ code: 4902 }); + await expect(p.request({ method: "wallet_switchEthereumChain", params: [{}] })).rejects.toMatchObject({ code: -32602 }); + await expect(p.request({ method: "tron_requestAccounts" })).rejects.toMatchObject({ code: 4200 }); + await expect(p.request({ method: "eth_sign", params: [] })).rejects.toMatchObject({ code: 4200 }); + + await p.disconnect(); + expect(events.slice(-2)).toEqual([ + ["accountsChanged", []], + ["disconnect", expect.objectContaining({ code: 4900 })], + ]); + }); + + it("signs through the TronWeb subset on the current chain", async () => { + const { r, h } = install((req) => (req.method === "tron_signMessage" ? { signature: "0xabc" } : { ...(req.params as { transaction: object }).transaction, signature: ["11"] })); + const tw = r.provider.tronWeb; + expect(tw.ready).toBe(false); + expect(tw.defaultAddress).toEqual({ base58: false, hex: false }); + await expect(tw.trx.sign({ raw_data_hex: "0a00" })).rejects.toMatchObject({ code: 4100 }); + await r.provider.request({ method: "eth_requestAccounts" }); + expect(tw.ready).toBe(true); + expect(tw.defaultAddress).toEqual({ base58: ME, hex: "41c8599111f29c1e1e061265b4af93ea1f274ad78a" }); + expect(tw.fullNode.host).toBe("https://nile.trongrid.io"); + + expect(await tw.trx.sign({ txID: "aa", raw_data_hex: "0a00" })).toEqual({ txID: "aa", raw_data_hex: "0a00", signature: ["11"] }); + expect(h.handled.at(-1)).toMatchObject({ method: "tron_signTransaction", networkId: NILE.id, params: { address: ME, transaction: { txID: "aa", raw_data_hex: "0a00" } } }); + expect(await tw.trx.signMessageV2("hello")).toBe("0xabc"); + expect(h.handled.at(-1)).toMatchObject({ method: "tron_signMessage", params: { address: ME, message: "hello" } }); + await tw.trx.signMessageV2(new Uint8Array([0, 255])); + expect(h.handled.at(-1)?.params).toEqual({ address: ME, message: "00ff", encoding: "hex" }); + + await expect(tw.trx.sign("deadbeef")).rejects.toMatchObject({ code: 4200 }); + await expect(tw.trx.sign({ raw_data_hex: "0a00" }, "a".repeat(64))).rejects.toMatchObject({ code: -32602 }); + await expect(tw.trx.multiSign()).rejects.toMatchObject({ code: 4200 }); + await expect(tw.trx._signTypedData()).rejects.toMatchObject({ code: 4200 }); + await r.provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x2b6653dc" }] }); + await tw.trx.signMessageV2("on mainnet"); + expect(h.handled.at(-1)?.networkId).toBe(MAINNET.id); + }); + + it("maps chain ids both ways", () => { + expect(tronNetworkIdOf("0x2B6653DC")).toBe("tron:0x2b6653dc"); + expect(tronNetworkIdOf("728126428")).toBe("tron:0x2b6653dc"); + expect(tronNetworkIdOf("tron")).toBeNull(); + expect(tronChainIdOf("tron:0xcd8690dc")).toBe("0xcd8690dc"); + expect(tronChainIdOf("eip155:1")).toBeNull(); + }); +}); diff --git a/packages/1mask/test/walletconnect-bch.test.ts b/packages/1mask/test/walletconnect-bch.test.ts new file mode 100644 index 00000000..5f9e2fbd --- /dev/null +++ b/packages/1mask/test/walletconnect-bch.test.ts @@ -0,0 +1,140 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import { describe, expect, it } from "vitest"; +import { createWalletConnectWallet, mapProposalNamespaces, type WalletKitLike, type WcSessionLike } from "../src/walletconnect/index.js"; +import { BTC_ADDR, NETWORKS, tick } from "./helpers.js"; + +/** + * Bitcoin Cash over WalletConnect, wc2-bch-bcr (https://github.com/mainnet-pat/wc2-bch-bcr): namespace "bch", chains + * "bch:bitcoincash" / "bch:bchtest" aliased to the wallet's bip122 BCH networks, session accounts "bch:" + * as Cashonize builds them. The public "abandon … about" BCH account 0, vault (mainnet) spelling. + */ +const BCH_MAIN = "bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6"; +const BCH_TEST = "bchtest:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnqseeszx8x"; +const CHIPNET = "bip122:00000000040ba9641ba98a37b2e5ceea"; +const MAINNET = "bip122:000000000000000000651ef99cb9fcbe"; +const BTC_TESTNET = "bip122:000000000933ea01ad0ee984209779ba"; + +const asset = (networkId: string) => ({ key: "bch", symbol: "BCH", name: "Bitcoin Cash", decimals: 8, networkId }); +const BCH_NETS: Network[] = [ + { id: CHIPNET, family: "bitcoincash", name: "Bitcoin Cash Chipnet", nativeAsset: asset(CHIPNET), testnet: true, rpcUrls: [], explorerUrl: "" }, + { id: MAINNET, family: "bitcoincash", name: "Bitcoin Cash", nativeAsset: asset(MAINNET), testnet: false, rpcUrls: [], explorerUrl: "" }, +]; +const networks = [...NETWORKS, ...BCH_NETS]; +const addressesFor = (_chain: string, family: Family): string[] => (family === "bitcoincash" ? [BCH_MAIN] : family === "bitcoin" ? [BTC_ADDR] : []); + +const BCH_METHODS = ["bch_getAddresses", "bch_signTransaction", "bch_signMessage"]; +const proposal = { + requiredNamespaces: { bch: { chains: ["bch:bchtest"], methods: BCH_METHODS, events: ["addressesChanged"] } }, + optionalNamespaces: { + bch: { chains: ["bch:bitcoincash", "bch:bchreg"], methods: BCH_METHODS, events: ["addressesChanged"] }, + bip122: { chains: [BTC_TESTNET], methods: ["signPsbt"], events: ["bip122_addressesChanged"] }, + }, +}; + +describe("wc2-bch-bcr namespace mapping", () => { + it("aliases bch:* chains to the BCH networks and spells session accounts per chain", () => { + const m = mapProposalNamespaces(proposal, { networks, addressesFor }); + expect(m.ok).toBe(true); + if (!m.ok) return; + expect(m.namespaces.bch).toEqual({ + chains: ["bch:bchtest", "bch:bitcoincash"], + accounts: [`bch:${BCH_TEST}`, `bch:${BCH_MAIN}`], + methods: BCH_METHODS, + events: ["addressesChanged"], + }); + expect(m.unsupported.chains).toEqual(["bch:bchreg"]); + // the Bitcoin bip122 namespace is untouched + expect(m.namespaces.bip122).toEqual({ chains: [BTC_TESTNET], accounts: [`${BTC_TESTNET}:${BTC_ADDR}`], methods: ["signPsbt"], events: ["bip122_addressesChanged"] }); + }); + + it("refuses bch chains the wallet doesn't have", () => { + const m = mapProposalNamespaces(proposal, { networks: NETWORKS, addressesFor }); + expect(m).toMatchObject({ ok: false, reason: "UNSUPPORTED_CHAINS", unsupported: { chains: ["bch:bchtest"] } }); + }); +}); + +function fakeKit() { + const handlers = new Map void>(); + const sessions: Record = {}; + const calls: { name: string; args: any }[] = []; + const rec = (name: string) => async (args: any) => void calls.push({ name, args }); + const kit: WalletKitLike = { + pair: rec("pair"), + approveSession: async (args) => (calls.push({ name: "approveSession", args }), { topic: "t1" }), + rejectSession: rec("rejectSession"), + respondSessionRequest: rec("respondSessionRequest"), + disconnectSession: rec("disconnectSession"), + getActiveSessions: () => sessions, + emitSessionEvent: rec("emitSessionEvent"), + updateSession: rec("updateSession"), + approveSessionAuthenticate: rec("approveSessionAuthenticate"), + rejectSessionAuthenticate: rec("rejectSessionAuthenticate"), + formatAuthMessage: () => "", + on: (e, l) => void handlers.set(e, l), + off: (e) => void handlers.delete(e), + }; + return { kit, sessions, calls, fire: async (e: string, a: any) => (handlers.get(e)!(a), tick(5)) }; +} + +const peer = { name: "TapSwap", description: "", url: "https://tapswap.example", icons: [] }; + +describe("wc2-bch-bcr sessions (fake WalletKit)", () => { + async function setup() { + const f = fakeKit(); + const handled: DappRequest[] = []; + const summaries: { approvedChains: string[]; networks?: unknown }[] = []; + const w = await createWalletConnectWallet({ + projectId: "test", + metadata: { name: "Clip Wallet", description: "", url: "https://clip.example", icons: [] }, + networks, + addressesFor, + approveProposal: async (s) => (summaries.push(s as never), true), + handle: async (req) => (handled.push(req), { signedTransaction: "02", signedTransactionHash: "ab" }), + walletKitFactory: async () => f.kit, + }); + await f.fire("session_proposal", { id: 3, params: { id: 3, proposer: { metadata: peer }, ...proposal } }); + const approved = f.calls.find((c) => c.name === "approveSession")!.args.namespaces; + f.sessions.t1 = { topic: "t1", expiry: 0, peer: { metadata: peer }, namespaces: approved }; + return { w, f, handled, summaries, approved }; + } + + it("approves the session with bch accounts and names the networks on the connect screen", async () => { + const { approved, summaries } = await setup(); + expect(approved.bch.accounts).toEqual([`bch:${BCH_TEST}`, `bch:${BCH_MAIN}`]); + expect(summaries[0]!.approvedChains).toContain("bch:bchtest"); + }); + + it("answers bch_getAddresses locally and forwards signing to chains-bitcoincash on the aliased network", async () => { + const { f, handled } = await setup(); + await f.fire("session_request", { id: 10, topic: "t1", params: { chainId: "bch:bchtest", request: { method: "bch_getAddresses", params: {} } } }); + expect(f.calls.at(-1)).toMatchObject({ name: "respondSessionRequest", args: { response: { id: 10, result: [BCH_TEST] } } }); + expect(handled).toHaveLength(0); + + await f.fire("session_request", { id: 11, topic: "t1", params: { chainId: "bch:bchtest", request: { method: "bch_signTransaction", params: { transaction: "00" } } } }); + expect(handled[0]).toMatchObject({ family: "bitcoincash", networkId: CHIPNET, method: "bch_signTransaction", via: "walletconnect", origin: "https://tapswap.example.unverified.invalid" }); + expect(f.calls.at(-1)).toMatchObject({ args: { response: { id: 11, result: { signedTransaction: "02" } } } }); + + await f.fire("session_request", { id: 12, topic: "t1", params: { chainId: "bch:bitcoincash", request: { method: "bch_signMessage", params: { message: "hi" } } } }); + expect(handled[1]).toMatchObject({ family: "bitcoincash", networkId: MAINNET }); + + await f.fire("session_request", { id: 13, topic: "t1", params: { chainId: BTC_TESTNET, request: { method: "signPsbt", params: {} } } }); + expect(handled[2]).toMatchObject({ family: "bitcoin", networkId: BTC_TESTNET, method: "signPsbt" }); + }); + + it("refuses a bch chain outside the session", async () => { + const { f } = await setup(); + await f.fire("session_request", { id: 20, topic: "t1", params: { chainId: "bch:bchreg", request: { method: "bch_getAddresses", params: {} } } }); + expect(f.calls.at(-1)).toMatchObject({ args: { response: { id: 20, error: { code: 5100 } } } }); + }); + + it("notifyAccountsChanged updates the session and emits addressesChanged with CashAddrs", async () => { + const { w, f } = await setup(); + await w.notifyAccountsChanged(); + const upd = f.calls.find((c) => c.name === "updateSession")!; + expect(upd.args.namespaces.bch.accounts).toEqual([`bch:${BCH_TEST}`, `bch:${BCH_MAIN}`]); + const ev = f.calls.filter((c) => c.name === "emitSessionEvent").map((c) => c.args); + expect(ev).toContainEqual({ topic: "t1", event: { name: "addressesChanged", data: [BCH_TEST] }, chainId: "bch:bchtest" }); + expect(ev).toContainEqual({ topic: "t1", event: { name: "addressesChanged", data: [BCH_MAIN] }, chainId: "bch:bitcoincash" }); + expect(ev).toContainEqual({ topic: "t1", event: { name: "bip122_addressesChanged", data: [`${BTC_TESTNET}:${BTC_ADDR}`] }, chainId: BTC_TESTNET }); + }); +}); diff --git a/packages/1mask/test/xrpl.test.ts b/packages/1mask/test/xrpl.test.ts new file mode 100644 index 00000000..377089b0 --- /dev/null +++ b/packages/1mask/test/xrpl.test.ts @@ -0,0 +1,146 @@ +import type { DappRequest, Family, Network } from "@clip-wallet/core"; +import type { Wallet } from "@wallet-standard/base"; +import { describe, expect, it } from "vitest"; +import { createXrplDispatcher, type XrplRouterInternals } from "../src/background/xrpl.js"; +import { ClipXrplWallet, XRPL_FEATURES, installXrpl, xrplChains } from "../src/inpage/xrpl.js"; +import type { InpageTransport } from "../src/inpage/transport.js"; +import { DEFAULT_IDENTITY } from "../src/shared/config.js"; +import { ProviderRpcError } from "../src/shared/errors.js"; +import { xrplAllowlist, xrplChainId } from "../src/shared/xrpl.js"; + +const asset = (id: string) => ({ key: "xrp", symbol: "XRP", name: "XRP", decimals: 6, networkId: id }); +const net = (id: string, family: Family = "xrpl"): Network => ({ id, family, name: id, nativeAsset: asset(id), testnet: id !== "xrpl:0", rpcUrls: [], explorerUrl: "" }); +const NETS = [net("xrpl:1"), net("xrpl:0"), net("solana:devnet", "solana")]; +const ME = "rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"; +const PUB = "031d68bc1a142e6766b2bdfb006ccfe135ef2e0e2e94abb5cf5c9ab6104776fbae"; +const OTHER = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"; +const TX = { TransactionType: "Payment", Account: ME, Destination: OTHER, Amount: "1" }; + +/** Router stand-in: one connected origin with one account; records approvals. */ +function internals(o: { permitted?: boolean } = {}) { + let permitted = o.permitted ?? true; + const approved: DappRequest[] = []; + const r: XrplRouterInternals = { + permitted: async () => permitted, + requirePermission: async () => { + if (!permitted) throw new ProviderRpcError(4100, "not connected"); + }, + accounts: async () => (permitted ? [{ address: ME, publicKey: PUB }] : []), + connect: async () => { + permitted = true; + return [{ address: ME, publicKey: PUB }]; + }, + approve: async (req) => { + approved.push(req); + return req.method === "xrpl:signTransaction" ? { signed_tx_blob: "AB" } : { tx_hash: "H", tx_json: {} }; + }, + makeReq: (origin, family, n, method, params) => ({ id: "1", origin, via: "injected", family, networkId: n.id, method, params }), + requireNetwork: (_f, _o, chain) => { + const n = NETS.find((x) => x.family === "xrpl" && x.id === (chain ?? "xrpl:1")); + if (!n) throw new ProviderRpcError(4901, "no such chain"); + return n; + }, + revoke: async () => { + permitted = false; + }, + }; + return { r, approved }; +} + +describe("XLS-72d background dispatcher", () => { + it("allowlists only the XLS-72d methods and maps network aliases", () => { + expect([...xrplAllowlist()].sort()).toEqual(["1mask_getAccounts", "standard:connect", "standard:disconnect", "xrpl:signAndSubmitTransaction", "xrpl:signTransaction"].sort()); + expect(xrplChainId("xrpl:testnet")).toBe("xrpl:1"); + expect(xrplChainId("xrpl:21337")).toBe("xrpl:21337"); + expect(xrplChainId("eip155:1")).toBeUndefined(); + }); + + it("connects, then approves a sign request with the network and signer checked", async () => { + const { r, approved } = internals({ permitted: false }); + const d = createXrplDispatcher(r); + await expect(d.dispatch("https://dapp.example", "xrpl:signTransaction", { tx_json: TX, account: ME, network: "xrpl:1" }, "xrpl:1")).rejects.toMatchObject({ code: 4100 }); + expect(await d.dispatch("https://dapp.example", "1mask_getAccounts", undefined, undefined)).toEqual([]); + expect(await d.dispatch("https://dapp.example", "standard:connect", undefined, undefined)).toEqual([{ address: ME, publicKey: PUB }]); + expect(await d.dispatch("https://dapp.example", "xrpl:signTransaction", { tx_json: TX, account: ME, network: "xrpl:testnet", options: { autofill: true } }, "xrpl:1")).toEqual({ signed_tx_blob: "AB" }); + expect(approved[0]).toMatchObject({ family: "xrpl", networkId: "xrpl:1", method: "xrpl:signTransaction", params: { tx_json: TX, account: ME, network: "xrpl:1", options: { autofill: true } } }); + await d.dispatch("https://dapp.example", "xrpl:signAndSubmitTransaction", { tx_json: TX, account: ME, network: "xrpl:0" }, "xrpl:0"); + expect(approved[1]).toMatchObject({ networkId: "xrpl:0", method: "xrpl:signAndSubmitTransaction" }); + }); + + it("refuses other accounts, mixed networks, unknown networks and other methods", async () => { + const { r, approved } = internals(); + const d = createXrplDispatcher(r); + await expect(d.dispatch("https://dapp.example", "xrpl:signTransaction", { tx_json: TX, account: OTHER, network: "xrpl:1" }, undefined)).rejects.toMatchObject({ code: 4100 }); + await expect(d.dispatch("https://dapp.example", "xrpl:signTransaction", { tx_json: { ...TX, Account: OTHER }, account: ME }, undefined)).rejects.toMatchObject({ code: 4100 }); + await expect(d.dispatch("https://dapp.example", "xrpl:signTransaction", { tx_json: TX, account: ME, network: "xrpl:0" }, "xrpl:1")).rejects.toMatchObject({ code: -32602 }); + await expect(d.dispatch("https://dapp.example", "xrpl:signTransaction", { tx_json: TX, account: ME, network: "xrpl:21337" }, undefined)).rejects.toMatchObject({ code: 4901 }); + await expect(d.dispatch("https://dapp.example", "xrpl:signTransaction", { account: ME }, undefined)).rejects.toMatchObject({ code: -32602 }); + await expect(d.dispatch("https://dapp.example", "xrpl:signMessage", {}, undefined)).rejects.toMatchObject({ code: 4200 }); + expect(approved).toEqual([]); + expect(await d.dispatch("https://dapp.example", "standard:disconnect", undefined, undefined)).toBeNull(); + }); +}); + +/** Transport stand-in that answers like the background would. */ +function transport() { + const sent: { family: Family; method: string; params?: unknown; chain?: string }[] = []; + let listener: ((f: Family, e: string, d: unknown) => void) | undefined; + const t: InpageTransport = { + request: async (family, method, params, chain) => { + sent.push({ family, method, params, chain }); + if (method === "standard:connect" || method === "1mask_getAccounts") return [{ address: ME, publicKey: PUB }]; + if (method === "xrpl:signTransaction") return { signed_tx_blob: "BLOB", extra: 1 }; + if (method === "xrpl:signAndSubmitTransaction") return { tx_hash: "HASH", tx_json: { validated: true } }; + return null; + }, + onEvent: (l) => { + listener = l as typeof listener; + return () => (listener = undefined); + }, + } as InpageTransport; + return { t, sent, emit: (e: string, d: unknown) => listener?.("xrpl", e, d) }; +} + +describe("XLS-72d in-page wallet", () => { + it("registers under the wallet's own identity with the XLS-72d features and chains", () => { + const registered: Wallet[] = []; + const win = {} as Window; + const { t } = transport(); + const out = installXrpl(win, DEFAULT_IDENTITY, NETS, t, { register: (w) => void registered.push(w) })!; + expect(registered).toEqual([out.wallet]); + expect(out.wallet.name).toBe(DEFAULT_IDENTITY.name); + expect(out.wallet.chains).toEqual(["xrpl:1", "xrpl:0"]); + expect(Object.keys(out.wallet.features).sort()).toEqual([...XRPL_FEATURES].sort()); + expect((win as unknown as { clipwallet: { xrpl: unknown } }).clipwallet.xrpl).toBe(out.wallet); + out.stop(); + expect(installXrpl({} as Window, DEFAULT_IDENTITY, [net("solana:devnet", "solana")], t, { register: () => {} })).toBeUndefined(); + expect(xrplChains(NETS)).toEqual(["xrpl:1", "xrpl:0"]); + }); + + it("connects, signs with the connected account and maps aliases to the CAIP-2 chain", async () => { + const { t, sent } = transport(); + const w = new ClipXrplWallet({ ...DEFAULT_IDENTITY, name: "Kit Wallet" }, NETS, t); + const { accounts } = await w.features["standard:connect"].connect(); + expect(accounts.map((a) => a.address)).toEqual([ME]); + expect(w.name).toBe("Kit Wallet"); + const signed = await w.features["xrpl:signTransaction"].signTransaction({ tx_json: TX, account: accounts[0]!, network: "xrpl:testnet", options: { autofill: true } }); + expect(signed).toEqual({ signed_tx_blob: "BLOB" }); + expect(sent.at(-1)).toEqual({ family: "xrpl", method: "xrpl:signTransaction", params: { tx_json: TX, account: ME, network: "xrpl:1", options: { autofill: true } }, chain: "xrpl:1" }); + expect(await w.features["xrpl:signAndSubmitTransaction"].signAndSubmitTransaction({ tx_json: TX, account: accounts[0]!, network: "xrpl:0" })).toEqual({ tx_hash: "HASH", tx_json: { validated: true } }); + }); + + it("refuses accounts that aren't connected and networks it doesn't have", async () => { + const { t, emit } = transport(); + const w = new ClipXrplWallet(DEFAULT_IDENTITY, NETS, t); + const stranger = { address: OTHER, publicKey: new Uint8Array(), chains: ["xrpl:1"], features: [] } as never; + await expect(w.features["xrpl:signTransaction"].signTransaction({ tx_json: TX, account: stranger, network: "xrpl:1" })).rejects.toMatchObject({ code: 4100 }); + const { accounts } = await w.features["standard:connect"].connect(); + await expect(w.features["xrpl:signTransaction"].signTransaction({ tx_json: TX, account: accounts[0]!, network: "xrpl:21337" })).rejects.toMatchObject({ code: 4901 }); + await expect(w.features["xrpl:signTransaction"].signTransaction({ tx_json: TX, account: accounts[0]!, network: "xrpl:2" })).rejects.toMatchObject({ code: 4901 }); + const changes: unknown[] = []; + w.features["standard:events"].on("change", (c) => changes.push(c)); + emit("disconnect", undefined); + expect(w.accounts).toEqual([]); + expect(changes).toHaveLength(1); + }); +}); diff --git a/packages/backup-client/src/index.ts b/packages/backup-client/src/index.ts index 2464d36b..33293641 100644 --- a/packages/backup-client/src/index.ts +++ b/packages/backup-client/src/index.ts @@ -35,6 +35,13 @@ import { tokenFromLink, } from "./protocol.js"; +/** `url` without trailing slashes (a scan, not a regex: linear on any input). */ +function withoutTrailingSlashes(url: string): string { + let end = url.length; + while (end > 0 && url[end - 1] === "/") end--; + return url.slice(0, end); +} + export * from "./protocol.js"; export interface BackupClientOptions { @@ -88,7 +95,7 @@ export class BackupClient { private readonly random: (n: number) => Uint8Array; constructor(opts: BackupClientOptions) { - this.base = `${opts.baseUrl.replace(/\/+$/, "")}/${API_VERSION}`; + this.base = `${withoutTrailingSlashes(opts.baseUrl)}/${API_VERSION}`; this.f = opts.fetch ?? globalThis.fetch.bind(globalThis); this.session = opts.session ?? null; this.now = opts.now ?? Date.now; diff --git a/packages/chains-antelope/LICENSE b/packages/chains-antelope/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-antelope/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-antelope/NOTICE b/packages/chains-antelope/NOTICE new file mode 100644 index 00000000..2f081dce --- /dev/null +++ b/packages/chains-antelope/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-antelope +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-antelope/README.md b/packages/chains-antelope/README.md new file mode 100644 index 00000000..a8c2fc8d --- /dev/null +++ b/packages/chains-antelope/README.md @@ -0,0 +1,112 @@ +# @clip-wallet/chains-antelope + +Antelope `ChainModule` for Clip Wallet: Vaulta (formerly EOS), Telos and XPR Network, with their testnets. One K1 key +signs for all three. It builds, decodes and checks transactions in plain words and never touches keys: `prepare()` +returns SHA-256(chain_id ‖ packed_trx ‖ 32 zero bytes) for the vault to sign (`ecdsa-secp256k1`, canonical K1), and +`finalize()` checks the signature (verify, canonical, recovery id) before anything is assembled or sent. + +No SDK at runtime: names, assets, keys, `SIG_K1_` strings, transactions and the ABI (de)serialiser are hand-written on +`@noble/hashes`, `@noble/curves` (verification and recovery only) and `@scure/base`. Tests check bytes, transaction +ids, digests and signature strings against `@wharfkit/antelope` (devDependency) and verify with +`Signature.verifyDigest` / `recoverDigest`. + +## Networks + +| network | NetworkId | chain API (public, no key) | Hyperion | explorer | +|---|---|---|---|---| +| Jungle4 (Vaulta testnet) | `antelope:73e4385a2708e6d7048834fbc1079f2f` | jungle4.greymass.com, jungle4.cryptolions.io, jungle4.api.eosnation.io | jungle4.cryptolions.io | jungle4.unicove.com | +| Vaulta | `antelope:aca376f206b8fc25a6ed44dbdc66547c` | eos.greymass.com, eos.api.eosnation.io | eos.eosusa.io | bloks.io | +| Telos Testnet | `antelope:1eaa0824707c8c16bd25145493bf062a` | testnet.telos.net, telos-testnet.cryptolions.io | testnet.telos.net | explorer-test.telos.net | +| Telos | `antelope:4667b205c6838ef70ff7988f6e8257e8` | mainnet.telos.net, telos.greymass.com | mainnet.telos.net | explorer.telos.net | +| XPR Network Testnet | `antelope:71ee83bcf52142d61019d95f9cc5427b` | tn1.protonnz.com, testnet.protonchain.com, proton-testnet.cryptolions.io | — | testnet.explorer.xprnetwork.org | +| XPR Network | `antelope:384da888112027f0321850a169f737c3` | proton.greymass.com, api.protonnz.com, proton.eosusa.io | proton.eosusa.io | explorer.xprnetwork.org | + +- Ids: ChainAgnostic namespaces `antelope/caip2.md` ("antelope:" + the first 32 hex of the chain id; its test cases + include EOS Mainnet and Telos Mainnet). `netOf` also takes the full chain id. +- Every endpoint answered `get_info` with the chain id above (October 2026). `send_transaction2` exists on all of + them (`push_transaction` is the fallback on a 404). `get_accounts_by_authorizers` is load-balanced on + testnet.telos.net (some backends 404), so the module tries every server, then Hyperion. + +## Keys, accounts, addresses + +`curve: "secp256k1"`, `derivationPath(i) = m/44'/194'/0'/0/i` (SLIP-44 194, TokenPocket). `Account.address` and +`addressFromPublicKey` give the key as `PUB_K1_…` (the vault's `antelopePublicKey`; legacy `EOS…` keys are parsed too). +Antelope accounts are 12-character names created on chain that point at keys, so: + +- `accounts(ctx)` / `receiveAddress(ctx)`: the names whose permission this key satisfies alone (weight ≥ threshold), from + `/v1/chain/get_accounts_by_authorizers`, else Hyperion `/v2/state/get_key_accounts`. `receiveAddress` returns the + first (an `active` one first). None → ClipError `antelope/no-account` in plain words ("No Jungle4 account uses this + key yet … create a free test account for your public key …" / on mainnets: an exchange, a friend or an + account-creation service creates it). +- `isAddress` = a valid account name (what you send to); keys are not addresses. `networksForAddress` = every Antelope + network (a name can exist on each, owned by different people). +- The public "abandon … about" key `PUB_K1_6zpSNY1Y…NK2aD4t` already has accounts made by strangers (kasekitest11 on + Jungle4, binancegold4 on Vaulta). Never use that phrase for anything real. + +## Assets + +- Vaulta: **EOS** (eosio.token, key `eos`) is still the system token: RAM, PowerUp and staking are priced in it. + **A** (Vaulta, core.vaulta, key `vaulta`; EOS and A swap 1:1 through core.vaulta) and **USDT** (Tether's own issue on + tethertether, key `usdt`) are shown beside it. Jungle4 has EOS and A. +- Telos: **TLOS**; XPR Network: **XPR** (eosio.token, 4 decimals). +- Balances: `get_currency_balance` for those; other tokens from Hyperion `state/get_tokens` (non-zero only). A token + that borrows a known symbol (EOS, A, USDT, TLOS, XPR, USD…) from another contract is `spam: true` (XPR mainnet has + an "XPR" on `ternary`). +- `getNfts` returns `[]` (AtomicAssets has no `Nft.standard` value in core). + +## Requests + +There is no dapp standard to implement on the wallet side (below), so the methods are Clip Wallet's own: + +| method | params | result | +|---|---|---| +| `antelope_signTransaction` | `{ transaction, account?, chainId? }` | `{ signatures: ["SIG_K1_…"], packed_trx, transaction_id }` | +| `antelope_signAndPushTransaction` | same | `{ transaction_id, processed? }` (send_transaction2) | + +`transaction` is the abieos JSON; the header (expiration, ref_block_num, ref_block_prefix) may be left out, and is then +filled once in `decode()` (TAPoS from the last irreversible block, expiry 5 minutes after head) and cached per +request, so the signature covers what was shown. Action `data` is packed hex, or JSON serialised with the contract's +ABI (`/v1/chain/get_abi`, cached). eosio.token-style transfers of the known token contracts use a built-in ABI, so +`buildTransfer` makes no ABI call. + +- Refused: another network, context-free actions or transaction extensions, an action no authorization of this key can + sign ("This transaction doesn't need your signature"), a permission of our account this key doesn't hold, and on + sign-and-push any other signer (co-signing is fine with `antelope_signTransaction`: "Also needs bob@active"). +- Signature: r ‖ s + recovery from the vault → `SIG_K1_` (recovery byte = recid + 31, checksum RIPEMD-160(sig ‖ "K1")). +- Chain refusals in plain words (`plainAntelopeError`): CPU/NET exhausted → per network (Vaulta: rent with PowerUp; + Telos: stake TLOS or PowerUp; XPR: the network covers CPU/NET for users, try again), RAM, overdrawn balance, + expired, authorization, missing recipient, duplicate. + +### decode() + +- Any action whose ABI struct is the eosio.token transfer shape (from, to, quantity: asset, memo): "Send 1.5000 EOS to + bob" with the memo, the balance change, a check that the recipient exists; an unknown contract's token → caution, a + look-alike → danger `known-scam`. +- `eosio::updateauth` / `deleteauth` / `linkauth` → **danger `account-takeover`**; `unlinkauth` → caution. +- Anything else readable through the contract's ABI: "Call {action} on {contract}" with every field, caution + `unknown-call`. Several actions: "Approve N operations" with "Action N" lines. +- No ABI, a type the serialiser doesn't know (floats, 128-bit integers, WebAuthn keys) or data that doesn't parse → + **blind**. +- `delay_sec` > 0 → caution. The fee line says there's no token fee (CPU/NET, or covered by the network on XPR). + +## Dapp connectivity: send/receive only + +Checked October 2026; none lets a wallet extension answer under its own name: + +- **WharfKit** (the current Antelope dapp SDK): the wallet *plugin* lives in the dapp (`@wharfkit/wallet-plugin-*`); + a wallet appears in a dapp only when the dapp ships that wallet's plugin. Nothing for an extension to implement. +- **Anchor Link / ESR** (EOSIO Signing Requests over a Buoy relay): the dapp's `wallet-plugin-anchor` shows "Anchor"; + a different wallet answering it would appear as Anchor. +- **Scatter-compatible injection** (`window.scatter`, ScatterJS; what TokenPocket and the WharfKit Scatter / + TokenPocket plugins use): answering it means pretending to be Scatter/TokenPocket (AGENTS.md rule 8). +- **XPR WebAuth** (proton-web-sdk) talks to WebAuth's own wallet and relay. + +So 1Mask has no Antelope provider; sends and receives work from the wallet itself. + +## Getting a testnet account (needs a person) + +| network | create an account for a public key | test tokens | +|---|---|---| +| Jungle4 | `POST https://jungle4.greymass.com/account/create` `{ accountName: "<9 chars>.gm", ownerKey, activeKey, network: "73e4…6c4d" }` → 201 (what `npx @wharfkit/cli account create` sends; no captcha). Or monitor.jungletestnet.io (Google reCAPTCHA). | monitor.jungletestnet.io faucet (reCAPTCHA). New accounts also need CPU: PowerUp paid by the account, or another account | +| Telos Testnet | `POST https://api-dev.telos.net/v1/testnet/account` `{ accountName, ownerKey, activeKey }` (the app.telos.net testnet developer page calls it; no captcha, one per IP per 24 h) | `GET https://api-dev.telos.net/v1/testnet/faucet/` (one per IP/account per 24 h) | +| XPR Testnet | `proton account:create` → `identity.api.dev.metalx.com` with an e-mail address and a 6-digit code sent to it (or the WebAuth testnet app) | `token.faucet::claim` on chain (`proton faucet:claim XPR @active`) | diff --git a/packages/chains-antelope/package.json b/packages/chains-antelope/package.json new file mode 100644 index 00000000..42f29512 --- /dev/null +++ b/packages/chains-antelope/package.json @@ -0,0 +1,79 @@ +{ + "name": "@clip-wallet/chains-antelope", + "version": "0.1.0", + "description": "Antelope (Vaulta, Telos, XPR Network) ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "antelope", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-antelope#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-antelope" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "devDependencies": { + "@wharfkit/antelope": "^4.0.2" + } +} diff --git a/packages/chains-antelope/src/abi.ts b/packages/chains-antelope/src/abi.ts new file mode 100644 index 00000000..f33a8e37 --- /dev/null +++ b/packages/chains-antelope/src/abi.ts @@ -0,0 +1,315 @@ +import { + Reader, + Writer, + formatAsset, + fromHex, + hex, + nameFromBigInt, + nameToBigInt, + packPublicKey, + packSignature, + parseAsset, + publicKeyString, + symbolFromBigInt, + symbolToBigInt, +} from "./bytes.js"; + +/** + * ABI-driven (de)serialisation for action data (eosio::abi/1.x JSON as `/v1/chain/get_abi` returns it): type aliases, + * structs with bases, `T[]`, `T?`, `T$` (binary extensions), variants, and the built-in types Clip Wallet meets in + * token, system and dapp contracts. Anything else (float types, 128-bit integers, fixed-size arrays, WebAuthn keys) + * throws AbiError, so the request becomes blind instead of being guessed. JSON follows abieos / WharfKit: 64-bit + * integers as decimal strings, `bytes` and checksums as hex, variants as [type, value]. + */ + +export interface Abi { + version?: string; + types?: { new_type_name: string; type: string }[]; + structs?: { name: string; base?: string; fields: { name: string; type: string }[] }[]; + actions?: { name: string; type: string; ricardian_contract?: string }[]; + variants?: { name: string; types: string[] }[]; +} + +export class AbiError extends Error { + constructor(message: string) { + super(message); + this.name = "AbiError"; + } +} + +/** eosio.token's transfer, so token sends never need an ABI round trip. */ +export const TOKEN_ABI: Abi = { + version: "eosio::abi/1.2", + structs: [{ name: "transfer", base: "", fields: [{ name: "from", type: "name" }, { name: "to", type: "name" }, { name: "quantity", type: "asset" }, { name: "memo", type: "string" }] }], + actions: [{ name: "transfer", type: "transfer" }], +}; + +const INT_SIZES: Record = { + int8: [1, true], + uint8: [1, false], + int16: [2, true], + uint16: [2, false], + int32: [4, true], + uint32: [4, false], + int64: [8, true], + uint64: [8, false], +}; + +const isObj = (v: unknown): v is Record => !!v && typeof v === "object" && !Array.isArray(v); + +function resolve(abi: Abi, type: string, depth = 0): string { + if (depth > 32) throw new AbiError(`type alias loop at ${type}`); + const alias = abi.types?.find((t) => t.new_type_name === type); + return alias ? resolve(abi, alias.type, depth + 1) : type; +} + +function bigOf(v: unknown, what: string): bigint { + if (typeof v === "bigint") return v; + if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v); + if (typeof v === "string" && /^-?\d+$/.test(v)) return BigInt(v); + throw new AbiError(`${what} isn't an integer`); +} + +function hexOf(v: unknown, bytes: number | null, what: string): Uint8Array { + if (typeof v !== "string" || !/^([0-9a-fA-F]{2})*$/.test(v) || (bytes !== null && v.length !== bytes * 2)) throw new AbiError(`${what} isn't valid hex`); + return fromHex(v); +} + +/** Microseconds since epoch ↔ "2024-01-01T00:00:00.000" (UTC, no zone, as Spring prints it). */ +function parseTime(v: unknown, what: string): number { + if (typeof v !== "string") throw new AbiError(`${what} isn't a time`); + const ms = Date.parse(/[zZ]|[+-]\d\d:\d\d$/.test(v) ? v : `${v}Z`); + if (Number.isNaN(ms)) throw new AbiError(`${what} isn't a time`); + return ms; +} +const timeText = (ms: number) => new Date(ms).toISOString().replace(/Z$/, ""); + +const SLOT_EPOCH_MS = 946684800000; // block_timestamp_type: half-seconds since 2000-01-01 + +export function serialize(abi: Abi, type: string, value: unknown, w: Writer = new Writer(), depth = 0): Writer { + if (depth > 32) throw new AbiError("value nests too deep"); + if (type.endsWith("$")) { + if (value !== undefined) serialize(abi, type.slice(0, -1), value, w, depth + 1); + return w; + } + if (type.endsWith("?")) { + if (value === undefined || value === null) return w.byte(0); + w.byte(1); + return serialize(abi, type.slice(0, -1), value, w, depth + 1); + } + if (type.endsWith("[]")) { + if (!Array.isArray(value)) throw new AbiError(`${type} needs an array`); + w.varuint32(value.length); + for (const v of value) serialize(abi, type.slice(0, -2), v, w, depth + 1); + return w; + } + const t = resolve(abi, type); + if (t !== type) return serialize(abi, t, value, w, depth + 1); + const int = INT_SIZES[t]; + if (int) return int[1] ? w.int(bigOf(value, t), int[0]) : w.uint(bigOf(value, t), int[0]); + switch (t) { + case "bool": + if (typeof value !== "boolean") throw new AbiError("bool needs true or false"); + return w.byte(value ? 1 : 0); + case "varuint32": + return w.varuint32(Number(bigOf(value, t))); + case "varint32": + return w.varint32(Number(bigOf(value, t))); + case "name": + if (typeof value !== "string") throw new AbiError("name needs a string"); + try { + return w.uint(nameToBigInt(value), 8); + } catch { + throw new AbiError(`invalid name ${value}`); + } + case "string": + if (typeof value !== "string") throw new AbiError("string needs a string"); + return w.blob(new TextEncoder().encode(value)); + case "bytes": + return w.blob(hexOf(value, null, t)); + case "checksum160": + return w.bytes(hexOf(value, 20, t)); + case "checksum256": + return w.bytes(hexOf(value, 32, t)); + case "checksum512": + return w.bytes(hexOf(value, 64, t)); + case "public_key": + try { + return w.bytes(packPublicKey(String(value))); + } catch { + throw new AbiError("unsupported public key"); + } + case "signature": + try { + return w.bytes(packSignature(String(value))); + } catch { + throw new AbiError("unsupported signature"); + } + case "time_point": + return w.int(BigInt(parseTime(value, t)) * 1000n, 8); + case "time_point_sec": + return w.uint(Math.floor(parseTime(value, t) / 1000), 4); + case "block_timestamp_type": + return w.uint(Math.round((parseTime(value, t) - SLOT_EPOCH_MS) / 500), 4); + case "symbol": { + const m = typeof value === "string" ? /^(\d{1,2}),([A-Z]{1,7})$/.exec(value) : null; + if (!m) throw new AbiError("symbol needs \"4,EOS\""); + return w.uint(symbolToBigInt({ precision: Number(m[1]), code: m[2]! }), 8); + } + case "symbol_code": + if (typeof value !== "string" || !/^[A-Z]{1,7}$/.test(value)) throw new AbiError("symbol_code needs \"EOS\""); + return w.uint(symbolToBigInt({ precision: 0, code: value }) >> 8n, 8); + case "asset": { + let a; + try { + a = parseAsset(String(value)); + } catch { + throw new AbiError(`invalid asset ${String(value)}`); + } + return w.int(a.amount, 8).uint(symbolToBigInt(a.symbol), 8); + } + case "extended_asset": + if (!isObj(value)) throw new AbiError("extended_asset needs { quantity, contract }"); + serialize(abi, "asset", value.quantity, w, depth + 1); + return serialize(abi, "name", value.contract, w, depth + 1); + } + const variant = abi.variants?.find((v) => v.name === t); + if (variant) { + if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "string") throw new AbiError(`${t} needs [type, value]`); + const i = variant.types.indexOf(value[0]); + if (i < 0) throw new AbiError(`${t} has no ${value[0]}`); + w.varuint32(i); + return serialize(abi, value[0], value[1], w, depth + 1); + } + const struct = abi.structs?.find((s) => s.name === t); + if (struct) { + if (!isObj(value)) throw new AbiError(`${t} needs an object`); + if (struct.base) serialize(abi, struct.base, value, w, depth + 1); + for (const f of struct.fields) { + if (value[f.name] === undefined && !f.type.endsWith("$") && !f.type.endsWith("?")) throw new AbiError(`${t}.${f.name} is missing`); + serialize(abi, f.type, value[f.name], w, depth + 1); + } + return w; + } + throw new AbiError(`unknown type ${t}`); +} + +export function deserialize(abi: Abi, type: string, r: Reader, depth = 0): unknown { + if (depth > 32) throw new AbiError("value nests too deep"); + if (type.endsWith("$")) return r.remaining > 0 ? deserialize(abi, type.slice(0, -1), r, depth + 1) : undefined; + if (type.endsWith("?")) return r.byte() ? deserialize(abi, type.slice(0, -1), r, depth + 1) : null; + if (type.endsWith("[]")) { + const n = r.varuint32(); + if (n > r.remaining) throw new AbiError("array longer than the data"); + return Array.from({ length: n }, () => deserialize(abi, type.slice(0, -2), r, depth + 1)); + } + const t = resolve(abi, type); + if (t !== type) return deserialize(abi, t, r, depth + 1); + const int = INT_SIZES[t]; + if (int) { + const v = int[1] ? r.int(int[0]) : r.uint(int[0]); + return int[0] > 4 ? v.toString() : Number(v); + } + switch (t) { + case "bool": { + const b = r.byte(); + if (b > 1) throw new AbiError("bad bool"); + return b === 1; + } + case "varuint32": + return r.varuint32(); + case "varint32": + return r.varint32(); + case "name": + return nameFromBigInt(r.uint(8)); + case "string": + try { + return new TextDecoder("utf-8", { fatal: true }).decode(r.blob()); + } catch { + throw new AbiError("string isn't UTF-8"); + } + case "bytes": + return hex(r.blob()); + case "checksum160": + return hex(r.bytes(20)); + case "checksum256": + return hex(r.bytes(32)); + case "checksum512": + return hex(r.bytes(64)); + case "public_key": { + if (r.byte() !== 0) throw new AbiError("only K1 public keys"); + return publicKeyString(r.bytes(33)); + } + case "time_point": + return timeText(Number(r.int(8) / 1000n)); + case "time_point_sec": + return timeText(Number(r.uint(4)) * 1000).replace(/\.000$/, ""); + case "block_timestamp_type": + return timeText(Number(r.uint(4)) * 500 + SLOT_EPOCH_MS); + case "symbol": { + const s = symbolFromBigInt(r.uint(8)); + return `${s.precision},${s.code}`; + } + case "symbol_code": + return symbolFromBigInt(r.uint(8) << 8n).code; + case "asset": { + const amount = r.int(8); + return formatAsset({ amount, symbol: symbolFromBigInt(r.uint(8)) }); + } + case "extended_asset": + return { quantity: deserialize(abi, "asset", r, depth + 1), contract: deserialize(abi, "name", r, depth + 1) }; + } + const variant = abi.variants?.find((v) => v.name === t); + if (variant) { + const i = r.varuint32(); + const vt = variant.types[i]; + if (!vt) throw new AbiError(`${t} index ${i} out of range`); + return [vt, deserialize(abi, vt, r, depth + 1)]; + } + const struct = abi.structs?.find((s) => s.name === t); + if (struct) { + const out: Record = struct.base ? (deserialize(abi, struct.base, r, depth + 1) as Record) : {}; + for (const f of struct.fields) { + const v = deserialize(abi, f.type, r, depth + 1); + if (v !== undefined) out[f.name] = v; + } + return out; + } + throw new AbiError(`unknown type ${t}`); +} + +/** The struct type an action's data uses, per the contract's ABI. */ +export function actionType(abi: Abi, action: string): string | undefined { + return abi.actions?.find((a) => a.name === action)?.type; +} + +/** Action data bytes → JSON (all bytes must be consumed). */ +export function decodeActionData(abi: Abi, action: string, data: Uint8Array): Record { + const type = actionType(abi, action); + if (!type) throw new AbiError(`the contract's ABI has no action ${action}`); + const r = new Reader(data); + const out = deserialize(abi, type, r); + if (r.remaining) throw new AbiError(`${r.remaining} bytes left over`); + if (!isObj(out)) throw new AbiError("action data isn't a struct"); + return out; +} + +export function encodeActionData(abi: Abi, action: string, value: unknown): Uint8Array { + const type = actionType(abi, action); + if (!type) throw new AbiError(`the contract's ABI has no action ${action}`); + return serialize(abi, type, value).done(); +} + +/** True when the action's struct is the eosio.token transfer shape (from, to, quantity: asset, memo). */ +export function isTokenTransfer(abi: Abi, action: string): boolean { + const type = actionType(abi, action); + const s = type ? abi.structs?.find((x) => x.name === resolve(abi, type)) : undefined; + if (!s || s.base) return false; + const want = [ + ["from", "name"], + ["to", "name"], + ["quantity", "asset"], + ["memo", "string"], + ]; + return s.fields.length === 4 && s.fields.every((f, i) => f.name === want[i]![0] && resolve(abi, f.type) === want[i]![1]); +} diff --git a/packages/chains-antelope/src/bytes.ts b/packages/chains-antelope/src/bytes.ts new file mode 100644 index 00000000..616be487 --- /dev/null +++ b/packages/chains-antelope/src/bytes.ts @@ -0,0 +1,289 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { ripemd160 } from "@noble/hashes/legacy.js"; +import { base58 } from "@scure/base"; + +/** + * Antelope wire primitives (Spring/Leap fc + abieos; https://docs.eosnetwork.com/docs/latest/advanced-topics/ + * transactions-protocol): little-endian integers, LEB128 varuint32, 64-bit names, assets, K1 keys and signatures. + * Cross-checked against @wharfkit/antelope in test/serializer.test.ts. + */ + +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.replace(/^0x/, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export const utf8 = (s: string) => new TextEncoder().encode(s); + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +/* ------------------------------------------------------------------ writer / reader */ + +export class Writer { + private chunks: number[] = []; + + byte(b: number): this { + this.chunks.push(b & 0xff); + return this; + } + bytes(b: Uint8Array): this { + for (const x of b) this.chunks.push(x); + return this; + } + /** Unsigned little-endian integer of `size` bytes. */ + uint(v: bigint | number, size: number): this { + let n = BigInt(v); + if (n < 0n || n >= 1n << BigInt(size * 8)) throw new RangeError("integer out of range"); + for (let i = 0; i < size; i++, n >>= 8n) this.chunks.push(Number(n & 0xffn)); + return this; + } + /** Signed little-endian integer (two's complement). */ + int(v: bigint | number, size: number): this { + const n = BigInt(v); + const bits = BigInt(size * 8); + if (n < -(1n << (bits - 1n)) || n >= 1n << (bits - 1n)) throw new RangeError("integer out of range"); + return this.uint(n < 0n ? (1n << bits) + n : n, size); + } + varuint32(v: number): this { + if (!Number.isInteger(v) || v < 0 || v > 0xffffffff) throw new RangeError("varuint32 out of range"); + let n = v >>> 0; + for (;;) { + if (n >>> 7) { + this.chunks.push(0x80 | (n & 0x7f)); + n >>>= 7; + } else { + this.chunks.push(n); + return this; + } + } + } + varint32(v: number): this { + if (!Number.isInteger(v) || v < -0x80000000 || v > 0x7fffffff) throw new RangeError("varint32 out of range"); + return this.varuint32(((v << 1) ^ (v >> 31)) >>> 0); + } + blob(b: Uint8Array): this { + return this.varuint32(b.length).bytes(b); + } + done(): Uint8Array { + return Uint8Array.from(this.chunks); + } +} + +export class Reader { + pos = 0; + constructor(private readonly buf: Uint8Array) {} + + get remaining(): number { + return this.buf.length - this.pos; + } + bytes(n: number): Uint8Array { + if (n < 0 || this.pos + n > this.buf.length) throw new RangeError("read past end"); + const out = this.buf.slice(this.pos, this.pos + n); + this.pos += n; + return out; + } + byte(): number { + return this.bytes(1)[0]!; + } + uint(size: number): bigint { + const b = this.bytes(size); + let n = 0n; + for (let i = size - 1; i >= 0; i--) n = (n << 8n) | BigInt(b[i]!); + return n; + } + int(size: number): bigint { + const n = this.uint(size); + const bits = BigInt(size * 8); + return n >= 1n << (bits - 1n) ? n - (1n << bits) : n; + } + varuint32(): number { + let v = 0; + let shift = 0; + for (;;) { + if (shift > 28) throw new RangeError("varuint32 too long"); + const b = this.byte(); + v |= (b & 0x7f) << shift; + if (!(b & 0x80)) return v >>> 0; + shift += 7; + } + } + varint32(): number { + const u = this.varuint32(); + return u & 1 ? ~(u >>> 1) : u >>> 1; + } + blob(): Uint8Array { + return this.bytes(this.varuint32()); + } +} + +/* ------------------------------------------------------------------ names */ + +const NAME_CHARS = ".12345abcdefghijklmnopqrstuvwxyz"; + +/** Account / action / table names (up to 12 chars of [.1-5a-z], a 13th of [.1-5a-j]) as uint64. */ +export function nameToBigInt(name: string): bigint { + if (!isName(name)) throw new Error(`invalid name: ${name}`); + let v = 0n; + for (let i = 0; i < 13; i++) { + const c = i < name.length ? NAME_CHARS.indexOf(name[i]!) : 0; + if (i < 12) v |= BigInt(c & 0x1f) << BigInt(64 - 5 * (i + 1)); + else v |= BigInt(c & 0x0f); + } + return v; +} + +export function nameFromBigInt(v: bigint): string { + let out = ""; + let tmp = v; + for (let i = 0; i <= 12; i++) { + const c = i === 0 ? Number(tmp & 0x0fn) : Number(tmp & 0x1fn); + out = NAME_CHARS[c]! + out; + tmp >>= i === 0 ? 4n : 5n; + } + return out.replace(/\.+$/, ""); +} + +/** A name the chain would store unchanged (no trailing dots, canonical round trip). */ +export function isName(name: string): boolean { + if (typeof name !== "string" || name.length > 13 || !/^[.1-5a-z]*$/.test(name)) return false; + if (name.length === 13 && !/[.1-5a-j]$/.test(name)) return false; + return !name.endsWith("."); +} + +/** An account a person can be: 1–12 characters, [a-z1-5.], not ending with ".". */ +export function isAccountName(name: string): boolean { + return /^[a-z1-5.]{1,12}$/.test(name) && !name.endsWith(".") && !name.startsWith("."); +} + +/* ------------------------------------------------------------------ symbols and assets */ + +export interface Sym { + precision: number; + code: string; +} + +export function symbolToBigInt(s: Sym): bigint { + if (!/^[A-Z]{1,7}$/.test(s.code) || s.precision < 0 || s.precision > 18) throw new Error(`invalid symbol ${s.precision},${s.code}`); + let v = BigInt(s.precision); + for (let i = 0; i < s.code.length; i++) v |= BigInt(s.code.charCodeAt(i)) << BigInt(8 * (i + 1)); + return v; +} + +export function symbolFromBigInt(v: bigint): Sym { + const precision = Number(v & 0xffn); + let code = ""; + for (let i = 1; i < 8; i++) { + const c = Number((v >> BigInt(8 * i)) & 0xffn); + if (!c) break; + code += String.fromCharCode(c); + } + return { precision, code }; +} + +export interface Asset { + amount: bigint; + symbol: Sym; +} + +/** "1.2345 EOS" (the precision is the number of decimals written). */ +export function parseAsset(text: string): Asset { + const m = /^(-?)(\d+)(?:\.(\d+))? ([A-Z]{1,7})$/.exec(text.trim()); + if (!m) throw new Error(`invalid asset: ${text}`); + const frac = m[3] ?? ""; + const amount = BigInt(`${m[2]}${frac}`) * (m[1] ? -1n : 1n); + return { amount, symbol: { precision: frac.length, code: m[4]! } }; +} + +export function formatAsset(a: Asset): string { + const neg = a.amount < 0n; + const v = neg ? -a.amount : a.amount; + const p = a.symbol.precision; + const s = v.toString().padStart(p + 1, "0"); + const num = p ? `${s.slice(0, -p)}.${s.slice(-p)}` : s; + return `${neg ? "-" : ""}${num} ${a.symbol.code}`; +} + +/* ------------------------------------------------------------------ keys and signatures */ + +const keyCheck = (data: Uint8Array, suffix: string) => ripemd160(concat(data, utf8(suffix))).subarray(0, 4); + +/** "PUB_K1_…" or legacy "EOS…"/"PUB…" prefixed K1 key → 33 compressed bytes, checksums verified. */ +export function parsePublicKey(text: string): Uint8Array { + let raw: Uint8Array; + if (text.startsWith("PUB_K1_")) { + raw = base58.decode(text.slice(7)); + if (raw.length !== 37 || !bytesEqual(keyCheck(raw.subarray(0, 33), "K1"), raw.subarray(33))) throw new Error("bad PUB_K1 checksum"); + } else { + const m = /^([A-Z]{2,5})([1-9A-HJ-NP-Za-km-z]{50})$/.exec(text); + if (!m) throw new Error("not an Antelope public key"); + raw = base58.decode(m[2]!); + if (raw.length !== 37 || !bytesEqual(ripemd160(raw.subarray(0, 33)).subarray(0, 4), raw.subarray(33))) throw new Error("bad legacy key checksum"); + } + const key = raw.slice(0, 33); + secp256k1.Point.fromBytes(key); // must be on the curve + return key; +} + +export function publicKeyString(compressed: Uint8Array): string { + const key = secp256k1.Point.fromBytes(compressed).toBytes(true); + return `PUB_K1_${base58.encode(concat(key, keyCheck(key, "K1")))}`; +} + +export function isPublicKey(text: string): boolean { + try { + parsePublicKey(text); + return true; + } catch { + return false; + } +} + +/** Packed public_key: type byte (0 = K1) ‖ 33 bytes. */ +export function packPublicKey(text: string): Uint8Array { + return concat(new Uint8Array([0]), parsePublicKey(text)); +} + +/** r ‖ s + recovery id (0/1) → "SIG_K1_…": base58(i ‖ r ‖ s ‖ RIPEMD-160(i ‖ r ‖ s ‖ "K1")[0..4]), i = recid + 31. */ +export function signatureString(rs: Uint8Array, recovery: number): string { + if (rs.length !== 64 || (recovery !== 0 && recovery !== 1)) throw new Error("expected r||s and a recovery id"); + const data = concat(new Uint8Array([recovery + 31]), rs); + return `SIG_K1_${base58.encode(concat(data, keyCheck(data, "K1")))}`; +} + +/** Packed signature: type byte (0 = K1) ‖ 65 bytes. */ +export function packSignature(text: string): Uint8Array { + if (!text.startsWith("SIG_K1_")) throw new Error("only K1 signatures"); + const raw = base58.decode(text.slice(7)); + if (raw.length !== 69 || !bytesEqual(keyCheck(raw.subarray(0, 65), "K1"), raw.subarray(65))) throw new Error("bad SIG_K1 checksum"); + return concat(new Uint8Array([0]), raw.subarray(0, 65)); +} + +/** Canonical K1 (Spring/Leap fc is_canonical): neither r nor s has its top bit set or a needless zero byte. */ +export function isCanonical(rs: Uint8Array): boolean { + const r = rs.subarray(0, 32); + const s = rs.subarray(32, 64); + return !(r[0]! & 0x80) && !(r[0] === 0 && !(r[1]! & 0x80)) && !(s[0]! & 0x80) && !(s[0] === 0 && !(s[1]! & 0x80)); +} diff --git a/packages/chains-antelope/src/describe.ts b/packages/chains-antelope/src/describe.ts new file mode 100644 index 00000000..a16c2a39 --- /dev/null +++ b/packages/chains-antelope/src/describe.ts @@ -0,0 +1,156 @@ +import { type BalanceChange, type NetworkId, type Warning, say } from "@clip-wallet/core"; +import { type Abi, AbiError, TOKEN_ABI, decodeActionData, isTokenTransfer } from "./abi.js"; +import { fromHex, parseAsset } from "./bytes.js"; +import { assetFor, knownTokens } from "./networks.js"; +import type { Rpc } from "./rpc.js"; +import type { ActionJson } from "./transaction.js"; + +export interface Line { + label: string; + value: string; +} + +export interface Described { + title: string; + lines: Line[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + blind: boolean; +} + +export interface DescribeContext { + networkId: NetworkId; + /** This key's accounts on the network. */ + mine: Set; + rpc: Rpc | null; +} + +/** Actions on eosio that hand control of an account to other keys or accounts. */ +const AUTH_ACTIONS = new Set(["updateauth", "deleteauth", "linkauth", "unlinkauth"]); + +const danger = (code: Warning["code"], message: string): Warning => ({ level: "danger", code, message }); +const caution = (code: Warning["code"], message: string): Warning => ({ level: "caution", code, message }); + +function short(v: unknown): string { + const s = typeof v === "string" ? v : JSON.stringify(v); + return s.length > 120 ? `${s.slice(0, 117)}…` : s; +} + +/** The ABI to read an action with: eosio.token-style transfers of known token contracts need no network call. */ +export async function abiFor(a: ActionJson, dc: { networkId: NetworkId; rpc: Rpc | null }): Promise { + if (a.name === "transfer" && knownTokens(dc.networkId).some((t) => t.contract === a.account)) return TOKEN_ABI; + if (!dc.rpc) return null; + return dc.rpc.abi(a.account); +} + +interface One { + title: string; + lines: Line[]; + changes: BalanceChange[]; + warnings: Warning[]; + blind?: boolean; +} + +async function describeAction(a: ActionJson, dc: DescribeContext): Promise { + let abi: Abi | null = null; + try { + abi = await abiFor(a, dc); + } catch { + abi = null; + } + let data: Record; + try { + if (!abi) throw new AbiError("no ABI"); + data = decodeActionData(abi, a.name, fromHex(a.data)); + } catch { + return { title: say("bg.antelope.callOn", { action: a.name, contract: a.account }), lines: [{ label: "Data", value: `0x${a.data.slice(0, 64)}${a.data.length > 64 ? "…" : ""}` }], changes: [], warnings: [], blind: true }; + } + + if (abi && isTokenTransfer(abi, a.name)) { + const from = String(data.from); + const to = String(data.to); + let q; + try { + q = parseAsset(String(data.quantity)); + } catch { + return { title: say("bg.antelope.callOn", { action: a.name, contract: a.account }), lines: [], changes: [], warnings: [], blind: true }; + } + const asset = assetFor(dc.networkId, a.account, q.symbol.code, q.symbol.precision); + const amount = `${String(data.quantity).split(" ")[0]} ${q.symbol.code}`; + const lines: Line[] = [{ label: "To", value: to }]; + if (!dc.mine.has(from)) lines.unshift({ label: "From", value: from }); + if (data.memo) lines.push({ label: "Memo", value: short(data.memo) }); + lines.push({ label: "Token", value: `${q.symbol.code} · ${a.account}` }); + const warnings: Warning[] = []; + if (asset.spam) warnings.push(danger("known-scam", say("bg.warn.spamToken", { symbol: q.symbol.code }))); + else if (!knownTokens(dc.networkId).some((t) => t.contract === a.account && t.symbol === q.symbol.code)) { + warnings.push(caution("unknown-call", say("bg.antelope.unknownToken", { symbol: q.symbol.code, contract: a.account }))); + } + const changes: BalanceChange[] = []; + if (dc.mine.has(from) && !dc.mine.has(to)) changes.push({ asset, delta: (-q.amount).toString() }); + if (!dc.mine.has(from) && dc.mine.has(to)) changes.push({ asset, delta: q.amount.toString() }); + const title = dc.mine.has(from) ? say("bg.req.sendTo", { amount, to }) : say("bg.req.othersSend", { from, amount, to }); + if (dc.rpc && dc.mine.has(from) && !dc.mine.has(to)) { + const exists = await dc.rpc.account(to).catch(() => undefined); + if (exists === null) warnings.push(danger("simulation-failed", say("bg.antelope.noRecipient", { to }))); + } + return { title, lines, changes, warnings }; + } + + const fields: Line[] = Object.entries(data).map(([k, v]) => ({ label: k, value: short(v) })); + if (a.account === "eosio" && AUTH_ACTIONS.has(a.name)) { + const account = String(data.account ?? ""); + const permission = String(data.permission ?? data.requirement ?? ""); + const title = + a.name === "updateauth" + ? say("bg.antelope.updateAuth", { permission, account }) + : a.name === "deleteauth" + ? say("bg.antelope.deleteAuth", { permission, account }) + : a.name === "linkauth" + ? say("bg.antelope.linkAuth", { permission: String(data.requirement ?? ""), action: `${String(data.code)}::${String(data.type || "*")}` }) + : say("bg.antelope.unlinkAuth", { action: `${String(data.code)}::${String(data.type || "*")}` }); + const warning = a.name === "unlinkauth" ? caution("account-takeover", say("bg.antelope.unlinkWarning")) : danger("account-takeover", say("bg.antelope.authWarning", { account: account || [...dc.mine][0] || "?" })); + return { title, lines: fields, changes: [], warnings: [warning] }; + } + + return { + title: say("bg.antelope.callOn", { action: a.name, contract: a.account }), + lines: fields, + changes: [], + warnings: [caution("unknown-call", "Clip Wallet can name this call but can't fully read it, so some of its effects may not be shown.")], + }; +} + +/** Reads every action. Blind when any action can't be read through its contract's ABI. */ +export async function describeActions(actions: ActionJson[], dc: DescribeContext): Promise { + const parts: One[] = []; + for (const a of actions) parts.push(await describeAction(a, dc)); + const blind = parts.some((p) => p.blind); + const lines: Line[] = []; + if (parts.length === 1) lines.push(...parts[0]!.lines); + else { + parts.forEach((p, i) => { + lines.push({ label: say("bg.label.actionN", { n: i + 1 }), value: p.title }); + lines.push(...p.lines); + }); + } + for (const a of actions) { + for (const p of a.authorization) if (!dc.mine.has(p.actor)) lines.push({ label: say("bg.antelope.label.alsoSigns"), value: `${p.actor}@${p.permission}` }); + } + const warnings = parts.flatMap((p) => p.warnings); + if (blind) warnings.unshift({ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }); + // Net balance changes per asset. + const net = new Map(); + for (const c of parts.flatMap((p) => p.changes)) { + const k = `${c.asset.key}/${c.asset.address ?? ""}`; + const prev = net.get(k); + net.set(k, { asset: c.asset, delta: (BigInt(prev?.delta ?? "0") + BigInt(c.delta)).toString() }); + } + return { + title: parts.length === 1 ? parts[0]!.title : say("bg.req.approveOps", { count: parts.length }), + lines, + balanceChanges: [...net.values()].filter((c) => c.delta !== "0"), + warnings, + blind, + }; +} diff --git a/packages/chains-antelope/src/index.ts b/packages/chains-antelope/src/index.ts new file mode 100644 index 00000000..1cec9d45 --- /dev/null +++ b/packages/chains-antelope/src/index.ts @@ -0,0 +1,28 @@ +export { ANTELOPE_METHODS, type AntelopeModuleOptions, createAntelopeModule, normalize } from "./module.js"; +export { + ANTELOPE_NETS, + ANTELOPE_NETWORKS, + JUNGLE4, + TELOS_MAINNET, + TELOS_TESTNET, + VAULTA_MAINNET, + XPR_MAINNET, + XPR_TESTNET, + type AntelopeNet, + type AntelopeNetSpec, + type AntelopeToken, + assetFor, + caip2Of, + explorerTxUrl, + knownTokens, + netOf, +} from "./networks.js"; +export { type Abi, AbiError, TOKEN_ABI, decodeActionData, encodeActionData, isTokenTransfer } from "./abi.js"; +export { formatAsset, isAccountName, isPublicKey, nameFromBigInt, nameToBigInt, parseAsset, parsePublicKey, publicKeyString, signatureString } from "./bytes.js"; +export { type ActionJson, type TransactionJson, packTransaction, signingDigest, tapos, transactionId, unpackTransaction } from "./transaction.js"; +export { AntelopeRpcError, type KeyAccount, clearAbiCache, plainAntelopeError } from "./rpc.js"; + +import { createAntelopeModule } from "./module.js"; + +/** Default instance. */ +export const antelopeModule = createAntelopeModule(); diff --git a/packages/chains-antelope/src/module.ts b/packages/chains-antelope/src/module.ts new file mode 100644 index 00000000..7581e8b2 --- /dev/null +++ b/packages/chains-antelope/src/module.ts @@ -0,0 +1,385 @@ +import { + type AssetRef, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + WALLET_ORIGIN, + type Warning, + msg, + say, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { AbiError, TOKEN_ABI, encodeActionData } from "./abi.js"; +import { fromHex, hex, isAccountName, isCanonical, isName, parseAsset, parsePublicKey, publicKeyString, signatureString, formatAsset } from "./bytes.js"; +import { abiFor, describeActions } from "./describe.js"; +import { assetFor, knownTokens, specFor, tokenAssetOf, type AntelopeToken } from "./networks.js"; +import { AntelopeRpcError, type KeyAccount, type Rpc, plainAntelopeError, rpcFor } from "./rpc.js"; +import { type ActionJson, type TransactionJson, expirationSeconds, expirationText, packTransaction, signingDigest, tapos, transactionId } from "./transaction.js"; + +/** + * Request methods (Clip Wallet's own; no wallet-side Antelope standard exists to implement, see README): + * - antelope_signTransaction: { transaction, account? } → { signatures, packed_trx, transaction_id } + * - antelope_signAndPushTransaction: same → { transaction_id, processed? } (sent with send_transaction2) + * `transaction` is the abieos JSON of a transaction; the header (expiration, ref_block_num, ref_block_prefix) may be + * left out for the wallet to fill. Action `data` is packed hex, or an object serialised with the contract's ABI. + * buildTransfer uses antelope_signAndPushTransaction with WALLET_ORIGIN. + */ +export const ANTELOPE_METHODS = { + signTransaction: "antelope_signTransaction", + signAndPushTransaction: "antelope_signAndPushTransaction", +} as const; + +export interface AntelopeModuleOptions { + /** Seconds until a filled-in transaction expires (default 300). */ + expireSeconds?: number; + /** How long discovered account names are kept (default 60 s). */ + accountsTtlMs?: number; +} + +interface Normalized { + header: Partial; + actions: { account: string; name: string; authorization: { actor: string; permission: string }[]; data: unknown }[]; + push: boolean; + needsHeader: boolean; +} + +const isObj = (v: unknown): v is Record => !!v && typeof v === "object" && !Array.isArray(v); + +function malformed(): ClipError { + return new ClipError("This request from the app is malformed, so we stopped it.", "antelope/bad-params"); +} + +export function normalize(request: DappRequest, ctx: ChainContext): Normalized { + if (request.method !== ANTELOPE_METHODS.signTransaction && request.method !== ANTELOPE_METHODS.signAndPushTransaction) { + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "antelope/unsupported-method"); + } + if (!specFor(request.networkId) || request.networkId !== ctx.network.id) { + throw new ClipError(msg("bg.antelope.err.wrongNetwork"), "antelope/network-mismatch"); + } + const p = request.params; + if (!isObj(p)) throw malformed(); + const t = isObj(p.transaction) ? p.transaction : p; + if (p.chainId !== undefined && (typeof p.chainId !== "string" || specFor(p.chainId)?.chainId !== specFor(ctx.network.id)?.chainId)) { + throw new ClipError(msg("bg.antelope.err.wrongNetwork"), "antelope/network-mismatch"); + } + if (!Array.isArray(t.actions) || !t.actions.length) throw malformed(); + if ((Array.isArray(t.context_free_actions) && t.context_free_actions.length) || (Array.isArray(t.transaction_extensions) && t.transaction_extensions.length)) { + throw new ClipError(msg("bg.antelope.err.contextFree"), "antelope/unsupported"); + } + const actions = t.actions.map((a) => { + if (!isObj(a) || typeof a.account !== "string" || typeof a.name !== "string" || !isName(a.account) || !isName(a.name) || !Array.isArray(a.authorization)) throw malformed(); + const authorization = a.authorization.map((x) => { + if (!isObj(x) || typeof x.actor !== "string" || typeof x.permission !== "string" || !isName(x.actor) || !isName(x.permission)) throw malformed(); + return { actor: x.actor, permission: x.permission }; + }); + return { account: a.account, name: a.name, authorization, data: a.data }; + }); + const header: Partial = {}; + for (const k of ["expiration", "ref_block_num", "ref_block_prefix", "max_net_usage_words", "max_cpu_usage_ms", "delay_sec"] as const) { + if (t[k] !== undefined) (header as Record)[k] = t[k]; + } + const needsHeader = header.expiration === undefined || header.ref_block_num === undefined || header.ref_block_prefix === undefined; + return { header, actions, push: request.method === ANTELOPE_METHODS.signAndPushTransaction, needsHeader }; +} + +export function createAntelopeModule(options: AntelopeModuleOptions = {}): ChainModule & { + normalize: typeof normalize; + accounts(ctx: ChainContext): Promise; + receiveAddress(ctx: ChainContext): Promise; +} { + const expireSeconds = options.expireSeconds ?? 300; + const ttl = options.accountsTtlMs ?? 60_000; + const accountCache = new Map(); + /** Per request: the exact transaction decode showed (header filled once). */ + const built = new Map(); + const remember = (id: string, v: { tx: TransactionJson; packed: Uint8Array }) => { + built.set(id, v); + if (built.size > 64) built.delete(built.keys().next().value!); + }; + + function keyOf(ctx: ChainContext): string { + return ctx.account.address.startsWith("PUB_K1_") ? ctx.account.address : publicKeyString(fromHex(ctx.account.publicKey)); + } + + async function accounts(ctx: ChainContext): Promise { + const k = `${ctx.network.id}/${keyOf(ctx)}`; + const hit = accountCache.get(k); + if (hit && Date.now() - hit.at < ttl) return hit.list; + const list = await rpcFor(ctx).accountsForKey(keyOf(ctx)); + accountCache.set(k, { at: Date.now(), list }); + return list; + } + + function noAccount(ctx: ChainContext): ClipError { + return new ClipError(msg(ctx.network.testnet ? "bg.antelope.err.noAccountTestnet" : "bg.antelope.err.noAccount", { network: ctx.network.name }), "antelope/no-account"); + } + + async function firstAccount(ctx: ChainContext): Promise { + const list = await accounts(ctx); + const pick = list.find((a) => a.permission === "active") ?? list[0]; + if (!pick) throw noAccount(ctx); + return pick.account; + } + + async function header(rpc: Rpc, h: Partial): Promise> { + if (h.expiration !== undefined && h.ref_block_num !== undefined && h.ref_block_prefix !== undefined) { + return { expiration: String(h.expiration), ref_block_num: Number(h.ref_block_num), ref_block_prefix: Number(h.ref_block_prefix) }; + } + const info = await rpc.info(); + const head = expirationSeconds(info.head_block_time.replace(/\.\d+$/, "")); + return { expiration: h.expiration !== undefined ? String(h.expiration) : expirationText(head + expireSeconds), ...tapos(info.last_irreversible_block_id) }; + } + + async function build(n: Normalized, ctx: ChainContext, rpc: Rpc): Promise<{ tx: TransactionJson; packed: Uint8Array }> { + const actions: ActionJson[] = []; + for (const a of n.actions) { + let data: string; + if (typeof a.data === "string" && /^([0-9a-fA-F]{2})*$/.test(a.data)) data = a.data.toLowerCase(); + else if (isObj(a.data)) { + const abi = await abiFor({ ...a, data: "" }, { networkId: ctx.network.id, rpc }); + if (!abi) throw new ClipError(msg("bg.antelope.err.noAbi", { contract: a.account }), "antelope/unreadable-operation"); + try { + data = hex(encodeActionData(abi, a.name, a.data)); + } catch (e) { + if (e instanceof AbiError) throw new ClipError(msg("bg.antelope.err.noAbi", { contract: a.account }), "antelope/unreadable-operation", e); + throw e; + } + } else throw malformed(); + actions.push({ account: a.account, name: a.name, authorization: a.authorization, data }); + } + const h = await header(rpc, n.header); + const tx: TransactionJson = { + ...h, + max_net_usage_words: Number(n.header.max_net_usage_words ?? 0), + max_cpu_usage_ms: Number(n.header.max_cpu_usage_ms ?? 0), + delay_sec: Number(n.header.delay_sec ?? 0), + context_free_actions: [], + actions, + transaction_extensions: [], + }; + let packed: Uint8Array; + try { + packed = packTransaction(tx); + } catch (e) { + throw new ClipError("This request from the app is malformed, so we stopped it.", "antelope/bad-params", e); + } + return { tx, packed }; + } + + /** Every authorization must be one this key holds (sign-and-push), or at least one (sign only: others co-sign). */ + function checkAuth(tx: TransactionJson, mine: KeyAccount[], push: boolean): void { + const holds = (p: { actor: string; permission: string }) => mine.some((m) => m.account === p.actor && (m.permission === p.permission || (m.permission === "owner" && p.permission === "active"))); + const auths = tx.actions.flatMap((a) => a.authorization); + const ours = auths.filter(holds); + if (!ours.length) { + if (auths.some((p) => mine.some((m) => m.account === p.actor))) throw new ClipError(msg("bg.antelope.err.permission"), "antelope/wrong-account"); + throw new ClipError("This transaction doesn't need your signature.", "antelope/wrong-account"); + } + if (push && ours.length !== auths.length) throw new ClipError(msg("bg.antelope.err.otherSigners"), "antelope/missing-signature"); + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const rpc = rpcFor(ctx); + const mine = await accounts(ctx); + if (!mine.length) throw noAccount(ctx); + const b = await build(n, ctx, rpc); + checkAuth(b.tx, mine, n.push); + remember(req.id, b); + const spec = specFor(ctx.network.id); + const d = await describeActions(b.tx.actions, { networkId: ctx.network.id, mine: new Set(mine.map((m) => m.account)), rpc }); + const warnings: Warning[] = [...d.warnings]; + if (b.tx.delay_sec > 0) warnings.push({ level: "caution", code: "durable-nonce", message: say("bg.antelope.delayed", { seconds: b.tx.delay_sec }) }); + const lines = [...d.lines]; + lines.push({ label: "Network fee", value: spec?.resources === "free" ? say("bg.antelope.feeFree") : say("bg.antelope.feeResources") }); + if (!n.push) lines.push({ label: "Sent by", value: say("bg.antelope.appSends", { host: hostOf(req.origin) }) }); + return { requestId: req.id, networkId: req.networkId, title: d.title, lines, balanceChanges: d.balanceChanges, simulated: false, blind: d.blind, warnings }; + } + + async function builtFor(req: DappRequest, ctx: ChainContext): Promise<{ tx: TransactionJson; packed: Uint8Array }> { + const cached = built.get(req.id); + if (cached) return cached; + const n = normalize(req, ctx); + if (n.needsHeader) throw new ClipError("This request expired. Please try again from the app.", "antelope/not-prepared"); + const b = await build(n, ctx, rpcFor(ctx)); + checkAuth(b.tx, await accounts(ctx), n.push); + remember(req.id, b); + return b; + } + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + const b = await builtFor(req, ctx); + const spec = specFor(ctx.network.id)!; + return [{ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes: signingDigest(spec.chainId, b.packed), approvalId }]; + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const b = built.get(req.id); + if (!b) throw new ClipError("This request expired. Please try again from the app.", "antelope/not-prepared"); + const spec = specFor(ctx.network.id)!; + const digest = signingDigest(spec.chainId, b.packed); + const sig = signatures[0]; + const pub = parsePublicKey(keyOf(ctx)); + if ( + signatures.length !== 1 || + !sig || + sig.scheme !== "ecdsa-secp256k1" || + sig.bytes.length !== 64 || + (sig.recovery !== 0 && sig.recovery !== 1) || + !isCanonical(sig.bytes) || + !secp256k1.verify(sig.bytes, digest, pub, { prehash: false }) || + !recoversTo(sig.bytes, sig.recovery, digest, pub) + ) { + throw new ClipError("The signature didn't match. Nothing was sent.", "antelope/bad-signature"); + } + const sigText = signatureString(sig.bytes, sig.recovery); + const packedHex = hex(b.packed); + const id = transactionId(b.packed); + built.delete(req.id); + if (!n.push) return { signatures: [sigText], packed_trx: packedHex, transaction_id: id }; + try { + const r = await rpcFor(ctx).send(packedHex, [sigText]); + return { transaction_id: r.transaction_id ?? id, ...(r.processed ? { processed: r.processed } : {}) }; + } catch (e) { + if (e instanceof AntelopeRpcError) { + const m = plainAntelopeError(e, spec, ctx.network.name); + const resource = /cpu|net|ram/i.test(m.id); + throw new ClipError(m, resource ? "antelope/insufficient-resources" : "antelope/send-failed", e); + } + throw e; + } + } + + async function getBalances(ctx: ChainContext): Promise { + const spec = specFor(ctx.network.id); + if (!spec) return []; + const list = await accounts(ctx); + const account = list[0]?.account; + const core = tokenAssetOf(ctx.network.id, spec.core); + if (!account) return [{ asset: core, amount: "0" }]; + const rpc = rpcFor(ctx); + const out: TokenBalance[] = []; + for (const t of knownTokens(ctx.network.id)) { + const text = await rpc.balance(t.contract, account, t.symbol); + const units = text ? parseAsset(text).amount : 0n; + if (t === spec.core || units > 0n) out.push({ asset: tokenAssetOf(ctx.network.id, t), amount: units.toString() }); + } + for (const t of (await rpc.tokens(account)) ?? []) { + if (knownTokens(ctx.network.id).some((k) => k.contract === t.contract && k.symbol === t.symbol)) continue; + let units: bigint; + try { + units = parseAsset(`${t.amount} ${t.symbol}`).amount; + } catch { + continue; + } + if (units > 0n) out.push({ asset: assetFor(ctx.network.id, t.contract, t.symbol, t.precision), amount: units.toString() }); + } + return out; + } + + async function getNfts(_ctx: ChainContext): Promise { + return []; + } + + function tokenOf(asset: AssetRef, ctx: ChainContext): AntelopeToken { + const spec = specFor(ctx.network.id)!; + if (!asset.address) return spec.core; + const [contract, symbol] = asset.address.split(":"); + if (!contract || !symbol || !isName(contract) || !/^[A-Z]{1,7}$/.test(symbol)) throw new ClipError(msg("bg.antelope.err.notToken"), "antelope/bad-asset"); + return knownTokens(ctx.network.id).find((t) => t.contract === contract && t.symbol === symbol) ?? { contract, symbol, precision: asset.decimals, key: asset.key, name: asset.name }; + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + const to = p.to.trim(); + if (!isAccountName(to)) throw new ClipError(msg("bg.antelope.err.badAccount"), "antelope/bad-address"); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "antelope/bad-amount"); + const from = await firstAccount(ctx); + if (to === from) throw new ClipError("That's your own account.", "antelope/self-transfer"); + const token = tokenOf(p.asset, ctx); + const rpc = rpcFor(ctx); + if ((await rpc.account(to)) === null) throw new ClipError(msg("bg.antelope.noRecipient", { to }), "antelope/bad-recipient"); + const held = await rpc.balance(token.contract, from, token.symbol); + const have = held ? parseAsset(held) : null; + const precision = have?.symbol.precision ?? token.precision; + const amount = BigInt(p.amount); + if (!have || have.amount < amount) throw new ClipError(msg("bg.err.notEnough", { symbol: token.symbol }), "antelope/insufficient-token"); + const quantity = formatAsset({ amount, symbol: { precision, code: token.symbol } }); + const data = hex(encodeActionData(TOKEN_ABI, "transfer", { from, to, quantity, memo: "" })); + const h = await header(rpc, {}); + const transaction: TransactionJson = { + ...h, + max_net_usage_words: 0, + max_cpu_usage_ms: 0, + delay_sec: 0, + context_free_actions: [], + actions: [{ account: token.contract, name: "transfer", authorization: [{ actor: from, permission: "active" }], data }], + transaction_extensions: [], + }; + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "antelope", + networkId: ctx.network.id, + method: ANTELOPE_METHODS.signAndPushTransaction, + params: { transaction, account: from }, + }; + } + + return { + family: "antelope", + curve: "secp256k1", + /** SLIP-44 194 (EOS/Vaulta), as TokenPocket: one K1 key for Vaulta, Telos and XPR Network. */ + derivationPath: (index: number) => `m/44'/194'/0'/0/${index}`, + /** The account's key as "PUB_K1_…" (accounts are names on chain that point at it: receiveAddress). */ + addressFromPublicKey: (publicKey: Uint8Array, _network: Network) => publicKeyString(publicKey), + /** An account name (what you send to). Keys aren't addresses on Antelope. */ + isAddress: (value: string) => isAccountName(value.trim()), + /** A name can exist on any Antelope network (and mean different owners on each). */ + networksForAddress: (value: string, candidates: Network[]) => (isAccountName(value.trim()) ? candidates.filter((c) => c.family === "antelope") : []), + receiveAddress: firstAccount, + accounts, + getBalances, + getNfts, + decode, + prepare, + finalize, + buildTransfer, + normalize, + }; +} + +/** The vault's recovery id must recover the account key (it becomes the SIG_K1 recovery byte). */ +function recoversTo(rs: Uint8Array, recovery: number, digest: Uint8Array, pub: Uint8Array): boolean { + try { + const rec = new Uint8Array(65); + rec[0] = recovery; + rec.set(rs, 1); + const key = secp256k1.recoverPublicKey(rec, digest, { prehash: false }); + return hex(key) === hex(pub); + } catch { + return false; + } +} + +function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} diff --git a/packages/chains-antelope/src/networks.ts b/packages/chains-antelope/src/networks.ts new file mode 100644 index 00000000..02dda715 --- /dev/null +++ b/packages/chains-antelope/src/networks.ts @@ -0,0 +1,172 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * Antelope networks: Vaulta (formerly EOS), Telos and XPR Network, each with its testnet. + * - Ids: ChainAgnostic namespaces antelope/caip2.md — "antelope:" + the first 32 hex characters of the chain id + * (its test cases include EOS Mainnet antelope:aca376f206b8fc25a6ed44dbdc66547c and Telos Mainnet). + * - Chain ids and endpoints checked with GET /v1/chain/get_info in October 2026; every chain API listed answers + * /v1/chain/get_accounts_by_authorizers and send_transaction2 (Hyperion `/v2/state/get_key_accounts` is the + * fallback for nodes that don't). No API keys. + * - Vaulta: the system (gas) token is still EOS on eosio.token (the RAM market, PowerUp and staking are priced in + * EOS); the Vaulta "A" token (core.vaulta, swapped 1:1 with EOS by transferring either to core.vaulta) is shown + * beside it. USDT is Tether's own issue on tethertether. + */ +export type AntelopeNet = "jungle4" | "vaulta" | "telos" | "telos-testnet" | "xpr" | "xpr-testnet"; + +export interface AntelopeToken { + contract: string; + symbol: string; + precision: number; + key: string; + name: string; +} + +export interface AntelopeNetSpec { + chainId: string; + name: string; + testnet: boolean; + rpc: string[]; + /** Hyperion v2 (state/get_key_accounts, state/get_tokens), when one answered. */ + hyperion?: string; + explorer: string; + /** The system token (fees, resources): native asset. */ + core: AntelopeToken; + /** Other tokens shown by default (same issuer as on other networks only where the key says so). */ + tokens: AntelopeToken[]; + /** How CPU/NET are paid for, for plain error messages. */ + resources: "powerup" | "stake-or-powerup" | "free"; +} + +const eos = (): AntelopeToken => ({ contract: "eosio.token", symbol: "EOS", precision: 4, key: "eos", name: "EOS" }); +const vaultaA: AntelopeToken = { contract: "core.vaulta", symbol: "A", precision: 4, key: "vaulta", name: "Vaulta" }; + +export const ANTELOPE_NETS: Record = { + jungle4: { + chainId: "73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d", + name: "Jungle4 (Vaulta testnet)", + testnet: true, + rpc: ["https://jungle4.greymass.com", "https://jungle4.cryptolions.io", "https://jungle4.api.eosnation.io"], + hyperion: "https://jungle4.cryptolions.io", + explorer: "https://jungle4.unicove.com", + core: eos(), + tokens: [vaultaA], + resources: "powerup", + }, + vaulta: { + chainId: "aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906", + name: "Vaulta", + testnet: false, + rpc: ["https://eos.greymass.com", "https://eos.api.eosnation.io"], + hyperion: "https://eos.eosusa.io", + explorer: "https://bloks.io", + core: eos(), + tokens: [vaultaA, { contract: "tethertether", symbol: "USDT", precision: 4, key: "usdt", name: "Tether USD" }], + resources: "powerup", + }, + "telos-testnet": { + chainId: "1eaa0824707c8c16bd25145493bf062aecddfeb56c736f6ba6397f3195f33c9f", + name: "Telos Testnet", + testnet: true, + rpc: ["https://testnet.telos.net", "https://telos-testnet.cryptolions.io"], + hyperion: "https://testnet.telos.net", + explorer: "https://explorer-test.telos.net", + core: { contract: "eosio.token", symbol: "TLOS", precision: 4, key: "tlos", name: "Telos" }, + tokens: [], + resources: "stake-or-powerup", + }, + telos: { + chainId: "4667b205c6838ef70ff7988f6e8257e8be0e1284a2f59699054a018f743b1d11", + name: "Telos", + testnet: false, + rpc: ["https://mainnet.telos.net", "https://telos.greymass.com"], + hyperion: "https://mainnet.telos.net", + explorer: "https://explorer.telos.net", + core: { contract: "eosio.token", symbol: "TLOS", precision: 4, key: "tlos", name: "Telos" }, + tokens: [], + resources: "stake-or-powerup", + }, + "xpr-testnet": { + chainId: "71ee83bcf52142d61019d95f9cc5427ba6a0d7ff8accd9e2088ae2abeaf3d3dd", + name: "XPR Network Testnet", + testnet: true, + rpc: ["https://tn1.protonnz.com", "https://testnet.protonchain.com", "https://proton-testnet.cryptolions.io"], + explorer: "https://testnet.explorer.xprnetwork.org", + core: { contract: "eosio.token", symbol: "XPR", precision: 4, key: "xpr", name: "XPR Network" }, + tokens: [], + resources: "free", + }, + xpr: { + chainId: "384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0", + name: "XPR Network", + testnet: false, + rpc: ["https://proton.greymass.com", "https://api.protonnz.com", "https://proton.eosusa.io"], + hyperion: "https://proton.eosusa.io", + explorer: "https://explorer.xprnetwork.org", + core: { contract: "eosio.token", symbol: "XPR", precision: 4, key: "xpr", name: "XPR Network" }, + tokens: [], + resources: "free", + }, +}; + +export const caip2Of = (chainId: string): NetworkId => `antelope:${chainId.slice(0, 32).toLowerCase()}`; + +export function tokenAssetOf(networkId: NetworkId, t: AntelopeToken): AssetRef { + const a: AssetRef = { key: t.key, symbol: t.symbol, name: t.name, decimals: t.precision, networkId }; + return t.contract === "eosio.token" && isCore(networkId, t) ? a : { ...a, address: `${t.contract}:${t.symbol}` }; +} + +function isCore(networkId: NetworkId, t: AntelopeToken): boolean { + const s = specFor(networkId); + return !!s && s.core.contract === t.contract && s.core.symbol === t.symbol; +} + +function network(net: AntelopeNet): Network { + const c = ANTELOPE_NETS[net]; + const id = caip2Of(c.chainId); + return { id, family: "antelope", name: c.name, nativeAsset: tokenAssetOf(id, c.core), testnet: c.testnet, rpcUrls: c.rpc, explorerUrl: c.explorer, ...(c.hyperion ? { indexerUrl: c.hyperion } : {}) }; +} + +export const JUNGLE4 = network("jungle4"); +export const VAULTA_MAINNET = network("vaulta"); +export const TELOS_TESTNET = network("telos-testnet"); +export const TELOS_MAINNET = network("telos"); +export const XPR_TESTNET = network("xpr-testnet"); +export const XPR_MAINNET = network("xpr"); +export const ANTELOPE_NETWORKS: Network[] = [JUNGLE4, VAULTA_MAINNET, TELOS_TESTNET, TELOS_MAINNET, XPR_TESTNET, XPR_MAINNET]; + +/** A CAIP-2 id, a full 64-hex chain id, or its 32-hex prefix → the network, else null. */ +export function netOf(chain: string): AntelopeNet | null { + const ref = chain.replace(/^(antelope|eosio):/, "").toLowerCase(); + if (!/^[0-9a-f]{32}([0-9a-f]{32})?$/.test(ref)) return null; + for (const [name, c] of Object.entries(ANTELOPE_NETS) as [AntelopeNet, AntelopeNetSpec][]) if (c.chainId.startsWith(ref)) return name; + return null; +} + +export function specFor(networkId: NetworkId): AntelopeNetSpec | null { + const n = netOf(networkId); + return n ? ANTELOPE_NETS[n] : null; +} + +/** All tokens Clip Wallet shows on a network, core first. */ +export function knownTokens(networkId: NetworkId): AntelopeToken[] { + const s = specFor(networkId); + return s ? [s.core, ...s.tokens] : []; +} + +/** + * AssetRef for any token contract + symbol. Known ones keep their key; anything else is + * `antelope::` and, when it borrows a known symbol (EOS, A, USDT, TLOS, XPR) from another + * contract, `spam: true`. + */ +export function assetFor(networkId: NetworkId, contract: string, symbol: string, precision: number): AssetRef { + const known = knownTokens(networkId).find((t) => t.contract === contract && t.symbol === symbol); + if (known) return tokenAssetOf(networkId, { ...known, precision }); + const asset: AssetRef = { key: `antelope:${contract}:${symbol}`, symbol, name: symbol, decimals: precision, networkId, address: `${contract}:${symbol}` }; + const famous = new Set(Object.values(ANTELOPE_NETS).flatMap((s) => [s.core.symbol, ...s.tokens.map((t) => t.symbol)])); + if (famous.has(symbol) || /^USD/.test(symbol)) asset.spam = true; + return asset; +} + +export function explorerTxUrl(net: Network, id: string): string { + return `${net.explorerUrl}/transaction/${id}`; +} diff --git a/packages/chains-antelope/src/rpc.ts b/packages/chains-antelope/src/rpc.ts new file mode 100644 index 00000000..8343dffe --- /dev/null +++ b/packages/chains-antelope/src/rpc.ts @@ -0,0 +1,185 @@ +import { type ChainContext, ClipError, type Msg, msg } from "@clip-wallet/core"; +import type { Abi } from "./abi.js"; +import { bytesEqual, parsePublicKey } from "./bytes.js"; +import type { AntelopeNetSpec } from "./networks.js"; +import { specFor } from "./networks.js"; + +/** + * Spring/Leap chain API (`/v1/chain/*`) over `ctx.fetch`, trying each of the network's servers in turn. A server that + * can't be reached, answers 5xx without an Antelope error, or doesn't have the endpoint (404 "Unknown Endpoint") is + * skipped; an Antelope error answer (`{ code, error: { name, what, details } }`) is the chain's answer and is thrown + * as AntelopeRpcError. + */ +export class AntelopeRpcError extends Error { + constructor( + /** fc exception name, e.g. "tx_cpu_usage_exceeded". */ + public readonly name: string, + public readonly code: number, + public readonly details: string[], + ) { + super(`${name}: ${details.join("; ")}`); + } +} + +class Missing extends Error {} + +export interface ChainInfo { + chain_id: string; + head_block_num: number; + head_block_time: string; + last_irreversible_block_num: number; + last_irreversible_block_id: string; +} + +export interface KeyAccount { + account: string; + permission: string; +} + +export interface Rpc { + post(path: string, body: unknown): Promise; + info(): Promise; + /** null when the account has no contract. Cached per network and account. */ + abi(account: string): Promise; + /** null when the account doesn't exist. */ + account(name: string): Promise | null>; + balance(code: string, account: string, symbol: string): Promise; + /** Accounts (and permissions) this key can sign for: chain API, then Hyperion. */ + accountsForKey(publicKey: string): Promise; + /** Hyperion state/get_tokens, when the network has a Hyperion. */ + tokens(account: string): Promise<{ contract: string; symbol: string; precision: number; amount: string }[] | null>; + send(packedTrx: string, signatures: string[]): Promise<{ transaction_id: string; processed?: Record }>; +} + +const abiCache = new Map(); +export const clearAbiCache = () => abiCache.clear(); + +export function rpcFor(ctx: ChainContext): Rpc { + const spec = specFor(ctx.network.id); + const urls = ctx.network.rpcUrls; + const hyperion = ctx.network.indexerUrl; + + async function once(base: string, path: string, body: unknown): Promise { + const res = await ctx.fetch(`${base.replace(/\/$/, "")}${path}`, body === undefined ? undefined : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); + let json: Record | null = null; + try { + json = (await res.json()) as Record; + } catch { + /* not JSON */ + } + const err = json && typeof json.error === "object" && json.error ? (json.error as { name?: string; code?: number; what?: string; details?: { message?: string }[] }) : null; + if (res.status === 404 || (err && /Unknown Endpoint/i.test(JSON.stringify(err.details ?? "")))) throw new Missing(path); + if (err && err.name) throw new AntelopeRpcError(err.name, Number(err.code ?? 0), (err.details ?? []).map((d) => String(d.message ?? "")).filter(Boolean)); + if (!res.ok || !json) throw new Error(`HTTP ${res.status}`); + return json as T; + } + + async function post(path: string, body: unknown): Promise { + let last: unknown; + let missing = 0; + for (const url of urls) { + try { + return await once(url, path, body); + } catch (e) { + if (e instanceof AntelopeRpcError) throw e; + if (e instanceof Missing) missing++; + last = e; + } + } + if (missing === urls.length) throw new Missing(path); + throw new ClipError(msg("bg.antelope.err.offline", { network: ctx.network.name }), "antelope/offline", last); + } + + return { + post, + info: () => post("/v1/chain/get_info", {}), + async abi(account) { + const key = `${ctx.network.id}/${account}`; + if (abiCache.has(key)) return abiCache.get(key)!; + const r = await post<{ abi?: Abi }>("/v1/chain/get_abi", { account_name: account }); + const abi = r.abi && Array.isArray(r.abi.structs) ? r.abi : null; + abiCache.set(key, abi); + return abi; + }, + async account(name) { + try { + return await post>("/v1/chain/get_account", { account_name: name }); + } catch (e) { + if (e instanceof AntelopeRpcError) return null; // unknown account (exception name differs across versions) + throw e; + } + }, + async balance(code, account, symbol) { + try { + const r = await post("/v1/chain/get_currency_balance", { code, account, symbol }); + return Array.isArray(r) ? (r[0] ?? null) : null; + } catch (e) { + if (e instanceof AntelopeRpcError) return null; + throw e; + } + }, + async accountsForKey(publicKey) { + const mine = parsePublicKey(publicKey); + const same = (k: unknown) => { + try { + return typeof k === "string" && bytesEqual(parsePublicKey(k), mine); + } catch { + return false; + } + }; + try { + const r = await post<{ accounts: { account_name: string; permission_name: string; authorizing_key?: string; weight: number; threshold: number }[] }>( + "/v1/chain/get_accounts_by_authorizers", + { keys: [publicKey] }, + ); + // Only permissions this key satisfies alone (weight ≥ threshold). + return r.accounts.filter((a) => same(a.authorizing_key) && a.weight >= a.threshold).map((a) => ({ account: a.account_name, permission: a.permission_name })); + } catch (e) { + if (!(e instanceof Missing) && !(e instanceof ClipError)) throw e; + if (!hyperion) throw e instanceof Missing ? new ClipError(msg("bg.antelope.err.offline", { network: ctx.network.name }), "antelope/offline", e) : e; + } + const r = await once<{ account_names?: string[] }>(hyperion, `/v2/state/get_key_accounts?public_key=${encodeURIComponent(publicKey)}`, undefined).catch((e) => { + throw new ClipError(msg("bg.antelope.err.offline", { network: ctx.network.name }), "antelope/offline", e); + }); + return (r.account_names ?? []).map((account) => ({ account, permission: "active" })); + }, + async tokens(account) { + if (!hyperion) return null; + try { + const r = await once<{ tokens?: { contract: string; symbol: string; precision?: number; amount: number | string }[] }>(hyperion, `/v2/state/get_tokens?account=${account}`, undefined); + return (r.tokens ?? []) + .filter((t) => typeof t.precision === "number") + .map((t) => ({ contract: t.contract, symbol: t.symbol, precision: t.precision!, amount: typeof t.amount === "number" ? t.amount.toFixed(t.precision!) : String(t.amount) })); + } catch { + return null; + } + }, + async send(packedTrx, signatures) { + const transaction = { signatures, compression: 0, packed_context_free_data: "", packed_trx: packedTrx }; + try { + const r = await post<{ transaction_id: string; processed?: Record }>("/v1/chain/send_transaction2", { transaction, return_failure_trace: false, retry_trx: false }); + return r; + } catch (e) { + if (!(e instanceof Missing)) throw e; + } + return post<{ transaction_id: string; processed?: Record }>("/v1/chain/push_transaction", transaction); + }, + }; +} + +/** The chain's refusal in plain words, with the network's own way of getting CPU, NET and RAM. */ +export function plainAntelopeError(e: AntelopeRpcError, spec: AntelopeNetSpec | null, networkName: string): Msg { + const text = `${e.name} ${e.details.join(" ")}`; + if (/tx_cpu_usage_exceeded|leeway_deadline|deadline_exception/.test(text) || /tx_net_usage_exceeded/.test(text)) { + if (spec?.resources === "free") return msg("bg.antelope.err.cpuFree", { network: networkName }); + if (spec?.resources === "stake-or-powerup") return msg("bg.antelope.err.cpuStake", { network: networkName }); + return msg("bg.antelope.err.cpuPowerup", { network: networkName }); + } + if (/ram_usage_exceeded/.test(text)) return msg("bg.antelope.err.ram", { network: networkName }); + if (/overdrawn balance|no balance object found|balance/.test(text)) return msg("bg.antelope.err.balance"); + if (/expired_tx|tx_exp|expiration/.test(text)) return msg("bg.antelope.err.expired"); + if (/unsatisfied_authorization|missing_auth|irrelevant_auth/.test(text)) return msg("bg.antelope.err.auth"); + if (/to account does not exist|account_query|unknown account/i.test(text)) return msg("bg.antelope.err.noRecipient"); + if (/tx_duplicate/.test(text)) return msg("bg.antelope.err.duplicate"); + return msg("bg.antelope.err.rejected", { network: networkName }); +} diff --git a/packages/chains-antelope/src/transaction.ts b/packages/chains-antelope/src/transaction.ts new file mode 100644 index 00000000..c3fea190 --- /dev/null +++ b/packages/chains-antelope/src/transaction.ts @@ -0,0 +1,115 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { Reader, Writer, concat, fromHex, hex, nameFromBigInt, nameToBigInt } from "./bytes.js"; + +/** + * Antelope transactions (Spring/Leap `transaction`): header (expiration uint32 seconds, ref_block_num uint16, + * ref_block_prefix uint32, max_net_usage_words varuint32, max_cpu_usage_ms uint8, delay_sec varuint32), then + * context_free_actions[], actions[] (account name, name, authorization[{actor, permission}], data bytes) and + * transaction_extensions[] (uint16 type, bytes). Signing digest = SHA-256(chain_id ‖ packed_trx ‖ SHA-256 of the + * context-free data, or 32 zero bytes when there is none); the id = SHA-256(packed_trx). + */ + +export interface PermissionLevel { + actor: string; + permission: string; +} + +export interface ActionJson { + account: string; + name: string; + authorization: PermissionLevel[]; + /** Packed action data, hex. */ + data: string; +} + +export interface TransactionJson { + /** "2026-10-06T12:00:00" (UTC). */ + expiration: string; + ref_block_num: number; + ref_block_prefix: number; + max_net_usage_words: number; + max_cpu_usage_ms: number; + delay_sec: number; + context_free_actions: ActionJson[]; + actions: ActionJson[]; + transaction_extensions: [number, string][]; +} + +export function expirationSeconds(expiration: string): number { + const ms = Date.parse(/[zZ]$/.test(expiration) ? expiration : `${expiration}Z`); + if (Number.isNaN(ms)) throw new Error(`bad expiration ${expiration}`); + return Math.floor(ms / 1000); +} + +export function expirationText(seconds: number): string { + return new Date(seconds * 1000).toISOString().replace(/\.000Z$/, ""); +} + +function packAction(w: Writer, a: ActionJson): void { + w.uint(nameToBigInt(a.account), 8).uint(nameToBigInt(a.name), 8).varuint32(a.authorization.length); + for (const p of a.authorization) w.uint(nameToBigInt(p.actor), 8).uint(nameToBigInt(p.permission), 8); + w.blob(fromHex(a.data)); +} + +export function packTransaction(tx: TransactionJson): Uint8Array { + const w = new Writer() + .uint(expirationSeconds(tx.expiration), 4) + .uint(tx.ref_block_num, 2) + .uint(tx.ref_block_prefix, 4) + .varuint32(tx.max_net_usage_words) + .uint(tx.max_cpu_usage_ms, 1) + .varuint32(tx.delay_sec); + w.varuint32(tx.context_free_actions.length); + for (const a of tx.context_free_actions) packAction(w, a); + w.varuint32(tx.actions.length); + for (const a of tx.actions) packAction(w, a); + w.varuint32(tx.transaction_extensions.length); + for (const [type, data] of tx.transaction_extensions) w.uint(type, 2).blob(fromHex(data)); + return w.done(); +} + +function unpackAction(r: Reader): ActionJson { + const account = nameFromBigInt(r.uint(8)); + const name = nameFromBigInt(r.uint(8)); + const n = r.varuint32(); + const authorization: PermissionLevel[] = []; + for (let i = 0; i < n; i++) authorization.push({ actor: nameFromBigInt(r.uint(8)), permission: nameFromBigInt(r.uint(8)) }); + return { account, name, authorization, data: hex(r.blob()) }; +} + +export function unpackTransaction(bytes: Uint8Array): TransactionJson { + const r = new Reader(bytes); + const tx: TransactionJson = { + expiration: expirationText(Number(r.uint(4))), + ref_block_num: Number(r.uint(2)), + ref_block_prefix: Number(r.uint(4)), + max_net_usage_words: r.varuint32(), + max_cpu_usage_ms: r.byte(), + delay_sec: r.varuint32(), + context_free_actions: [], + actions: [], + transaction_extensions: [], + }; + for (let i = r.varuint32(); i > 0; i--) tx.context_free_actions.push(unpackAction(r)); + for (let i = r.varuint32(); i > 0; i--) tx.actions.push(unpackAction(r)); + for (let i = r.varuint32(); i > 0; i--) tx.transaction_extensions.push([Number(r.uint(2)), hex(r.blob())]); + if (r.remaining) throw new Error("bytes left after the transaction"); + return tx; +} + +export function signingDigest(chainId: string, packedTrx: Uint8Array): Uint8Array { + return sha256(concat(fromHex(chainId), packedTrx, new Uint8Array(32))); +} + +export function transactionId(packedTrx: Uint8Array): string { + return hex(sha256(packedTrx)); +} + +/** TAPoS from a block id: ref_block_num = block number & 0xffff, ref_block_prefix = bytes 8..12 little-endian. */ +export function tapos(blockId: string): { ref_block_num: number; ref_block_prefix: number } { + const b = fromHex(blockId); + if (b.length !== 32) throw new Error("bad block id"); + const num = (b[0]! << 24) | (b[1]! << 16) | (b[2]! << 8) | b[3]!; + const prefix = (b[8]! | (b[9]! << 8) | (b[10]! << 16) | (b[11]! << 24)) >>> 0; + return { ref_block_num: num & 0xffff, ref_block_prefix: prefix }; +} diff --git a/packages/chains-antelope/test/antelope.test.ts b/packages/chains-antelope/test/antelope.test.ts new file mode 100644 index 00000000..99bb1da3 --- /dev/null +++ b/packages/chains-antelope/test/antelope.test.ts @@ -0,0 +1,225 @@ +import { type DappRequest, WALLET_ORIGIN } from "@clip-wallet/core"; +import { Checksum256, PublicKey, Signature as WkSignature } from "@wharfkit/antelope"; +import { beforeEach, describe, expect, it } from "vitest"; +import { + ANTELOPE_METHODS, + ANTELOPE_NETWORKS, + JUNGLE4, + TELOS_MAINNET, + VAULTA_MAINNET, + XPR_MAINNET, + assetFor, + clearAbiCache, + createAntelopeModule, + netOf, + packTransaction, + signingDigest, + transactionId, + unpackTransaction, +} from "../src/index.js"; +import { fromHex, hex } from "../src/bytes.js"; +import { BOB, EOS_SEND, INFO, LEGACY, ME, PUB_HEX, PUB_K1, TAKEOVER, TAKEOVER_DATA } from "./fixtures.js"; +import { ctxFor, err, mockChain, signer } from "./helpers.js"; +import { SIGS } from "./signatures.js"; + +const mod = createAntelopeModule({ accountsTtlMs: 0 }); +const eos = JUNGLE4.nativeAsset; + +function req(method: string, params: unknown, over: Partial = {}): DappRequest { + return { id: Math.random().toString(36).slice(2), origin: "https://dapp.example", via: "injected", family: "antelope", networkId: JUNGLE4.id, method, params, ...over }; +} + +beforeEach(() => clearAbiCache()); + +describe("networks", () => { + it("uses ChainAgnostic antelope: ids (first 32 hex of the chain id)", () => { + expect(VAULTA_MAINNET.id).toBe("antelope:aca376f206b8fc25a6ed44dbdc66547c"); + expect(TELOS_MAINNET.id).toBe("antelope:4667b205c6838ef70ff7988f6e8257e8"); + expect(JUNGLE4.id).toBe("antelope:73e4385a2708e6d7048834fbc1079f2f"); + expect(XPR_MAINNET.id).toBe("antelope:384da888112027f0321850a169f737c3"); + expect(ANTELOPE_NETWORKS.filter((n) => n.testnet).map((n) => n.name)).toEqual(["Jungle4 (Vaulta testnet)", "Telos Testnet", "XPR Network Testnet"]); + expect(netOf(INFO.chain_id)).toBe("jungle4"); + expect(netOf("antelope:1064487b3cd1a897ce03ae5b6a865651")).toBeNull(); // WAX isn't one of ours + expect(VAULTA_MAINNET.nativeAsset).toMatchObject({ key: "eos", symbol: "EOS", decimals: 4 }); + }); + + it("known tokens keep their key; look-alikes from other contracts are spam", () => { + expect(assetFor(VAULTA_MAINNET.id, "tethertether", "USDT", 4)).toMatchObject({ key: "usdt", address: "tethertether:USDT" }); + expect(assetFor(VAULTA_MAINNET.id, "core.vaulta", "A", 4)).toMatchObject({ key: "vaulta" }); + expect(assetFor(VAULTA_MAINNET.id, "fakeusdt1111", "USDT", 4).spam).toBe(true); + expect(assetFor(XPR_MAINNET.id, "ternary", "XPR", 1).spam).toBe(true); // seen on XPR mainnet + expect(assetFor(VAULTA_MAINNET.id, "eosiopowcoin", "POW", 8).spam).toBeUndefined(); + }); +}); + +describe("keys and accounts", () => { + it("the address is the PUB_K1 key (matches WharfKit); account names are what you send to", () => { + expect(mod.addressFromPublicKey(fromHex(PUB_HEX), JUNGLE4)).toBe(PUB_K1); + expect(PublicKey.from(PUB_K1).toLegacyString()).toBe(LEGACY); + expect(mod.derivationPath(3)).toBe("m/44'/194'/0'/0/3"); + expect(mod.isAddress("bobbobbob123")).toBe(true); + expect(mod.isAddress(PUB_K1)).toBe(false); + expect(mod.isAddress("Bob")).toBe(false); + expect(mod.networksForAddress("eosio.token", ANTELOPE_NETWORKS)).toHaveLength(6); + }); + + it("receiveAddress: the account name on that network, discovered by key (chain API, then Hyperion)", async () => { + expect(await mod.receiveAddress(ctxFor(mockChain().fetch))).toBe(ME); + const hyperionOnly = mockChain({ "/v1/chain/get_accounts_by_authorizers": () => err("exception", 0, "Unknown Endpoint", 404), "/v2/state/get_key_accounts": () => ({ account_names: ["fromhyperion"] }) }); + expect(await mod.receiveAddress(ctxFor(hyperionOnly.fetch))).toBe("fromhyperion"); + expect(hyperionOnly.calls.some((c) => c.url.startsWith("https://jungle4.cryptolions.io/v2/state/get_key_accounts?public_key=PUB_K1_"))).toBe(true); + const none = ctxFor(mockChain({ "/v1/chain/get_accounts_by_authorizers": () => ({ accounts: [] }) }).fetch); + await expect(mod.receiveAddress(none)).rejects.toMatchObject({ code: "antelope/no-account", userMessage: expect.stringContaining("No Jungle4 (Vaulta testnet) account uses this key yet") }); + // A permission this key only partly controls (weight below the threshold) doesn't count. + const multi = ctxFor(mockChain({ "/v1/chain/get_accounts_by_authorizers": () => ({ accounts: [{ account_name: "shared", permission_name: "active", authorizing_key: PUB_K1, weight: 1, threshold: 2 }] }) }).fetch); + await expect(mod.receiveAddress(multi)).rejects.toMatchObject({ code: "antelope/no-account" }); + }); +}); + +describe("balances", () => { + it("core and known tokens from get_currency_balance, others from Hyperion with the spam check", async () => { + const b = await mod.getBalances(ctxFor(mockChain().fetch)); + expect(b.map((x) => [x.asset.key, x.amount, x.asset.spam ?? false])).toEqual([ + ["eos", "100000", false], + ["vaulta", "25000", false], + ["antelope:eosfakecoin1:EOS", "500000", true], + ["antelope:eosio.token:JUNGLE", "1000000", false], + ]); + const none = ctxFor(mockChain({ "/v1/chain/get_accounts_by_authorizers": () => ({ accounts: [] }) }).fetch); + expect(await mod.getBalances(none)).toEqual([{ asset: eos, amount: "0" }]); + }); +}); + +describe("buildTransfer → decode → prepare → finalize", () => { + it("sends EOS with eosio.token::transfer (no ABI round trip), signature cross-checked with WharfKit", async () => { + const { fetch, calls } = mockChain(); + const ctx = ctxFor(fetch); + const r = await mod.buildTransfer({ asset: eos, to: BOB, amount: "15000" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, method: ANTELOPE_METHODS.signAndPushTransaction, family: "antelope", networkId: JUNGLE4.id }); + expect((r.params as { transaction: unknown }).transaction).toEqual(EOS_SEND); + const d = await mod.decode(r, ctx); + expect(d.title).toBe(`Send 1.5000 EOS to ${BOB}`); + expect(d.balanceChanges).toEqual([{ asset: eos, delta: "-15000" }]); + expect(d.lines).toContainEqual({ label: "Network fee", value: "None in tokens: it uses your account's CPU and NET" }); + expect(d.warnings).toEqual([]); + expect(d.blind).toBe(false); + const [p] = await mod.prepare(r, ctx, "a"); + const packed = packTransaction(EOS_SEND); + expect(hex(p!.bytes)).toBe(hex(signingDigest(INFO.chain_id, packed))); + const out = (await mod.finalize(r, [signer.sign(p!)], ctx)) as { transaction_id: string }; + const sent = calls.find((c) => c.path === "/v1/chain/send_transaction2")!.body as { transaction: { signatures: string[]; packed_trx: string } }; + expect(sent.transaction.packed_trx).toBe(hex(packed)); + const sig = WkSignature.from(sent.transaction.signatures[0]!); + expect(sig.verifyDigest(Checksum256.from(p!.bytes), PublicKey.from(PUB_K1))).toBe(true); + expect(sig.recoverDigest(Checksum256.from(p!.bytes)).toString()).toBe(PUB_K1); + expect(out.transaction_id).toBe("will-be-replaced"); + expect(calls.some((c) => c.path === "/v1/chain/get_abi")).toBe(false); + }); + + it("refuses in plain words: bad name, own account, unknown recipient, not enough", async () => { + const ctx = ctxFor(mockChain().fetch); + await expect(mod.buildTransfer({ asset: eos, to: "Not A Name", amount: "1" }, ctx)).rejects.toMatchObject({ code: "antelope/bad-address" }); + await expect(mod.buildTransfer({ asset: eos, to: ME, amount: "1" }, ctx)).rejects.toMatchObject({ code: "antelope/self-transfer" }); + await expect(mod.buildTransfer({ asset: eos, to: BOB, amount: "200000" }, ctx)).rejects.toMatchObject({ code: "antelope/insufficient-token" }); + const noBob = ctxFor(mockChain({ "/v1/chain/get_account": (b) => (b.account_name === BOB ? err("account_query_exception", 3060002, "unknown key") : { account_name: b.account_name }) }).fetch); + await expect(mod.buildTransfer({ asset: eos, to: BOB, amount: "1" }, noBob)).rejects.toMatchObject({ code: "antelope/bad-recipient" }); + }); + + it("puts CPU, NET and RAM exhaustion in plain words per network", async () => { + const cpu = mockChain({ "/v1/chain/send_transaction2": () => err("tx_cpu_usage_exceeded", 3080004, "billed CPU time (300 us) is greater than the maximum billable CPU time for the transaction (0 us)") }); + const ctx = ctxFor(cpu.fetch); + const r = await mod.buildTransfer({ asset: eos, to: BOB, amount: "15000" }, ctx); + await mod.decode(r, ctx); + const [p] = await mod.prepare(r, ctx, "a"); + await expect(mod.finalize(r, [signer.sign(p!)], ctx)).rejects.toMatchObject({ code: "antelope/insufficient-resources", userMessage: expect.stringContaining("PowerUp") }); + }); + + it("refuses a bad or non-canonical signature before sending", async () => { + const { fetch, calls } = mockChain(); + const ctx = ctxFor(fetch); + const r = await mod.buildTransfer({ asset: eos, to: BOB, amount: "15000" }, ctx); + await mod.decode(r, ctx); + await mod.prepare(r, ctx, "a"); + const other = { scheme: "ecdsa-secp256k1" as const, bytes: fromHex(SIGS.TAKEOVER.rs), recovery: 0, publicKey: PUB_HEX }; + await expect(mod.finalize(r, [other], ctx)).rejects.toMatchObject({ code: "antelope/bad-signature" }); + const right = signer.sign((await mod.prepare(r, ctx, "a"))[0]!); + await expect(mod.finalize(r, [{ ...right, recovery: 1 }], ctx)).rejects.toMatchObject({ code: "antelope/bad-signature" }); + expect(calls.some((c) => c.path === "/v1/chain/send_transaction2")).toBe(false); + }); +}); + +describe("dapp-shaped requests", () => { + it("updateauth is account-takeover; JSON data is serialised with the contract's ABI; sign-only returns SIG_K1", async () => { + const { fetch, calls } = mockChain(); + const ctx = ctxFor(fetch); + const tx = { ...TAKEOVER, actions: [{ ...TAKEOVER.actions[0]!, data: TAKEOVER_DATA }] }; + const r = req(ANTELOPE_METHODS.signTransaction, { transaction: tx }); + const d = await mod.decode(r, ctx); + expect(d.title).toBe(`Change who controls the active permission of ${ME}`); + expect(d.warnings[0]).toMatchObject({ level: "danger", code: "account-takeover" }); + expect(d.lines).toContainEqual({ label: "Sent by", value: "dapp.example (it gets the signed transaction)" }); + const [p] = await mod.prepare(r, ctx, "a"); + const out = (await mod.finalize(r, [signer.sign(p!)], ctx)) as { signatures: string[]; packed_trx: string; transaction_id: string }; + expect(unpackTransaction(fromHex(out.packed_trx))).toEqual(TAKEOVER); + expect(out.transaction_id).toBe(transactionId(fromHex(out.packed_trx))); + expect(WkSignature.from(out.signatures[0]!).verifyDigest(Checksum256.from(p!.bytes), PublicKey.from(PUB_K1))).toBe(true); + expect(calls.filter((c) => c.path === "/v1/chain/get_abi")).toHaveLength(1); + expect(calls.some((c) => c.path === "/v1/chain/send_transaction2")).toBe(false); + }); + + it("any other action is read through its ABI with a caution; unknown ABI types are blind", async () => { + const ctx = ctxFor(mockChain().fetch); + const call = { ...EOS_SEND, actions: [{ account: "eosio", name: "buyrambytes", authorization: [{ actor: ME, permission: "active" }], data: { payer: ME, receiver: ME, bytes: 4096 } }] }; + const d = await mod.decode(req(ANTELOPE_METHODS.signAndPushTransaction, { transaction: call }), ctx); + expect(d.title).toBe("Call buyrambytes on eosio"); + expect(d.lines).toEqual(expect.arrayContaining([{ label: "payer", value: ME }, { label: "bytes", value: "4096" }])); + expect(d.warnings[0]).toMatchObject({ level: "caution", code: "unknown-call" }); + const blind = { ...EOS_SEND, actions: [{ account: "somedapp1111", name: "play", authorization: [{ actor: ME, permission: "active" }], data: "0102" }] }; + const b = await mod.decode(req(ANTELOPE_METHODS.signAndPushTransaction, { transaction: blind }), ctx); + expect(b.blind).toBe(true); + expect(b.warnings[0]).toMatchObject({ code: "blind-signing" }); + }); + + it("a transfer of an unknown contract's token gets a caution; a look-alike is a scam warning", async () => { + const tokenAbi = { + version: "eosio::abi/1.2", + structs: [{ name: "transfer", base: "", fields: [{ name: "from", type: "name" }, { name: "to", type: "name" }, { name: "quantity", type: "asset" }, { name: "memo", type: "string" }] }], + actions: [{ name: "transfer", type: "transfer" }], + }; + const ctx = ctxFor(mockChain({ "/v1/chain/get_abi": (b) => ({ account_name: b.account_name, abi: tokenAbi }) }).fetch); + const send = (contract: string, quantity: string) => ({ ...EOS_SEND, actions: [{ account: contract, name: "transfer", authorization: [{ actor: ME, permission: "active" }], data: { from: ME, to: BOB, quantity, memo: "gm" } }] }); + const unknown = await mod.decode(req(ANTELOPE_METHODS.signAndPushTransaction, { transaction: send("gamecoins111", "5.00 GAME") }), ctx); + expect(unknown.title).toBe(`Send 5.00 GAME to ${BOB}`); + expect(unknown.warnings[0]).toMatchObject({ level: "caution", code: "unknown-call" }); + expect(unknown.lines).toContainEqual({ label: "Memo", value: "gm" }); + const fake = await mod.decode(req(ANTELOPE_METHODS.signAndPushTransaction, { transaction: send("eosfakecoin1", "1.0000 EOS") }), ctx); + expect(fake.warnings[0]).toMatchObject({ level: "danger", code: "known-scam" }); + }); + + it("refuses another network, other signers on sign-and-push, a key that doesn't hold the permission, context-free actions", async () => { + const ctx = ctxFor(mockChain().fetch); + expect(() => mod.normalize(req(ANTELOPE_METHODS.signTransaction, { transaction: EOS_SEND }, { networkId: VAULTA_MAINNET.id }), ctx)).toThrow(expect.objectContaining({ code: "antelope/network-mismatch" })); + expect(() => mod.normalize(req(ANTELOPE_METHODS.signTransaction, { transaction: EOS_SEND, chainId: VAULTA_MAINNET.id }), ctx)).toThrow(expect.objectContaining({ code: "antelope/network-mismatch" })); + expect(() => mod.normalize(req(ANTELOPE_METHODS.signTransaction, { transaction: { ...EOS_SEND, context_free_actions: [EOS_SEND.actions[0]] } }), ctx)).toThrow(expect.objectContaining({ code: "antelope/unsupported" })); + expect(() => mod.normalize(req("eosio_signArbitrary", {}), ctx)).toThrow(expect.objectContaining({ code: "antelope/unsupported-method" })); + const twoSigners = { ...EOS_SEND, actions: [{ ...EOS_SEND.actions[0]!, authorization: [{ actor: ME, permission: "active" }, { actor: BOB, permission: "active" }] }] }; + await expect(mod.decode(req(ANTELOPE_METHODS.signAndPushTransaction, { transaction: twoSigners }), ctx)).rejects.toMatchObject({ code: "antelope/missing-signature" }); + const coSign = await mod.decode(req(ANTELOPE_METHODS.signTransaction, { transaction: twoSigners }), ctx); + expect(coSign.lines).toContainEqual({ label: "Also needs", value: `${BOB}@active` }); + const custom = { ...EOS_SEND, actions: [{ ...EOS_SEND.actions[0]!, authorization: [{ actor: ME, permission: "trading" }] }] }; + await expect(mod.decode(req(ANTELOPE_METHODS.signAndPushTransaction, { transaction: custom }), ctx)).rejects.toMatchObject({ code: "antelope/wrong-account" }); + const notMine = { ...EOS_SEND, actions: [{ ...EOS_SEND.actions[0]!, authorization: [{ actor: BOB, permission: "active" }] }] }; + await expect(mod.decode(req(ANTELOPE_METHODS.signAndPushTransaction, { transaction: notMine }), ctx)).rejects.toMatchObject({ code: "antelope/wrong-account" }); + }); + + it("fills a missing header once (TAPoS from the last irreversible block, expiry 5 minutes after head)", async () => { + const { fetch, calls } = mockChain(); + const ctx = ctxFor(fetch); + const { expiration: _e, ref_block_num: _n, ref_block_prefix: _p, ...rest } = EOS_SEND; + const r = req(ANTELOPE_METHODS.signTransaction, { transaction: rest }); + await mod.decode(r, ctx); + const [p] = await mod.prepare(r, ctx, "a"); + expect(hex(p!.bytes)).toBe(hex(signingDigest(INFO.chain_id, packTransaction(EOS_SEND)))); + expect(calls.filter((c) => c.path === "/v1/chain/get_info")).toHaveLength(1); + }); +}); diff --git a/packages/chains-antelope/test/fixtures.ts b/packages/chains-antelope/test/fixtures.ts new file mode 100644 index 00000000..44fea9c5 --- /dev/null +++ b/packages/chains-antelope/test/fixtures.ts @@ -0,0 +1,58 @@ +/** + * The public "abandon … about" test key at m/44'/194'/0'/0/0 (what the vault derives for antelope account 0), the + * account names the mocked chain says use it, and the transactions the tests sign. Public data only. + */ +import type { Abi } from "../src/abi.js"; +import { TOKEN_ABI, encodeActionData } from "../src/abi.js"; +import { hex } from "../src/bytes.js"; +import type { TransactionJson } from "../src/transaction.js"; + +export const PUB_HEX = "0315c358024ce46767102578947584c4342a6982b922d454f63588effa34597197"; +export const PUB_K1 = "PUB_K1_6zpSNY1YoLxNt2VsvJjoDfBueU6xC1M1ERJw1UoekL1NK2aD4t"; +export const LEGACY = "EOS6zpSNY1YoLxNt2VsvJjoDfBueU6xC1M1ERJw1UoekL1NHn8KNA"; +export const ME = "clipabandon1"; +export const BOB = "bobbobbob123"; + +/** get_info the mocked Jungle4 answers with. */ +export const INFO = { + chain_id: "73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d", + head_block_num: 290625015, + head_block_time: "2026-10-06T12:00:00.000", + last_irreversible_block_num: 290625013, + last_irreversible_block_id: "115295f5c53487176d0c821bfdf8bd44261573ace810f6fb68f38626cee854f3", +}; + +const header = { expiration: "2026-10-06T12:05:00", ref_block_num: 0x95f5, ref_block_prefix: 0x1b820c6d, max_net_usage_words: 0, max_cpu_usage_ms: 0, delay_sec: 0, context_free_actions: [], transaction_extensions: [] as [number, string][] }; + +/** buildTransfer: 1.5000 EOS from ME to BOB on Jungle4. */ +export const EOS_SEND: TransactionJson = { + ...header, + actions: [{ account: "eosio.token", name: "transfer", authorization: [{ actor: ME, permission: "active" }], data: hex(encodeActionData(TOKEN_ABI, "transfer", { from: ME, to: BOB, quantity: "1.5000 EOS", memo: "" })) }], +}; + +/** The eosio ABI subset the tests serve (structs as in the system contract). */ +export const EOSIO_ABI: Abi = { + version: "eosio::abi/1.2", + structs: [ + { name: "permission_level", base: "", fields: [{ name: "actor", type: "name" }, { name: "permission", type: "name" }] }, + { name: "key_weight", base: "", fields: [{ name: "key", type: "public_key" }, { name: "weight", type: "uint16" }] }, + { name: "permission_level_weight", base: "", fields: [{ name: "permission", type: "permission_level" }, { name: "weight", type: "uint16" }] }, + { name: "wait_weight", base: "", fields: [{ name: "wait_sec", type: "uint32" }, { name: "weight", type: "uint16" }] }, + { name: "authority", base: "", fields: [{ name: "threshold", type: "uint32" }, { name: "keys", type: "key_weight[]" }, { name: "accounts", type: "permission_level_weight[]" }, { name: "waits", type: "wait_weight[]" }] }, + { name: "updateauth", base: "", fields: [{ name: "account", type: "name" }, { name: "permission", type: "name" }, { name: "parent", type: "name" }, { name: "auth", type: "authority" }, { name: "authorized_by", type: "name$" }] }, + { name: "buyrambytes", base: "", fields: [{ name: "payer", type: "name" }, { name: "receiver", type: "name" }, { name: "bytes", type: "uint32" }] }, + ], + actions: [{ name: "updateauth", type: "updateauth" }, { name: "buyrambytes", type: "buyrambytes" }], +}; + +/** A dapp's updateauth handing the active permission to another key (data given as JSON, serialised with the ABI). */ +export const TAKEOVER_DATA = { + account: ME, + permission: "active", + parent: "owner", + auth: { threshold: 1, keys: [{ key: "PUB_K1_5UAjunGLeR6eBfbpU4CxGssxa9DKKjbPA4zrCuUpoJQwr6a12W", weight: 1 }], accounts: [], waits: [] }, +}; +export const TAKEOVER: TransactionJson = { + ...header, + actions: [{ account: "eosio", name: "updateauth", authorization: [{ actor: ME, permission: "owner" }], data: hex(encodeActionData(EOSIO_ABI, "updateauth", TAKEOVER_DATA)) }], +}; diff --git a/packages/chains-antelope/test/helpers.ts b/packages/chains-antelope/test/helpers.ts new file mode 100644 index 00000000..90c80a15 --- /dev/null +++ b/packages/chains-antelope/test/helpers.ts @@ -0,0 +1,61 @@ +import type { Account, ChainContext, SignablePayload, Signature } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { JUNGLE4 } from "../src/index.js"; +import { fromHex } from "../src/bytes.js"; +import { EOSIO_ABI, INFO, ME, PUB_HEX, PUB_K1 } from "./fixtures.js"; +import { SIGS } from "./signatures.js"; + +/** Vault stand-in: the precomputed signature that verifies for the payload (verification only). */ +export const signer = { + sign(p: SignablePayload): Signature { + const s = Object.values(SIGS).find((x) => secp256k1.verify(fromHex(x.rs), p.bytes, fromHex(PUB_HEX), { prehash: false })); + if (!s) throw new Error("no fixture signature for this payload"); + return { scheme: "ecdsa-secp256k1", bytes: fromHex(s.rs), recovery: s.recovery, publicKey: PUB_HEX }; + }, +}; + +export function makeAccount(): Account { + return { id: "antelope:0", family: "antelope", index: 0, curve: "secp256k1", derivationPath: "m/44'/194'/0'/0/0", publicKey: PUB_HEX, address: PUB_K1 }; +} + +type Handler = (body: Record, url: string) => unknown; +export const err = (name: string, code: number, message: string, status = 500) => ({ __status: status, code: status, message: "Internal Service Error", error: { code, name, what: name, details: [{ message }] } }); + +/** Chain API mock by path ("/v1/chain/get_info", "/v2/state/get_tokens"). A handler may return err(...) for an error answer. */ +export function mockChain(over: Record = {}) { + const calls: { url: string; path: string; body: Record }[] = []; + const routes: Record = { ...baseRoutes(), ...over }; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const path = new URL(url).pathname; + const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; + calls.push({ url, path, body }); + const h = routes[path]; + if (!h) return new Response(JSON.stringify(err("exception", 0, "Unknown Endpoint", 404)), { status: 404 }); + const r = h(body, url) as Record | unknown[]; + const status = !Array.isArray(r) && typeof r === "object" && r && "__status" in r ? Number(r.__status) : 200; + return new Response(JSON.stringify(r), { status }); + }) as typeof fetch; + return { fetch: f, calls }; +} + +export function baseRoutes(): Record { + return { + "/v1/chain/get_info": () => INFO, + "/v1/chain/get_accounts_by_authorizers": () => ({ + accounts: [ + { account_name: ME, permission_name: "active", authorizing_key: PUB_K1, weight: 1, threshold: 1 }, + { account_name: ME, permission_name: "owner", authorizing_key: "EOS6zpSNY1YoLxNt2VsvJjoDfBueU6xC1M1ERJw1UoekL1NHn8KNA", weight: 1, threshold: 1 }, + ], + }), + "/v1/chain/get_currency_balance": (b) => (b.symbol === "EOS" ? ["10.0000 EOS"] : b.symbol === "A" ? ["2.5000 A"] : []), + "/v1/chain/get_account": (b) => ({ account_name: b.account_name }), + "/v1/chain/get_abi": (b) => ({ account_name: b.account_name, abi: b.account_name === "eosio" ? EOSIO_ABI : undefined }), + "/v1/chain/send_transaction2": () => ({ transaction_id: "will-be-replaced", processed: { receipt: { status: "executed" } } }), + "/v2/state/get_tokens": () => ({ tokens: [{ symbol: "EOS", precision: 4, amount: 10, contract: "eosio.token" }, { symbol: "EOS", precision: 4, amount: 50, contract: "eosfakecoin1" }, { symbol: "JUNGLE", precision: 4, amount: 100, contract: "eosio.token" }] }), + }; +} + +export function ctxFor(fetchImpl: typeof fetch, network = JUNGLE4): ChainContext { + return { network, account: makeAccount(), fetch: fetchImpl }; +} diff --git a/packages/chains-antelope/test/serializer.test.ts b/packages/chains-antelope/test/serializer.test.ts new file mode 100644 index 00000000..00a91fd7 --- /dev/null +++ b/packages/chains-antelope/test/serializer.test.ts @@ -0,0 +1,152 @@ +import { ABI, Action, Name, PublicKey, Serializer, Signature, Transaction } from "@wharfkit/antelope"; +import { describe, expect, it } from "vitest"; +import { type Abi, AbiError, TOKEN_ABI, decodeActionData, encodeActionData, isTokenTransfer } from "../src/abi.js"; +import { fromHex, hex, isAccountName, isName, nameFromBigInt, nameToBigInt, parsePublicKey, publicKeyString, signatureString } from "../src/bytes.js"; +import { type TransactionJson, packTransaction, signingDigest, tapos, transactionId, unpackTransaction } from "../src/transaction.js"; + +/** A contract ABI with the shapes dapps use: aliases, bases, optionals, arrays, extensions, variants, time types. */ +const DAPP_ABI: Abi = { + version: "eosio::abi/1.2", + types: [{ new_type_name: "account_name", type: "name" }], + structs: [ + { name: "base", base: "", fields: [{ name: "owner", type: "account_name" }] }, + { + name: "doit", + base: "base", + fields: [ + { name: "flags", type: "uint8[]" }, + { name: "big", type: "uint64" }, + { name: "neg", type: "int64" }, + { name: "small", type: "int16" }, + { name: "count", type: "varuint32" }, + { name: "delta", type: "varint32" }, + { name: "ok", type: "bool" }, + { name: "note", type: "string?" }, + { name: "blob", type: "bytes" }, + { name: "hash", type: "checksum256" }, + { name: "key", type: "public_key" }, + { name: "when", type: "time_point_sec" }, + { name: "at", type: "time_point" }, + { name: "slot", type: "block_timestamp_type" }, + { name: "sym", type: "symbol" }, + { name: "code", type: "symbol_code" }, + { name: "fee", type: "extended_asset" }, + { name: "pick", type: "choice" }, + { name: "extra", type: "string$" }, + ], + }, + { name: "pair", base: "", fields: [{ name: "a", type: "uint32" }, { name: "b", type: "name" }] }, + ], + actions: [{ name: "doit", type: "doit", ricardian_contract: "" }], + variants: [{ name: "choice", types: ["uint32", "pair"] }], +}; + +const KEY = "PUB_K1_6zpSNY1YoLxNt2VsvJjoDfBueU6xC1M1ERJw1UoekL1NK2aD4t"; +const VALUE = { + owner: "alice.gm", + flags: [1, 2, 255], + big: "18446744073709551615", + neg: "-9007199254740993", + small: -2, + count: 300, + delta: -77, + ok: true, + note: null, + blob: "00ff10", + hash: "aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906", + key: KEY, + when: "2026-10-06T12:00:00", + at: "2026-10-06T12:00:00.500", + slot: "2026-10-06T12:00:00.500", + sym: "4,EOS", + code: "USDT", + fee: { quantity: "0.0100 EOS", contract: "eosio.token" }, + pick: ["pair", { a: 7, b: "bob" }], +}; + +describe("names, keys and signatures", () => { + it("names round-trip like WharfKit", () => { + for (const n of ["eosio", "eosio.token", "alice.gm", "a", "zzzzzzzzzzzzj", "1", "core.vaulta", "tethertether"]) { + expect(nameToBigInt(n)).toBe(Name.from(n).value.value.toString() === "" ? 0n : BigInt(Name.from(n).value.toString())); + expect(nameFromBigInt(nameToBigInt(n))).toBe(n); + } + expect(isName("eosio.")).toBe(false); + expect(isName("zzzzzzzzzzzzz")).toBe(false); + expect(isAccountName("eosio.token")).toBe(true); + expect(isAccountName("Eosio")).toBe(false); + expect(isAccountName("toolongname123")).toBe(false); + }); + + it("keys: PUB_K1 and legacy EOS spellings, checksums enforced", () => { + const wk = PublicKey.from(KEY); + expect(hex(parsePublicKey(KEY))).toBe(hex(wk.data.array)); + expect(hex(parsePublicKey(wk.toLegacyString()))).toBe(hex(wk.data.array)); + expect(publicKeyString(parsePublicKey(KEY))).toBe(KEY); + expect(() => parsePublicKey(`${KEY.slice(0, -1)}5`)).toThrow(); + }); + + it("SIG_K1 strings match WharfKit", () => { + const rs = fromHex("11".repeat(32) + "22".repeat(32)); + const s = signatureString(rs, 1); + const wk = Signature.from(s); + expect(wk.toString()).toBe(s); + expect(wk.data.array[0]).toBe(32); + }); +}); + +describe("ABI serialisation", () => { + it("encodes and decodes every supported type like WharfKit", () => { + const mine = encodeActionData(DAPP_ABI, "doit", VALUE); + const theirs = Serializer.encode({ object: VALUE, abi: ABI.from(DAPP_ABI as never), type: "doit" }); + expect(hex(mine)).toBe(theirs.hexString); + const back = decodeActionData(DAPP_ABI, "doit", mine); + expect(back).toEqual({ ...VALUE }); + }); + + it("eosio.token transfer without an ABI round trip, and the transfer shape check", () => { + const t = { from: "alice.gm", to: "bob", quantity: "1.5000 EOS", memo: "hi" }; + expect(hex(encodeActionData(TOKEN_ABI, "transfer", t))).toBe(Serializer.encode({ object: t, abi: ABI.from(TOKEN_ABI as never), type: "transfer" }).hexString); + expect(isTokenTransfer(TOKEN_ABI, "transfer")).toBe(true); + expect(isTokenTransfer(DAPP_ABI, "doit")).toBe(false); + }); + + it("refuses what it can't read", () => { + const bad: Abi = { structs: [{ name: "x", base: "", fields: [{ name: "f", type: "float64" }] }], actions: [{ name: "x", type: "x" }] }; + expect(() => encodeActionData(bad, "x", { f: 1 })).toThrow(AbiError); + expect(() => decodeActionData(bad, "x", new Uint8Array(8))).toThrow(AbiError); + expect(() => decodeActionData(TOKEN_ABI, "transfer", fromHex("00"))).toThrow(); + expect(() => decodeActionData(TOKEN_ABI, "issue", new Uint8Array())).toThrow(AbiError); + const t = encodeActionData(TOKEN_ABI, "transfer", { from: "a", to: "b", quantity: "1 X", memo: "" }); + expect(() => decodeActionData(TOKEN_ABI, "transfer", new Uint8Array([...t, 0]))).toThrow(/left over/); + }); +}); + +describe("transactions", () => { + const data = hex(encodeActionData(TOKEN_ABI, "transfer", { from: "alice.gm", to: "bob", quantity: "1.5000 EOS", memo: "hi" })); + const tx: TransactionJson = { + expiration: "2026-10-06T12:00:00", + ref_block_num: 4660, + ref_block_prefix: 3735928559, + max_net_usage_words: 0, + max_cpu_usage_ms: 0, + delay_sec: 0, + context_free_actions: [], + actions: [{ account: "eosio.token", name: "transfer", authorization: [{ actor: "alice.gm", permission: "active" }], data }], + transaction_extensions: [], + }; + const chainId = "73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d"; + + it("packs, ids and digests like WharfKit", () => { + const wk = Transaction.from({ ...tx, transaction_extensions: [], actions: tx.actions.map((a) => Action.from(a)) }); + const packed = packTransaction(tx); + expect(hex(packed)).toBe(Serializer.encode({ object: wk }).hexString); + expect(transactionId(packed)).toBe(wk.id.hexString); + expect(hex(signingDigest(chainId, packed))).toBe(wk.signingDigest(chainId).hexString); + expect(unpackTransaction(packed)).toEqual(tx); + }); + + it("TAPoS from a block id", () => { + const id = "115295f5c53487176d0c821bfdf8bd44261573ace810f6fb68f38626cee854f3"; + expect(tapos(id)).toEqual({ ref_block_num: 0x95f5, ref_block_prefix: 0x1b820c6d }); + }); +}); diff --git a/packages/chains-antelope/test/signatures.ts b/packages/chains-antelope/test/signatures.ts new file mode 100644 index 00000000..6aa10379 --- /dev/null +++ b/packages/chains-antelope/test/signatures.ts @@ -0,0 +1,11 @@ +/** + * Signatures computed once OFFLINE (outside the repo) with the vault's own `deriveFamilyKey` + `signEcdsaCanonical` + * (RFC 6979 with the canonical-K1 retry counter, low-S) for the public BIP-39 test vector ("abandon" ×11 + "about", + * empty passphrase) at m/44'/194'/0'/0/0, over SHA-256(chain_id ‖ packed_trx ‖ 32 zero bytes) of the fixtures in + * ./fixtures.ts on Jungle4. r ‖ s hex and the recovery id, as the vault returns them. AGENTS.md rule 1: only + * signatures live here, never keys or signing code. + */ +export const SIGS = { + EOS_SEND: { rs: "3684361037bd1a9a964e95b027298504ce3cdcb0f363717621423626685a97a95f10044be5e1f7a00b0a1b32d5b5517401e3eed48d671e22bbd997ad4863a597", recovery: 0 }, + TAKEOVER: { rs: "080f15df8c332fe9467f33ccd1ba28a5b5ad32c1dc4cb4bc0e6d6544f9ec863614155ea9b6f31d8a21cb5ed6a292c3d78bb1e89b54311da28a66d7b6ccfae486", recovery: 0 }, +} as const; diff --git a/packages/chains-antelope/tsconfig.json b/packages/chains-antelope/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-antelope/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-bitcoincash/LICENSE b/packages/chains-bitcoincash/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-bitcoincash/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-bitcoincash/NOTICE b/packages/chains-bitcoincash/NOTICE new file mode 100644 index 00000000..1a5e8326 --- /dev/null +++ b/packages/chains-bitcoincash/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-bitcoincash +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-bitcoincash/README.md b/packages/chains-bitcoincash/README.md new file mode 100644 index 00000000..5d191803 --- /dev/null +++ b/packages/chains-bitcoincash/README.md @@ -0,0 +1,100 @@ +# @clip-wallet/chains-bitcoincash + +Bitcoin Cash `ChainModule` for Clip Wallet. It builds and decodes BCH transactions in plain words and never touches +keys. `prepare()` returns one 32-byte BCH sighash per input the account signs; `finalize()` checks each signature +with `secp256k1.verify`, DER-encodes it with the sighash byte `0x41`, builds the P2PKH unlocking bytecode and +broadcasts through Fulcrum when the request asks for it. + +Everything is hand-written with `@noble/hashes` and `@noble/curves` (verification only): CashAddr, the transaction +format with CashTokens prefixes, the BCH signing serialization, DER, the Electrum Cash client. `@bitauth/libauth` 3 is +a dev dependency: the tests compare our addresses, transaction bytes and sighashes with it and run every signed +transaction through its BCH 2023 virtual machine (`createVirtualMachineBCH().verify`). + +## Networks + +| NetworkId (CAIP-2) | network | prefix | WalletConnect (wc2-bch-bcr) | Fulcrum (wss, in order) | explorer | +|---|---|---|---|---|---| +| `bip122:00000000040ba9641ba98a37b2e5ceea` | chipnet (default test network) | `bchtest` | `bch:bchtest` | chipnet.imaginary.cash:50004, chipnet.bch.ninja:50004, chipnet.c3-soft.com:64004, cbch.loping.net:62104 | https://cbch.loping.net | +| `bip122:000000001dd410c49a788668ce267517` | testnet4 | `bchtest` | `bch:bchtest` | testnet4.imaginary.cash:50004, tbch4.loping.net:62004 | https://tbch4.loping.net | +| `bip122:000000000000000000651ef99cb9fcbe` | mainnet | `bitcoincash` | `bch:bitcoincash` | bch.imaginary.cash:50004, electrum.imaginary.cash:50004, bch.loping.net:50004, bch.soul-dev.com:50004 | https://bch.loping.net | + +- Ids: BIP-122 / ChainAgnostic `bip122` (first 32 hex of a block hash). BCH shares Bitcoin's genesis, so the + namespace's own test case names BCH mainnet by its first block after the split (478559). Chipnet is named the same + way by its first block after it left testnet4 (115252, found by comparing both chains' headers on Fulcrum); + testnet4 by its genesis. None collides with chains-bitcoin. +- The BCH wallet community's WalletConnect spec uses `bch:bitcoincash` and `bch:bchtest` (any testnet) instead: + `BCH_NETS[n].wcChain`, and 1Mask's `BCH_WC_CHAINS` maps them (bchtest → chipnet). +- Every Fulcrum server above answered `server.version` (Fulcrum 2.1.x, protocol 1.4), + `blockchain.scripthash.get_balance`, `blockchain.headers.get_tip` and `blockchain.relayfee` (2026-10). They're + tried in order; the next one is used when one can't be reached. Calls: `blockchain.scripthash.get_balance`, + `listunspent` (with `token_data`), `blockchain.relayfee`, `blockchain.transaction.broadcast`. +- Faucet: https://tbch.googol.cash (testnet3, testnet4 and chipnet): no account, but a simple image captcha (type + the sum of the numbers shown). + +Assets: BCH is `bch` (8 decimals). Fungible CashTokens are `cashtoken:` (no metadata registry is +trusted, so they show as `CT-` with 0 decimals). `getNfts` returns `[]`. + +## Accounts and addresses + +- `derivationPath(i)` = `m/44'/145'/0'/0/i` (Electron Cash, Paytaca, Cashonize), secp256k1, the same key on every + network. +- P2PKH CashAddr, spelled per network: `bitcoincash:q…` on mainnet, `bchtest:q…` on chipnet / testnet4. + `Account.address` is the mainnet form; `receiveAddress(ctx)` / `addressFromPublicKey(key, network)` give the + network's. Fixtures: "abandon … about" → `bitcoincash:qqyx49mu…tahq3q6`, as libauth encodes it. +- `isAddress` accepts CashAddr P2PKH / P2SH (20 and 32-byte) and the token-aware `z…` / `r…` forms, with or without + the prefix, checksum verified; mixed case and legacy base58 (`1…`, `3…`, which Bitcoin uses too) are refused. + +## Sending (`buildTransfer`) + +- Coins from `listunspent`, largest first. A coin that carries CashTokens is never spent by a plain BCH send (it + would burn the tokens). +- Fee: the server's relay fee (1 sat/byte on every network today), at least `minFeeRate` (default 1). Size estimate + 148 bytes per P2PKH input, 34 per output, 10 overhead. +- Dust limit 546 sats: smaller sends are refused; change under it goes to the fee. +- **Change goes back to the same address** (the account's one P2PKH address; there is no change chain). Electron + Cash and Paytaca will see it like any other coin of that address. +- The request is a `bch_signTransaction` with the unsigned transaction hex, the coins as libauth-stringified + source outputs and `broadcast: true`, so wallet sends and app requests go through the same decoder. + +## Methods (`BCH_METHODS`, wc2-bch-bcr) + +| method | params | result | +|---|---|---| +| `bch_signTransaction` | `{ transaction: hex \| libauth Transaction \| stringify(Transaction), sourceOutputs: (stringified) libauth source outputs, broadcast? = true, userPrompt? }` | `{ signedTransaction: hex, signedTransactionHash: txid }` | +| `bch_signMessage` | `{ message, userPrompt? }` | base64 compact signature (Electron Cash format) | +| `bch_getAddresses` | (answered by 1Mask, not the module) | `["bchtest:q…"]` | + +- Signs every input whose source output is this account's P2PKH and whose unlocking bytecode is empty. Other + inputs (CashScript contracts, other people) are left as the app sent them. +- wc2-bch-bcr's zero-filled pubkey (33 bytes) / Schnorr signature (65 bytes) placeholders for CashScript contract + arguments are **refused** in plain words (`bitcoincash/unsupported-contract`): the vault signs ECDSA only. +- Source outputs must match the inputs one to one (same outpoints, same order) or the request is unreadable (blind). +- Refusals: other network → `bitcoincash/network-mismatch`; no input of ours → `bitcoincash/not-a-signer`. + +### What gets signed (`prepare`) + +- **Inputs:** double SHA-256 of version ‖ hashPrevouts ‖ hashSequence ‖ outpoint ‖ the spent output's token prefix + ‖ scriptCode (our P2PKH) ‖ value ‖ sequence ‖ hashOutputs ‖ locktime ‖ `41 00 00 00` (SIGHASH_ALL | FORKID, fork + id 0). The token prefix and the value are committed, so an app can't lie about our coins. +- **Messages:** double SHA-256 of CompactSize(24) ‖ "Bitcoin Signed Message:\n" ‖ CompactSize(len) ‖ message; + answered as base64(27 + recovery + 4 ‖ r ‖ s). + +### Decoding + +"Send 0.0003 BCH to qp8sfd…q4lg", one line per recipient (tokens included), "You get back" for change to this +address, the network fee, data outputs (OP_RETURN), the app's contract name and prompt (marked unverified), how +many inputs belong to others. Balance changes are this account's BCH in − out and token amounts. CashTokens leaving +the wallet get a caution. A fee above 20 sat/byte (and 10,000 sats) is a "high fee" caution. + +## Dapp connectivity + +There's no injected-provider standard for BCH (Paytaca's extension injects its own `window.paytaca`, which another +wallet mustn't imitate). Dapps reach wallets over WalletConnect v2 with wc2-bch-bcr, where the wallet shows up +under its own metadata. In `@clip-wallet/1mask` the WalletConnect session layer serves the `bch` namespace: +proposals for `bch:bitcoincash` / `bch:bchtest` map onto the bip122 networks (`networkIdForWcChain`), session +accounts are `bch:` spelled for the chain (as Cashonize builds them), `bch_getAddresses` is answered from +the session, the signing methods reach this module unchanged, and `addressesChanged` carries the CashAddrs. + +## Not supported (yet) + +Sending CashTokens, CashScript placeholders (Schnorr), P2SH spending, NFTs in `getNfts`, BCMR token metadata. diff --git a/packages/chains-bitcoincash/package.json b/packages/chains-bitcoincash/package.json new file mode 100644 index 00000000..ef4bd6af --- /dev/null +++ b/packages/chains-bitcoincash/package.json @@ -0,0 +1,78 @@ +{ + "name": "@clip-wallet/chains-bitcoincash", + "version": "0.1.0", + "description": "Bitcoin Cash ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "bitcoincash", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-bitcoincash#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-bitcoincash" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0" + }, + "devDependencies": { + "@bitauth/libauth": "^3.0.0" + } +} diff --git a/packages/chains-bitcoincash/src/cashaddr.ts b/packages/chains-bitcoincash/src/cashaddr.ts new file mode 100644 index 00000000..fabe3c03 --- /dev/null +++ b/packages/chains-bitcoincash/src/cashaddr.ts @@ -0,0 +1,140 @@ +import { concat, hash160 } from "./util.js"; + +/** + * CashAddr (https://reference.cash/protocol/blockchain/encoding/cashaddr): prefix ":" base32(version ‖ hash ‖ + * 40-bit BCH checksum). Version byte = type << 3 | size: type 0 P2PKH, 1 P2SH, 2 token-aware P2PKH, 3 token-aware + * P2SH (CHIP-2022-02-CashTokens); size 0 = 160-bit hash, 3 = 256-bit (P2SH32). Same encoding as the vault's + * `cashAddress` (packages/vault/src/encodings87.ts); checked against libauth in the tests. + */ +const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; +const SIZE_BITS: Record = { 0: 20, 1: 24, 2: 28, 3: 32, 4: 40, 5: 48, 6: 56, 7: 64 }; + +export type CashKind = "p2pkh" | "p2sh"; +export const PREFIXES = ["bitcoincash", "bchtest", "bchreg"] as const; +export type CashPrefix = (typeof PREFIXES)[number]; + +function polymod(values: number[]): bigint { + const GEN = [0x98f2bc8e61n, 0x79b76d99e2n, 0xf33e5fb3c4n, 0xae2eabe2a8n, 0x1e4f43e470n]; + let c = 1n; + for (const d of values) { + const c0 = c >> 35n; + c = ((c & 0x07ffffffffn) << 5n) ^ BigInt(d); + for (let i = 0; i < 5; i++) if ((c0 >> BigInt(i)) & 1n) c ^= GEN[i]!; + } + return c ^ 1n; +} + +function convertBits(data: ArrayLike, from: number, to: number, pad: boolean): number[] | null { + let acc = 0; + let bits = 0; + const out: number[] = []; + const max = (1 << to) - 1; + for (let i = 0; i < data.length; i++) { + const v = data[i]!; + if (v < 0 || v >> from) return null; + acc = (acc << from) | v; + bits += from; + while (bits >= to) { + bits -= to; + out.push((acc >> bits) & max); + } + } + if (pad) { + if (bits > 0) out.push((acc << (to - bits)) & max); + } else if (bits >= from || ((acc << (to - bits)) & max)) return null; + return out; +} + +const prefixWords = (prefix: string) => [...prefix].map((ch) => ch.charCodeAt(0) & 31); + +export function encodeCashAddress(prefix: string, type: number, hash: Uint8Array): string { + const size = Object.entries(SIZE_BITS).find(([, n]) => n === hash.length)?.[0]; + if (size === undefined || type < 0 || type > 15) throw new Error("bad cashaddr input"); + const payload = convertBits(concat(new Uint8Array([(type << 3) | Number(size)]), hash), 8, 5, true)!; + const mod = polymod([...prefixWords(prefix), 0, ...payload, 0, 0, 0, 0, 0, 0, 0, 0]); + const check: number[] = []; + for (let i = 0; i < 8; i++) check.push(Number((mod >> BigInt(5 * (7 - i))) & 31n)); + return `${prefix}:${[...payload, ...check].map((w) => CHARSET[w]).join("")}`; +} + +export interface DecodedCashAddress { + prefix: CashPrefix; + /** 0 P2PKH, 1 P2SH, 2 token-aware P2PKH, 3 token-aware P2SH. */ + type: number; + kind: CashKind; + tokenAware: boolean; + hash: Uint8Array; +} + +/** + * Parses a CashAddr with its prefix, or without one when `defaultPrefix` is given (the checksum decides). Mixed + * case is refused, as the spec says. Null for anything that isn't a valid P2PKH/P2SH CashAddr. + */ +export function decodeCashAddress(value: string, defaultPrefix?: CashPrefix): DecodedCashAddress | null { + const v = value.trim(); + if (v !== v.toLowerCase() && v !== v.toUpperCase()) return null; + const lower = v.toLowerCase(); + const i = lower.indexOf(":"); + const candidates: string[] = i >= 0 ? [lower.slice(0, i)] : defaultPrefix ? [defaultPrefix] : [...PREFIXES]; + const body = i >= 0 ? lower.slice(i + 1) : lower; + if (!body || body.length < 42 || body.length > 112) return null; + const words: number[] = []; + for (const ch of body) { + const w = CHARSET.indexOf(ch); + if (w < 0) return null; + words.push(w); + } + for (const prefix of candidates) { + if (!(PREFIXES as readonly string[]).includes(prefix)) continue; + if (polymod([...prefixWords(prefix), 0, ...words]) !== 0n) continue; + const data = convertBits(words.slice(0, -8), 5, 8, false); + if (!data || !data.length) return null; + const version = data[0]!; + if (version & 0x80) return null; + const type = version >> 3; + const size = SIZE_BITS[version & 7]; + const hash = Uint8Array.from(data.slice(1)); + if (size === undefined || hash.length !== size) return null; + if (type > 3) return null; + const kind: CashKind = type === 0 || type === 2 ? "p2pkh" : "p2sh"; + if (kind === "p2pkh" && hash.length !== 20) return null; + if (kind === "p2sh" && hash.length !== 20 && hash.length !== 32) return null; + return { prefix: prefix as CashPrefix, type, kind, tokenAware: type >= 2, hash }; + } + return null; +} + +/** P2PKH CashAddr of a compressed key. */ +export function p2pkhAddress(compressedKey: Uint8Array, prefix: CashPrefix): string { + if (compressedKey.length !== 33) throw new Error("Bitcoin Cash needs a compressed secp256k1 public key"); + return encodeCashAddress(prefix, 0, hash160(compressedKey)); +} + +/* ------------------------------------------------------------------ locking bytecode */ + +const OP_DUP = 0x76; +const OP_HASH160 = 0xa9; +const OP_HASH256 = 0xaa; +const OP_EQUALVERIFY = 0x88; +const OP_EQUAL = 0x87; +const OP_CHECKSIG = 0xac; + +export function p2pkhScript(h: Uint8Array): Uint8Array { + return concat(new Uint8Array([OP_DUP, OP_HASH160, 20]), h, new Uint8Array([OP_EQUALVERIFY, OP_CHECKSIG])); +} + +export function lockingBytecodeOf(a: DecodedCashAddress): Uint8Array { + if (a.kind === "p2pkh") return p2pkhScript(a.hash); + if (a.hash.length === 32) return concat(new Uint8Array([OP_HASH256, 32]), a.hash, new Uint8Array([OP_EQUAL])); + return concat(new Uint8Array([OP_HASH160, 20]), a.hash, new Uint8Array([OP_EQUAL])); +} + +/** The CashAddr a locking bytecode pays to (P2PKH, P2SH20, P2SH32), else null. */ +export function addressOfScript(script: Uint8Array, prefix: CashPrefix, tokenAware = false): string | null { + if (script.length === 25 && script[0] === OP_DUP && script[1] === OP_HASH160 && script[2] === 20 && script[23] === OP_EQUALVERIFY && script[24] === OP_CHECKSIG) { + return encodeCashAddress(prefix, tokenAware ? 2 : 0, script.subarray(3, 23)); + } + if (script.length === 23 && script[0] === OP_HASH160 && script[1] === 20 && script[22] === OP_EQUAL) return encodeCashAddress(prefix, tokenAware ? 3 : 1, script.subarray(2, 22)); + if (script.length === 35 && script[0] === OP_HASH256 && script[1] === 32 && script[34] === OP_EQUAL) return encodeCashAddress(prefix, tokenAware ? 3 : 1, script.subarray(2, 34)); + return null; +} diff --git a/packages/chains-bitcoincash/src/electrum.ts b/packages/chains-bitcoincash/src/electrum.ts new file mode 100644 index 00000000..a209af08 --- /dev/null +++ b/packages/chains-bitcoincash/src/electrum.ts @@ -0,0 +1,173 @@ +import { ClipError, msg } from "@clip-wallet/core"; + +/** + * A small Electrum Cash protocol (1.4) client over WebSocket for public Fulcrum servers + * (https://electrum-cash-protocol.readthedocs.io). One session per wallet operation: open, `server.version`, the + * calls, close. Servers are tried in order; the next one is used when one can't be reached or times out. An error + * the server answers with (e.g. a rejected broadcast) is not retried elsewhere. + */ +export interface SocketLike { + onopen: ((ev: unknown) => void) | null; + onmessage: ((ev: { data: unknown }) => void) | null; + onerror: ((ev: unknown) => void) | null; + onclose: ((ev: unknown) => void) | null; + send(data: string): void; + close(): void; +} + +export type SocketFactory = (url: string) => SocketLike; + +export const defaultSocketFactory: SocketFactory = (url) => { + const WS = (globalThis as { WebSocket?: new (u: string) => SocketLike }).WebSocket; + if (!WS) throw new Error("no WebSocket in this environment"); + return new WS(url); +}; + +export class ElectrumError extends Error { + constructor( + message: string, + readonly code?: number, + ) { + super(message); + } +} + +class Connection { + #id = 0; + readonly #pending = new Map }>(); + #closed = false; + + constructor( + readonly url: string, + readonly socket: SocketLike, + readonly timeoutMs: number, + ) { + socket.onmessage = (ev) => { + let m: { id?: number; result?: unknown; error?: { message?: string; code?: number } | string }; + try { + m = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data)); + } catch { + return; + } + const p = typeof m.id === "number" ? this.#pending.get(m.id) : undefined; + if (!p) return; // notifications + this.#pending.delete(m.id!); + clearTimeout(p.timer); + if (m.error !== undefined && m.error !== null) { + const e = typeof m.error === "string" ? { message: m.error } : m.error; + p.reject(new ElectrumError(String(e.message ?? "server error"), e.code)); + } else p.resolve(m.result); + }; + const fail = () => this.#failAll(new Error("connection closed")); + socket.onclose = fail; + socket.onerror = fail; + } + + #failAll(e: Error) { + this.#closed = true; + for (const [, p] of this.#pending) { + clearTimeout(p.timer); + p.reject(e); + } + this.#pending.clear(); + } + + call(method: string, params: unknown[]): Promise { + if (this.#closed) return Promise.reject(new Error("connection closed")); + const id = ++this.#id; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.#pending.delete(id); + reject(new Error("timeout")); + }, this.timeoutMs); + this.#pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer }); + this.socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, params })); + }); + } + + close() { + this.#closed = true; + try { + this.socket.close(); + } catch { + /* ignore */ + } + } +} + +function open(url: string, factory: SocketFactory, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + let socket: SocketLike; + try { + socket = factory(url); + } catch (e) { + reject(e); + return; + } + const timer = setTimeout(() => { + try { + socket.close(); + } catch { + /* ignore */ + } + reject(new Error("connect timeout")); + }, timeoutMs); + socket.onopen = () => { + clearTimeout(timer); + resolve(new Connection(url, socket, timeoutMs)); + }; + socket.onerror = () => { + clearTimeout(timer); + reject(new Error("connect failed")); + }; + }); +} + +export interface ElectrumOptions { + factory?: SocketFactory; + timeoutMs?: number; + clientName?: string; +} + +export type Call = (method: string, params?: unknown[]) => Promise; + +/** + * Runs `fn` against the first server that answers `server.version`. A transport failure inside `fn` (closed socket, + * timeout) moves on to the next server and runs `fn` again; an ElectrumError (the server's own answer) does not. + */ +export async function withElectrum(urls: readonly string[], opts: ElectrumOptions, fn: (call: Call) => Promise): Promise { + const factory = opts.factory ?? defaultSocketFactory; + const timeoutMs = opts.timeoutMs ?? 12_000; + let last: unknown; + for (const url of urls) { + let c: Connection | undefined; + try { + c = await open(url, factory, timeoutMs); + await c.call("server.version", [opts.clientName ?? "Clip Wallet", "1.4"]); + const call: Call = (method, params = []) => c!.call(method, params); + return await fn(call); + } catch (e) { + if (e instanceof ElectrumError || e instanceof ClipError) throw e; + last = e; + } finally { + c?.close(); + } + } + throw new ClipError(msg("bg.err.couldntReach", { what: "Bitcoin Cash" }), "bitcoincash/offline", last); +} + +/* ------------------------------------------------------------------ response shapes (Fulcrum 2.x) */ + +export interface ElectrumUtxo { + tx_hash: string; + tx_pos: number; + height: number; + value: number; + /** CashTokens (Fulcrum ≥ 1.9): present when the output carries tokens. */ + token_data?: { category: string; amount?: string; nft?: { capability: "none" | "mutable" | "minting"; commitment: string } }; +} + +export interface ElectrumBalance { + confirmed: number; + unconfirmed: number; +} diff --git a/packages/chains-bitcoincash/src/index.ts b/packages/chains-bitcoincash/src/index.ts new file mode 100644 index 00000000..e7f0498b --- /dev/null +++ b/packages/chains-bitcoincash/src/index.ts @@ -0,0 +1,12 @@ +export { BCH_METHODS, type BitcoinCashModule, type BitcoinCashModuleOptions, createBitcoinCashModule, normalize, ownScript, plainBchError, scripthash } from "./module.js"; +export { BCH_CHIPNET, BCH_MAINNET, BCH_NETS, BCH_NETWORKS, BCH_TESTNET4, type BchNet, type BchNetSpec, bchAsset, cashTokenAsset, explorerTxUrl, netOf } from "./networks.js"; +export { type CashPrefix, type DecodedCashAddress, PREFIXES, addressOfScript, decodeCashAddress, encodeCashAddress, lockingBytecodeOf, p2pkhAddress, p2pkhScript } from "./cashaddr.js"; +export { type BchTx, DUST, SIGHASH_ALL_FORKID, type TokenData, decodeTx, derSignature, encodeTokenPrefix, encodeTx, estimateSize, p2pkhUnlocking, readOutputField, sighash, txidOf } from "./tx.js"; +export { type SourceOutput, parseSourceOutputs, parseTransaction, revive, stringifyExtended } from "./wc.js"; +export { MESSAGE_MAGIC, compactSignatureBase64, messageHash } from "./message.js"; +export { type Call, type ElectrumOptions, ElectrumError, type SocketFactory, type SocketLike, withElectrum } from "./electrum.js"; + +import { createBitcoinCashModule } from "./module.js"; + +/** Default instance (Fulcrum servers over the environment's WebSocket). */ +export const bitcoincashModule = createBitcoinCashModule(); diff --git a/packages/chains-bitcoincash/src/message.ts b/packages/chains-bitcoincash/src/message.ts new file mode 100644 index 00000000..20d6130c --- /dev/null +++ b/packages/chains-bitcoincash/src/message.ts @@ -0,0 +1,23 @@ +import { compactSize, concat, sha256d, utf8 } from "./util.js"; + +/** + * Message signing as Electron Cash (and wc2-bch-bcr `bch_signMessage`) does it: the Bitcoin signed-message format, + * double SHA-256 of CompactSize(24) ‖ "Bitcoin Signed Message:\n" ‖ CompactSize(len) ‖ message, answered as base64 of + * header ‖ r ‖ s with header = 27 + recovery id + 4 (compressed key). + */ +export const MESSAGE_MAGIC = "Bitcoin Signed Message:\n"; + +export function messageHash(message: string | Uint8Array): Uint8Array { + const m = typeof message === "string" ? utf8(message) : message; + const magic = utf8(MESSAGE_MAGIC); + return sha256d(concat(compactSize(magic.length), magic, compactSize(m.length), m)); +} + +export function compactSignatureBase64(rs: Uint8Array, recovery: number): string { + const b = new Uint8Array(65); + b[0] = 27 + recovery + 4; + b.set(rs, 1); + let s = ""; + for (const x of b) s += String.fromCharCode(x); + return btoa(s); +} diff --git a/packages/chains-bitcoincash/src/module.ts b/packages/chains-bitcoincash/src/module.ts new file mode 100644 index 00000000..e2e1d65e --- /dev/null +++ b/packages/chains-bitcoincash/src/module.ts @@ -0,0 +1,476 @@ +import { + type AssetRef, + type BalanceChange, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Msg, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + WALLET_ORIGIN, + isWalletOrigin, + msg, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { type CashPrefix, PREFIXES, addressOfScript, decodeCashAddress, lockingBytecodeOf, p2pkhAddress, p2pkhScript } from "./cashaddr.js"; +import { type Call, type ElectrumBalance, ElectrumError, type ElectrumOptions, type ElectrumUtxo, withElectrum } from "./electrum.js"; +import { compactSignatureBase64, messageHash } from "./message.js"; +import { BCH_NETS, type BchNet, bchAsset, cashTokenAsset, netOf } from "./networks.js"; +import { type BchTx, DUST, type TokenData, decodeTx, encodeTx, estimateSize, p2pkhUnlocking, sighash, txidOf } from "./tx.js"; +import { type SourceOutput, parseSourceOutputs, parseTransaction, sourceOutputJson, stringifyExtended } from "./wc.js"; +import { bytesEqual, formatUnits, fromHex, hash160, hex, hostOf, randomId, reversed, short, textOf } from "./util.js"; + +/** + * Request methods: the BCH WalletConnect spec's (wc2-bch-bcr, https://github.com/mainnet-pat/wc2-bch-bcr), so a + * WalletConnect session hands them to this module unchanged. Wallet sends use bch_signTransaction too (with + * `broadcast: true`). + */ +export const BCH_METHODS = { + signTransaction: "bch_signTransaction", + signMessage: "bch_signMessage", + /** Answered by the 1Mask background (the account's address on the session's network), never by this module. */ + getAddresses: "bch_getAddresses", +} as const; + +export interface BitcoinCashModuleOptions extends ElectrumOptions { + /** Electrum (Fulcrum) WebSocket URLs per NetworkId, overriding `network.rpcUrls`. */ + electrumUrls?: Record; + /** Minimum fee rate in satoshis per byte (default 1; the network's relay fee if higher). */ + minFeeRate?: number; +} + +type Op = + | { kind: "message"; text: string; prompt?: string } + | { kind: "tx"; tx: BchTx; sources: SourceOutput[]; broadcast: boolean; prompt?: string } + | { kind: "unreadable"; reason: unknown }; + +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); +const fmt = (sats: bigint) => `${formatUnits(sats, 8)} BCH`; + +function netName(networkId: string): BchNet { + const n = netOf(networkId); + if (!n) throw new ClipError("That network isn't available in this wallet.", "bitcoincash/unknown-network"); + return n; +} + +function publicKeyOf(ctx: ChainContext): Uint8Array { + const raw = fromHex(ctx.account.publicKey); + return raw.length === 33 ? raw : secp256k1.Point.fromBytes(raw).toBytes(true); +} + +/** This account's P2PKH locking bytecode (the same on every network). */ +export function ownScript(ctx: ChainContext): Uint8Array { + return p2pkhScript(hash160(publicKeyOf(ctx))); +} + +/** Electrum scripthash: SHA-256 of the locking bytecode, byte-reversed, hex. */ +export function scripthash(locking: Uint8Array): string { + return hex(reversed(sha256(locking))); +} + +/** Checks method and network, and parses the request. Unparseable transactions become "unreadable" (blind). */ +export function normalize(request: DappRequest, ctx: ChainContext): Op { + if (!netOf(request.networkId) || request.networkId !== ctx.network.id) { + throw new ClipError("This app is asking for a different Bitcoin Cash network than the one it's connected to.", "bitcoincash/network-mismatch"); + } + const p = obj(request.params); + const prompt = typeof p.userPrompt === "string" && p.userPrompt ? p.userPrompt.slice(0, 200) : undefined; + if (request.method === BCH_METHODS.signMessage) { + if (typeof p.message !== "string") throw new ClipError("This request is missing its message.", "bitcoincash/bad-params"); + return { kind: "message", text: p.message, ...(prompt ? { prompt } : {}) }; + } + if (request.method === BCH_METHODS.signTransaction) { + try { + const tx = parseTransaction(p.transaction); + const sources = parseSourceOutputs(p.sourceOutputs); + if (sources.length !== tx.inputs.length) throw new Error("source outputs don't match inputs"); + tx.inputs.forEach((i, k) => { + if (sources[k]!.outpoint !== `${hex(i.txid)}:${i.vout}`) throw new Error("source output order"); + }); + return { kind: "tx", tx, sources, broadcast: p.broadcast !== false, ...(prompt ? { prompt } : {}) }; + } catch (reason) { + return { kind: "unreadable", reason }; + } + } + throw new ClipError("Clip Wallet doesn't support this Bitcoin Cash request yet.", "bitcoincash/unsupported-method"); +} + +const ZERO33 = new Uint8Array(33); +const ZERO65 = new Uint8Array(65); +function hasPlaceholder(b: Uint8Array): boolean { + const find = (n: Uint8Array) => { + outer: for (let i = 0; i + n.length <= b.length; i++) { + for (let j = 0; j < n.length; j++) if (b[i + j] !== 0) continue outer; + return true; + } + return false; + }; + return b.length >= 33 && (find(ZERO65) || find(ZERO33)); +} + +/** Indexes of the inputs this account signs: our P2PKH coin with an empty unlocking bytecode. */ +function signingInputs(op: Extract, ctx: ChainContext): number[] { + const mine = ownScript(ctx); + const out: number[] = []; + op.tx.inputs.forEach((input, i) => { + const src = op.sources[i]!; + if (input.unlocking.length === 0 && bytesEqual(src.locking, mine)) out.push(i); + else if (input.unlocking.length && hasPlaceholder(input.unlocking)) { + // wc2-bch-bcr: zero-filled 33/65-byte placeholders ask the wallet to insert its key / a Schnorr signature + // into a CashScript contract's unlocking data. Not supported: refuse in plain words instead of guessing. + throw new ClipError(msg("bg.bch.contractPlaceholders"), "bitcoincash/unsupported-contract"); + } + }); + if (!out.length) throw new ClipError("This transaction doesn't need your signature.", "bitcoincash/not-a-signer"); + return out; +} + +function tokenText(t: TokenData): string { + const cat = hex(t.category); + const parts = [] as string[]; + if (t.amount > 0n) parts.push(`${t.amount} × CT-${cat.slice(0, 6)}`); + if (t.nft) parts.push(`NFT CT-${cat.slice(0, 6)}${t.nft.commitment.length ? ` #${hex(t.nft.commitment).slice(0, 16)}` : ""}${t.nft.capability !== "none" ? ` (${t.nft.capability})` : ""}`); + return parts.join(" + "); +} + +export interface BitcoinCashModule extends ChainModule { + normalize: typeof normalize; + receiveAddress(ctx: ChainContext): Promise; + /** Satoshis: confirmed and unconfirmed (may be negative while a spend is unconfirmed). */ + bchBalance(ctx: ChainContext): Promise<{ confirmed: string; unconfirmed: string }>; +} + +export function createBitcoinCashModule(options: BitcoinCashModuleOptions = {}): BitcoinCashModule { + function prefixOf(networkId: string): CashPrefix { + return BCH_NETS[netName(networkId)].prefix; + } + + function electrum(ctx: ChainContext, fn: (call: Call) => Promise): Promise { + const urls = options.electrumUrls?.[ctx.network.id] ?? (ctx.network.rpcUrls.length ? ctx.network.rpcUrls : BCH_NETS[netName(ctx.network.id)].electrum); + return withElectrum(urls, options, fn); + } + + function addressOn(ctx: ChainContext): string { + return p2pkhAddress(publicKeyOf(ctx), prefixOf(ctx.network.id)); + } + + async function feeRate(call: Call): Promise { + const min = BigInt(Math.max(1, Math.ceil(options.minFeeRate ?? 1))); + const relay = await call("blockchain.relayfee").catch(() => 0); + // BCH per kB → satoshis per byte (round to whole satoshis first: 0.00001 * 1e8 isn't exactly 1000 in floats). + const perByte = typeof relay === "number" && relay > 0 ? (BigInt(Math.round(relay * 1e8)) + 999n) / 1000n : 1n; + return perByte > min ? perByte : min; + } + + /* ---------------------------------------------------------------- decode */ + + function describeTx(req: DappRequest, op: Extract, ctx: ChainContext): Omit { + const prefix = prefixOf(ctx.network.id); + const mine = ownScript(ctx); + const me = addressOn(ctx); + const host = hostOf(req.origin); + const signing = new Set(signingInputs(op, ctx)); + const lines: DecodedRequest["lines"] = []; + const warnings: DecodedRequest["warnings"] = []; + const changes: BalanceChange[] = []; + + let inTotal = 0n; + let ourIn = 0n; + /** Per token category: fungible amount and NFT count leaving this account (inputs − outputs back to it). */ + const tokensOut = new Map(); + op.sources.forEach((s) => { + inTotal += s.value; + if (bytesEqual(s.locking, mine)) { + ourIn += s.value; + if (s.token) { + const k = hex(s.token.category); + const prev = tokensOut.get(k); + tokensOut.set(k, { t: s.token, n: (prev?.n ?? 0n) + s.token.amount, nft: (prev?.nft ?? 0) + (s.token.nft ? 1 : 0) }); + } + } + }); + + let outTotal = 0n; + let ourOut = 0n; + const external: { to: string; value: bigint; token?: TokenData }[] = []; + for (const o of op.tx.outputs) { + outTotal += o.value; + if (bytesEqual(o.locking, mine)) { + ourOut += o.value; + if (o.token) { + const k = hex(o.token.category); + const prev = tokensOut.get(k); + tokensOut.set(k, { t: o.token, n: (prev?.n ?? 0n) - o.token.amount, nft: (prev?.nft ?? 0) - (o.token.nft ? 1 : 0) }); + } + continue; + } + if (o.locking[0] === 0x6a) { + const text = textOf(o.locking.subarray(o.locking[1]! <= 75 ? 2 : 3)); + lines.push({ label: text ? "Data" : "Data (not text)", value: text || hex(o.locking.subarray(1)).slice(0, 120) }); + continue; + } + const to = addressOfScript(o.locking, prefix, !!o.token) ?? `script ${hex(o.locking).slice(0, 40)}…`; + external.push({ to, value: o.value, ...(o.token ? { token: o.token } : {}) }); + } + + const fee = inTotal - outTotal; + if (fee < 0n) throw new ClipError("This transaction can't be read.", "bitcoincash/bad-transaction"); + const delta = ourOut - ourIn; + if (delta !== 0n) changes.push({ asset: bchAsset(ctx.network.id), delta: delta.toString() }); + for (const [k, v] of tokensOut) { + if (v.n !== 0n) changes.push({ asset: cashTokenAsset(ctx.network.id, k), delta: (-v.n).toString() }); + if (v.n > 0n || v.nft > 0) warnings.push({ level: "caution", code: "inscribed-utxo", ...warn(msg("bg.bch.spendsTokens", { tokens: tokenText({ ...v.t, amount: v.n > 0n ? v.n : 0n }) })) }); + } + + let title: Msg; + const sent = external.reduce((n, e) => n + e.value, 0n); + if (external.length === 1 && !external[0]!.token) title = msg("bg.req.sendTo", { amount: fmt(external[0]!.value), to: short(external[0]!.to) }); + else if (external.length === 1) title = msg("bg.req.sendToOnly", { to: short(external[0]!.to) }); + else if (external.length > 1) title = msg("bg.req.sendToMany", { amount: fmt(sent), count: external.length }); + else title = isWalletOrigin(req.origin) ? msg("bg.req.moveBetweenOwn", { symbol: "BCH" }) : msg("bg.req.approveTxFor", { host }); + + external.forEach((e, i) => { + lines.push({ label: i === 0 ? "To" : "Also", value: e.to }); + lines.push({ label: "Amount", value: e.token ? `${fmt(e.value)} + ${tokenText(e.token)}` : fmt(e.value) }); + }); + if (ourOut > 0n && external.length) lines.push({ label: "You get back", value: `${fmt(ourOut)} (${short(me)}, the same address)` }); + lines.push({ label: "Network fee", value: fmt(fee) }); + const size = encodeTx(op.tx).length + signing.size * 107; + if (fee > BigInt(size) * 20n && fee > 10_000n) warnings.push({ level: "caution", code: "high-fee", ...warn(msg("bg.bch.highFee", { fee: fmt(fee) })) }); + if (op.sources.some((s) => s.contractName)) lines.push({ label: "App contract", value: op.sources.find((s) => s.contractName)!.contractName! }); + if (op.tx.inputs.length > signing.size) { + const m = msg("bg.bch.othersInputs", { count: op.tx.inputs.length - signing.size }); + lines.push({ label: "Also", value: m.fallback, valueMsg: m }); + } + if (op.prompt && !isWalletOrigin(req.origin)) lines.push({ label: "App says (unverified)", value: op.prompt }); + if (!op.broadcast) lines.push({ label: "Sent by", value: `${host} (it gets the signed transaction)` }); + return { title: title.fallback, titleMsg: title, lines, balanceChanges: changes, fee: { asset: bchAsset(ctx.network.id), amount: fee.toString() }, warnings }; + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const op = normalize(req, ctx); + const host = hostOf(req.origin); + const base = { requestId: req.id, networkId: req.networkId, simulated: false }; + if (op.kind === "message") { + const title = msg("bg.req.signMessage", { host }); + const lines = [{ label: "Message", value: op.text }]; + if (op.prompt) lines.push({ label: "App says (unverified)", value: op.prompt }); + return { ...base, title: title.fallback, titleMsg: title, lines, balanceChanges: [], blind: false, warnings: [] }; + } + if (op.kind === "unreadable") { + const title = msg("bg.req.unreadableFrom", { host }); + return { ...base, title: title.fallback, titleMsg: title, lines: [], balanceChanges: [], blind: true, warnings: [{ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction." }] }; + } + return { ...base, ...describeTx(req, op, ctx), blind: false }; + } + + /* ---------------------------------------------------------------- sign */ + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + const op = normalize(req, ctx); + if (op.kind === "message") return [{ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes: messageHash(op.text), approvalId }]; + if (op.kind === "unreadable") throw new ClipError("This transaction can't be read.", "bitcoincash/bad-transaction", op.reason); + const script = ownScript(ctx); + return signingInputs(op, ctx).map((i) => ({ + accountId: ctx.account.id, + scheme: "ecdsa-secp256k1" as const, + bytes: sighash(op.tx, i, op.sources[i]!, script), + approvalId, + })); + } + + function checkSignature(sig: Signature | undefined, digest: Uint8Array, pub: Uint8Array): { rs: Uint8Array; recovery: number } { + if (!sig || sig.scheme !== "ecdsa-secp256k1" || sig.bytes.length !== 64) throw new ClipError("The signature didn't match. Nothing was sent.", "bitcoincash/bad-signature"); + let s = secp256k1.Signature.fromBytes(sig.bytes, "compact"); + if (s.hasHighS()) s = new secp256k1.Signature(s.r, secp256k1.Point.CURVE().n - s.s); + const rs = s.toBytes("compact"); + if (!secp256k1.verify(rs, digest, pub, { prehash: false })) throw new ClipError("The signature didn't match. Nothing was sent.", "bitcoincash/bad-signature"); + for (const rec of [0, 1]) { + try { + if (bytesEqual(s.addRecoveryBit(rec).recoverPublicKey(digest).toBytes(true), pub)) return { rs, recovery: rec }; + } catch { + /* next */ + } + } + throw new ClipError("The signature didn't match. Nothing was sent.", "bitcoincash/bad-signature"); + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const op = normalize(req, ctx); + const pub = publicKeyOf(ctx); + if (op.kind === "message") { + const { rs, recovery } = checkSignature(signatures[0], messageHash(op.text), pub); + return compactSignatureBase64(rs, recovery); + } + if (op.kind === "unreadable") throw new ClipError("This transaction can't be read.", "bitcoincash/bad-transaction", op.reason); + const idx = signingInputs(op, ctx); + if (signatures.length !== idx.length) throw new ClipError("Some signatures are missing. Nothing was sent.", "bitcoincash/bad-signature"); + const script = ownScript(ctx); + const inputs = op.tx.inputs.map((i) => ({ ...i })); + idx.forEach((i, k) => { + const { rs } = checkSignature(signatures[k], sighash(op.tx, i, op.sources[i]!, script), pub); + inputs[i] = { ...inputs[i]!, unlocking: p2pkhUnlocking(rs, pub) }; + }); + const signed: BchTx = { ...op.tx, inputs }; + const raw = encodeTx(signed); + const txid = hex(txidOf(raw)); + if (!op.broadcast) return { signedTransaction: hex(raw), signedTransactionHash: txid }; + const sent = await electrum(ctx, (call) => call("blockchain.transaction.broadcast", [hex(raw)])).catch((e: unknown) => { + throw plainBchError(e); + }); + if (typeof sent === "string" && /^[0-9a-f]{64}$/i.test(sent) && sent.toLowerCase() !== txid) { + throw new ClipError("Something went wrong. Please try again.", "bitcoincash/txid-mismatch"); + } + return { signedTransaction: hex(raw), signedTransactionHash: txid }; + } + + /* ---------------------------------------------------------------- balances and sends */ + + async function getBalances(ctx: ChainContext): Promise { + const sh = scripthash(ownScript(ctx)); + return electrum(ctx, async (call) => { + const b = await call("blockchain.scripthash.get_balance", [sh]); + const out: TokenBalance[] = [{ asset: bchAsset(ctx.network.id), amount: (BigInt(b.confirmed ?? 0) + BigInt(b.unconfirmed ?? 0)).toString() }]; + // CashTokens (fungible part): listunspent already carries token_data, so it's one more call. + const utxos = await call("blockchain.scripthash.listunspent", [sh, "include_tokens"]).catch(() => [] as ElectrumUtxo[]); + const ft = new Map(); + for (const u of utxos) { + const t = u.token_data; + if (!t || !/^[0-9a-f]{64}$/.test(t.category) || !t.amount || !/^\d+$/.test(t.amount)) continue; + ft.set(t.category, (ft.get(t.category) ?? 0n) + BigInt(t.amount)); + } + for (const [category, amount] of ft) if (amount > 0n) out.push({ asset: cashTokenAsset(ctx.network.id, category), amount: amount.toString() }); + return out; + }); + } + + async function bchBalance(ctx: ChainContext) { + const b = await electrum(ctx, (call) => call("blockchain.scripthash.get_balance", [scripthash(ownScript(ctx))])); + return { confirmed: String(b.confirmed ?? 0), unconfirmed: String(b.unconfirmed ?? 0) }; + } + + function recipientScript(to: string, ctx: ChainContext): Uint8Array { + const prefix = prefixOf(ctx.network.id); + const a = decodeCashAddress(to, prefix); + if (!a) throw new ClipError(msg("bg.bch.notCashAddr"), "bitcoincash/bad-address"); + if (a.prefix !== prefix) { + throw new ClipError(msg(prefix === "bitcoincash" ? "bg.bch.testAddressOnMainnet" : "bg.bch.mainAddressOnTestnet"), "bitcoincash/network-mismatch"); + } + const script = lockingBytecodeOf(a); + if (bytesEqual(script, ownScript(ctx))) throw new ClipError("That's your own address.", "bitcoincash/self-transfer"); + return script; + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + if (p.asset.address) throw new ClipError(msg("bg.bch.tokenSendUnavailable"), "bitcoincash/unsupported-token"); + const to = recipientScript(p.to, ctx); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "bitcoincash/bad-amount"); + const amount = BigInt(p.amount); + if (amount < DUST) throw new ClipError(msg("bg.bch.belowDust", { amount: fmt(DUST) }), "bitcoincash/too-small"); + const mine = ownScript(ctx); + const { utxos, rate } = await electrum(ctx, async (call) => ({ + utxos: await call("blockchain.scripthash.listunspent", [scripthash(mine), "include_tokens"]), + rate: await feeRate(call), + })); + // Never spend a coin that carries CashTokens in a plain BCH send: the tokens would be burned. + const coins = utxos + .filter((u) => !u.token_data && /^[0-9a-f]{64}$/i.test(u.tx_hash) && Number.isSafeInteger(u.value) && u.value > 0) + .sort((a, b) => b.value - a.value || (a.tx_hash < b.tx_hash ? -1 : 1)); + const picked: ElectrumUtxo[] = []; + let total = 0n; + let fee = 0n; + let change = 0n; + for (const c of coins) { + picked.push(c); + total += BigInt(c.value); + const withChange = BigInt(estimateSize(picked.length, [{ locking: to }, { locking: mine }])) * rate; + if (total >= amount + withChange) { + fee = withChange; + change = total - amount - fee; + if (change < DUST) { + fee = total - amount; // too small to keep: it goes to the fee + change = 0n; + } + break; + } + } + if (!picked.length || total < amount + fee || fee === 0n) { + throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: "BCH" }), "bitcoincash/insufficient-funds"); + } + const tx: BchTx = { + version: 2, + locktime: 0, + inputs: picked.map((c) => ({ txid: fromHex(c.tx_hash), vout: c.tx_pos, unlocking: new Uint8Array(), sequence: 0xffffffff })), + outputs: [{ value: amount, locking: to }, ...(change > 0n ? [{ value: change, locking: mine }] : [])], + }; + const sources = picked.map((c) => sourceOutputJson(fromHex(c.tx_hash), c.tx_pos, 0xffffffff, mine, BigInt(c.value))); + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "bitcoincash", + networkId: ctx.network.id, + method: BCH_METHODS.signTransaction, + params: { transaction: hex(encodeTx(tx)), sourceOutputs: stringifyExtended(sources), broadcast: true }, + }; + } + + function prefixesFor(value: string): CashPrefix[] { + const v = value.trim(); + if (v.includes(":")) { + const a = decodeCashAddress(v); + return a ? [a.prefix] : []; + } + return PREFIXES.filter((p) => decodeCashAddress(v, p)); + } + + return { + family: "bitcoincash", + curve: "secp256k1", + /** Electron Cash, Paytaca, Cashonize: BIP-44 coin 145, the same key on every network. */ + derivationPath: (index: number) => `m/44'/145'/0'/0/${index}`, + addressFromPublicKey(publicKey: Uint8Array, network: Network): string { + const key = publicKey.length === 33 ? publicKey : secp256k1.Point.fromBytes(publicKey).toBytes(true); + return p2pkhAddress(key, prefixOf(network.id)); + }, + /** CashAddr (P2PKH, P2SH, token-aware), with or without its prefix, checksum verified. Legacy base58 isn't accepted. */ + isAddress: (value: string) => prefixesFor(value).length > 0, + /** bitcoincash: → mainnet; bchtest: → chipnet and testnet4 (the same spelling). */ + networksForAddress: (value: string, candidates: Network[]) => { + const ps = prefixesFor(value); + return candidates.filter((c) => c.family === "bitcoincash" && netOf(c.id) && ps.includes(BCH_NETS[netOf(c.id)!].prefix)); + }, + getBalances, + getNfts: async (): Promise => [], + decode, + prepare, + finalize, + buildTransfer, + /** "bitcoincash:q…" on mainnet, "bchtest:q…" on chipnet / testnet4: same key and coins' script. */ + receiveAddress: async (ctx: ChainContext) => addressOn(ctx), + normalize, + bchBalance, + }; +} + +function warn(m: Msg): { message: string; msg: Msg } { + return { message: m.fallback, msg: m }; +} + +/** Node rejection reasons (bchn `RejectCode` texts relayed by Fulcrum) in plain words. */ +export function plainBchError(e: unknown): ClipError { + if (e instanceof ClipError) return e; + const text = e instanceof ElectrumError ? e.message : ""; + if (/insufficient fee|min relay fee|mempool min fee/i.test(text)) return new ClipError(msg("bg.bch.feeTooLow"), "bitcoincash/fee-too-low", e); + if (/missing inputs|missingorspent|txn-mempool-conflict|bad-txns-inputs-missingorspent/i.test(text)) return new ClipError(msg("bg.bch.coinsSpent"), "bitcoincash/inputs-spent", e); + if (/dust/i.test(text)) return new ClipError(msg("bg.bch.belowDust", { amount: fmt(DUST) }), "bitcoincash/too-small", e); + return new ClipError("The network rejected this. Nothing was sent.", "bitcoincash/rejected", e); +} + +export { decodeTx }; diff --git a/packages/chains-bitcoincash/src/networks.ts b/packages/chains-bitcoincash/src/networks.ts new file mode 100644 index 00000000..b3cc2a19 --- /dev/null +++ b/packages/chains-bitcoincash/src/networks.ts @@ -0,0 +1,89 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; +import type { CashPrefix } from "./cashaddr.js"; + +/** + * Bitcoin Cash networks. + * + * Ids are CAIP-2 `bip122:` + the first 32 hex characters of a block hash (BIP-122). BCH shares Bitcoin's genesis, so + * the ChainAgnostic bip122 namespace (bip122/caip2.md, test cases) names BCH mainnet by its first own block, 478559: + * `bip122:000000000000000000651ef99cb9fcbe`. The same rule gives chipnet its first block after it split from + * testnet4 (height 115252, 00000000040ba964…; found by comparing both chains' headers on Fulcrum, 2026-10) and + * testnet4 its genesis (000000001dd410c4…). None collides with chains-bitcoin's ids. + * + * The BCH wallet community's WalletConnect spec (wc2-bch-bcr, Cashonize/Paytaca, + * https://github.com/mainnet-pat/wc2-bch-bcr#pairing) names chains "bch:bitcoincash" (mainnet) and "bch:bchtest" + * (any testnet), carried here as `wcChain`. + * + * Indexers: public Fulcrum servers (Electrum Cash protocol 1.4) over WebSocket, each checked with server.version, + * blockchain.scripthash.get_balance, blockchain.headers.get_tip and blockchain.relayfee (2026-10). They're tried in + * order; the next one is used when one can't be reached. + */ +export type BchNet = "mainnet" | "chipnet" | "testnet4"; + +export interface BchNetSpec { + caip2: NetworkId; + prefix: CashPrefix; + wcChain: "bch:bitcoincash" | "bch:bchtest"; + electrum: string[]; + explorer: string; +} + +export const BCH_NETS: Record = { + mainnet: { + caip2: "bip122:000000000000000000651ef99cb9fcbe", + prefix: "bitcoincash", + wcChain: "bch:bitcoincash", + electrum: ["wss://bch.imaginary.cash:50004", "wss://electrum.imaginary.cash:50004", "wss://bch.loping.net:50004", "wss://bch.soul-dev.com:50004"], + explorer: "https://bch.loping.net", + }, + chipnet: { + caip2: "bip122:00000000040ba9641ba98a37b2e5ceea", + prefix: "bchtest", + wcChain: "bch:bchtest", + electrum: ["wss://chipnet.imaginary.cash:50004", "wss://chipnet.bch.ninja:50004", "wss://chipnet.c3-soft.com:64004", "wss://cbch.loping.net:62104"], + explorer: "https://cbch.loping.net", + }, + testnet4: { + caip2: "bip122:000000001dd410c49a788668ce267517", + prefix: "bchtest", + wcChain: "bch:bchtest", + electrum: ["wss://testnet4.imaginary.cash:50004", "wss://tbch4.loping.net:62004"], + explorer: "https://tbch4.loping.net", + }, +}; + +export function bchAsset(networkId: NetworkId): AssetRef { + return { key: "bch", symbol: "BCH", name: "Bitcoin Cash", decimals: 8, networkId }; +} + +const NAMES: Record = { mainnet: "Bitcoin Cash", chipnet: "Bitcoin Cash Chipnet", testnet4: "Bitcoin Cash Testnet4" }; + +function network(net: BchNet): Network { + const c = BCH_NETS[net]; + return { id: c.caip2, family: "bitcoincash", name: NAMES[net], nativeAsset: bchAsset(c.caip2), testnet: net !== "mainnet", rpcUrls: [...c.electrum], explorerUrl: c.explorer }; +} + +export const BCH_MAINNET = network("mainnet"); +export const BCH_CHIPNET = network("chipnet"); +export const BCH_TESTNET4 = network("testnet4"); +/** Chipnet first: it's where BCH developers test upcoming upgrades, and the wallet's default test network. */ +export const BCH_NETWORKS: Network[] = [BCH_CHIPNET, BCH_TESTNET4, BCH_MAINNET]; + +export function netOf(networkId: NetworkId): BchNet | null { + for (const [n, c] of Object.entries(BCH_NETS) as [BchNet, BchNetSpec][]) if (c.caip2 === networkId) return n; + return null; +} + +export function specFor(networkId: NetworkId): BchNetSpec | null { + const n = netOf(networkId); + return n ? BCH_NETS[n] : null; +} + +export function explorerTxUrl(networkId: NetworkId, txid: string): string { + return `${(specFor(networkId) ?? BCH_NETS.chipnet).explorer}/tx/${txid}`; +} + +/** CashToken AssetRef (fungible part). Category ids are shown as-is; names come from nowhere trusted, so none. */ +export function cashTokenAsset(networkId: NetworkId, category: string): AssetRef { + return { key: `cashtoken:${category}`, symbol: `CT-${category.slice(0, 6)}`, name: `CashToken ${category.slice(0, 8)}…`, decimals: 0, networkId, address: category }; +} diff --git a/packages/chains-bitcoincash/src/tx.ts b/packages/chains-bitcoincash/src/tx.ts new file mode 100644 index 00000000..14af8f30 --- /dev/null +++ b/packages/chains-bitcoincash/src/tx.ts @@ -0,0 +1,198 @@ +import { Reader, compactSize, concat, reversed, sha256d, uintLE } from "./util.js"; + +/** + * Bitcoin Cash transactions: the Bitcoin wire format with CashTokens (CHIP-2022-02) token prefixes in outputs, and the + * BCH signing serialization (BIP-143 layout with SIGHASH_FORKID, replay-protected: fork id 0, plus the spent output's + * token prefix), hand-written and cross-checked against @bitauth/libauth (encodeTransaction, + * generateSigningSerializationBCH) in the tests. https://reference.cash/protocol/blockchain/transaction/transaction-signing + */ + +export const SIGHASH_ALL_FORKID = 0x41; +export const DUST = 546n; + +export interface TokenData { + /** Category id in display (txid) order. */ + category: Uint8Array; + amount: bigint; + nft?: { capability: "none" | "mutable" | "minting"; commitment: Uint8Array }; +} + +export interface TxInput { + /** Previous transaction id in display order (as explorers and Electrum show it). */ + txid: Uint8Array; + vout: number; + unlocking: Uint8Array; + sequence: number; +} + +export interface TxOutput { + value: bigint; + locking: Uint8Array; + token?: TokenData; +} + +export interface BchTx { + version: number; + inputs: TxInput[]; + outputs: TxOutput[]; + locktime: number; +} + +const PREFIX_TOKEN = 0xef; +const HAS_AMOUNT = 0x10; +const HAS_NFT = 0x20; +const HAS_COMMITMENT = 0x40; +const RESERVED = 0x80; +const CAPABILITY = ["none", "mutable", "minting"] as const; + +export function encodeTokenPrefix(t: TokenData | undefined): Uint8Array { + if (!t || (!t.nft && t.amount < 1n)) return new Uint8Array(); + const hasCommitment = !!t.nft && t.nft.commitment.length > 0; + const bitfield = (t.nft ? HAS_NFT | CAPABILITY.indexOf(t.nft.capability) : 0) | (hasCommitment ? HAS_COMMITMENT : 0) | (t.amount > 0n ? HAS_AMOUNT : 0); + return concat( + new Uint8Array([PREFIX_TOKEN]), + reversed(t.category), + new Uint8Array([bitfield]), + ...(hasCommitment ? [compactSize(t.nft!.commitment.length), t.nft!.commitment] : []), + ...(t.amount > 0n ? [compactSize(t.amount)] : []), + ); +} + +/** Splits an output's locking bytecode field into token data and the locking bytecode. */ +export function readOutputField(field: Uint8Array): { locking: Uint8Array; token?: TokenData } { + if (field[0] !== PREFIX_TOKEN) return { locking: field }; + const r = new Reader(field); + r.u8(); + const category = reversed(r.read(32)); + const bits = r.u8(); + if (bits & RESERVED) throw new Error("bad token prefix"); + const hasNft = (bits & HAS_NFT) !== 0; + const capability = bits & 0x0f; + if (capability > 2 || (!hasNft && capability) || (!hasNft && bits & HAS_COMMITMENT)) throw new Error("bad token prefix"); + let commitment = new Uint8Array(); + if (bits & HAS_COMMITMENT) { + const n = Number(r.compact()); + if (n < 1 || n > 128) throw new Error("bad commitment length"); + commitment = r.read(n).slice(); + } + const amount = bits & HAS_AMOUNT ? r.compact() : 0n; + if (bits & HAS_AMOUNT && (amount < 1n || amount > 0x7fffffffffffffffn)) throw new Error("bad token amount"); + if (!hasNft && amount === 0n) throw new Error("empty token prefix"); + const token: TokenData = { category, amount, ...(hasNft ? { nft: { capability: CAPABILITY[capability]!, commitment } } : {}) }; + return { locking: field.subarray(r.at).slice(), token }; +} + +export function encodeOutput(o: TxOutput): Uint8Array { + const field = concat(encodeTokenPrefix(o.token), o.locking); + return concat(uintLE(o.value, 8), compactSize(field.length), field); +} + +export function encodeTx(tx: BchTx): Uint8Array { + return concat( + uintLE(tx.version, 4), + compactSize(tx.inputs.length), + ...tx.inputs.map((i) => concat(reversed(i.txid), uintLE(i.vout, 4), compactSize(i.unlocking.length), i.unlocking, uintLE(i.sequence, 4))), + compactSize(tx.outputs.length), + ...tx.outputs.map(encodeOutput), + uintLE(tx.locktime, 4), + ); +} + +export function decodeTx(bytes: Uint8Array): BchTx { + const r = new Reader(bytes); + const version = r.u32(); + const nIn = Number(r.compact()); + if (nIn < 1 || nIn > 10_000) throw new Error("bad input count"); + const inputs: TxInput[] = []; + for (let i = 0; i < nIn; i++) { + const txid = reversed(r.read(32)); + const vout = r.u32(); + const unlocking = r.read(Number(r.compact())).slice(); + inputs.push({ txid, vout, unlocking, sequence: r.u32() }); + } + const nOut = Number(r.compact()); + if (nOut < 1 || nOut > 10_000) throw new Error("bad output count"); + const outputs: TxOutput[] = []; + for (let i = 0; i < nOut; i++) { + const value = r.u64(); + outputs.push({ value, ...readOutputField(r.read(Number(r.compact())).slice()) }); + } + const locktime = r.u32(); + if (!r.done) throw new Error("trailing bytes"); + return { version, inputs, outputs, locktime }; +} + +/** Transaction id (display order). */ +export function txidOf(tx: BchTx | Uint8Array): Uint8Array { + return reversed(sha256d(tx instanceof Uint8Array ? tx : encodeTx(tx))); +} + +/** + * The 32-byte digest an input's key signs with SIGHASH_ALL | SIGHASH_FORKID (0x41, fork id 0): double SHA-256 of + * version ‖ hashPrevouts ‖ hashSequence ‖ outpoint ‖ spent token prefix ‖ scriptCode ‖ value ‖ sequence ‖ + * hashOutputs ‖ locktime ‖ 0x41000000. + */ +export function sighash(tx: BchTx, index: number, spent: { value: bigint; token?: TokenData }, scriptCode: Uint8Array): Uint8Array { + const input = tx.inputs[index]; + if (!input) throw new Error("no such input"); + const prevouts = concat(...tx.inputs.map((i) => concat(reversed(i.txid), uintLE(i.vout, 4)))); + const sequences = concat(...tx.inputs.map((i) => uintLE(i.sequence, 4))); + const outputs = concat(...tx.outputs.map(encodeOutput)); + const preimage = concat( + uintLE(tx.version, 4), + sha256d(prevouts), + sha256d(sequences), + reversed(input.txid), + uintLE(input.vout, 4), + encodeTokenPrefix(spent.token), + compactSize(scriptCode.length), + scriptCode, + uintLE(spent.value, 8), + uintLE(input.sequence, 4), + sha256d(outputs), + uintLE(tx.locktime, 4), + uintLE(SIGHASH_ALL_FORKID, 4), + ); + return sha256d(preimage); +} + +/* ------------------------------------------------------------------ signatures */ + +function derInt(b: Uint8Array): Uint8Array { + let i = 0; + while (i < b.length - 1 && b[i] === 0 && !(b[i + 1]! & 0x80)) i++; + const v = b.subarray(i); + return v[0]! & 0x80 ? concat(new Uint8Array([0]), v) : v; +} + +/** Strict DER of a 64-byte r ‖ s (low-S already applied by the caller). */ +export function derSignature(rs: Uint8Array): Uint8Array { + if (rs.length !== 64) throw new Error("bad signature length"); + const r = derInt(rs.subarray(0, 32)); + const s = derInt(rs.subarray(32)); + return concat(new Uint8Array([0x30, r.length + s.length + 4, 0x02, r.length]), r, new Uint8Array([0x02, s.length]), s); +} + +const push = (data: Uint8Array) => { + if (data.length > 75) throw new Error("push too large"); + return concat(new Uint8Array([data.length]), data); +}; + +/** + * P2PKH unlocking bytecode: . BCH reads a 65-byte signature (with its sighash + * byte) as Schnorr, so a DER signature of exactly 64 bytes (vanishingly rare) is refused instead of misread. + */ +export function p2pkhUnlocking(rs: Uint8Array, compressedKey: Uint8Array): Uint8Array { + const sig = concat(derSignature(rs), new Uint8Array([SIGHASH_ALL_FORKID])); + if (sig.length === 65) throw new Error("ambiguous 65-byte ECDSA signature"); + return concat(push(sig), push(compressedKey)); +} + +/** Byte size of a P2PKH-only transaction (signatures at their usual 72 bytes + sighash byte). */ +export function estimateSize(inputs: number, outputs: { locking: Uint8Array; token?: TokenData }[]): number { + const outBytes = outputs.reduce((n, o) => { + const field = encodeTokenPrefix(o.token).length + o.locking.length; + return n + 8 + compactSize(field).length + field; + }, 0); + return 4 + compactSize(inputs).length + inputs * 148 + compactSize(outputs.length).length + outBytes + 4; +} diff --git a/packages/chains-bitcoincash/src/util.ts b/packages/chains-bitcoincash/src/util.ts new file mode 100644 index 00000000..8dfc161c --- /dev/null +++ b/packages/chains-bitcoincash/src/util.ts @@ -0,0 +1,152 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { ripemd160 } from "@noble/hashes/legacy.js"; + +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.trim().replace(/^0x/i, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export const isHex = (s: unknown): s is string => typeof s === "string" && /^(0x)?([0-9a-f]{2})*$/i.test(s.trim()) && s.trim().replace(/^0x/i, "").length > 0; + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s); + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +export const hash160 = (b: Uint8Array): Uint8Array => ripemd160(sha256(b)); +export const sha256d = (b: Uint8Array): Uint8Array => sha256(sha256(b)); + +/** Little-endian unsigned integer of `size` bytes. */ +export function uintLE(value: bigint | number, size: number): Uint8Array { + let v = BigInt(value); + if (v < 0n || v >= 1n << BigInt(size * 8)) throw new Error("integer out of range"); + const out = new Uint8Array(size); + for (let i = 0; i < size; i++) { + out[i] = Number(v & 0xffn); + v >>= 8n; + } + return out; +} + +/** Bitcoin CompactSize. */ +export function compactSize(n: number | bigint): Uint8Array { + const v = BigInt(n); + if (v < 0xfdn) return new Uint8Array([Number(v)]); + if (v <= 0xffffn) return concat(new Uint8Array([0xfd]), uintLE(v, 2)); + if (v <= 0xffffffffn) return concat(new Uint8Array([0xfe]), uintLE(v, 4)); + return concat(new Uint8Array([0xff]), uintLE(v, 8)); +} + +/** Sequential little-endian reader; reading past the end throws. */ +export class Reader { + at = 0; + constructor(readonly bytes: Uint8Array) {} + get done(): boolean { + return this.at >= this.bytes.length; + } + read(n: number): Uint8Array { + if (n < 0 || this.at + n > this.bytes.length) throw new Error("unexpected end of data"); + const out = this.bytes.subarray(this.at, this.at + n); + this.at += n; + return out; + } + u8(): number { + return this.read(1)[0]!; + } + uintLE(n: number): bigint { + const b = this.read(n); + let v = 0n; + for (let i = n - 1; i >= 0; i--) v = (v << 8n) | BigInt(b[i]!); + return v; + } + u32(): number { + return Number(this.uintLE(4)); + } + u64(): bigint { + return this.uintLE(8); + } + compact(): bigint { + const f = this.u8(); + if (f < 0xfd) return BigInt(f); + const v = this.uintLE(f === 0xfd ? 2 : f === 0xfe ? 4 : 8); + // minimal encoding only + if ((f === 0xfd && v < 0xfdn) || (f === 0xfe && v <= 0xffffn) || (f === 0xff && v <= 0xffffffffn)) throw new Error("non-minimal CompactSize"); + return v; + } +} + +export const reversed = (b: Uint8Array): Uint8Array => b.slice().reverse(); + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +/** "bitcoincash:qpm2…4uvf" → "qpm2qs…uvf"-style short form (no prefix). */ +export function short(address: string): string { + const a = address.includes(":") ? address.slice(address.indexOf(":") + 1) : address; + return a.length > 14 ? `${a.slice(0, 6)}…${a.slice(-4)}` : a; +} + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +export function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} + +/** Printable text, or null when the bytes aren't readable UTF-8. */ +export function textOf(bytes: Uint8Array): string | null { + if (!bytes.length) return ""; + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + return /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s) ? null : s; + } catch { + return null; + } +} + +/** A BigInt from a number, decimal string or bigint. */ +export function toBig(v: unknown): bigint | null { + if (typeof v === "bigint") return v; + if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v); + if (typeof v === "string" && /^\d+$/.test(v.trim())) return BigInt(v.trim()); + return null; +} + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/packages/chains-bitcoincash/src/wc.ts b/packages/chains-bitcoincash/src/wc.ts new file mode 100644 index 00000000..41cbc04b --- /dev/null +++ b/packages/chains-bitcoincash/src/wc.ts @@ -0,0 +1,130 @@ +import type { BchTx, TokenData, TxOutput } from "./tx.js"; +import { decodeTx } from "./tx.js"; +import { fromHex, hex, isHex } from "./util.js"; + +/** + * The BCH WalletConnect request format (wc2-bch-bcr, https://github.com/mainnet-pat/wc2-bch-bcr#signtransaction), used + * by Cashonize, Paytaca and the apps listed there: `bch_signTransaction { transaction, sourceOutputs, broadcast?, + * userPrompt? }` where `transaction` is raw hex, a libauth `Transaction`, or that object passed through libauth's + * `stringify` (Uint8Array → "", bigint → ""), and `sourceOutputs` are libauth + * Input & Output (& optional CashScript contract info) objects, stringified the same way. An input to be signed by + * the wallet has an empty `unlockingBytecode`. + */ + +const U8 = /^[0-9a-f]*)>$/u; +const BIG = /^[0-9]+)n>$/u; + +/** libauth `stringify`-compatible reviver (the spec's parseExtendedJson). */ +export function revive(value: unknown): unknown { + if (typeof value === "string") { + const b = BIG.exec(value); + if (b) return BigInt(b.groups!.n!); + const u = U8.exec(value); + if (u) return fromHex(u.groups!.hex!.length ? u.groups!.hex! : ""); + return value; + } + if (Array.isArray(value)) return value.map(revive); + if (value && typeof value === "object" && !(value instanceof Uint8Array)) { + const out: Record = {}; + for (const [k, v] of Object.entries(value)) out[k] = revive(v); + return out; + } + return value; +} + +/** libauth `stringify` (what the spec's apps send; used for wallet-built requests too). */ +export function stringifyExtended(value: unknown): string { + return JSON.stringify(value, (_k, v) => (typeof v === "bigint" ? `` : v instanceof Uint8Array ? `` : v)); +} + +function parseMaybe(v: unknown): unknown { + if (typeof v === "string" && !isHex(v)) { + try { + return revive(JSON.parse(v)); + } catch { + throw new Error("bad JSON"); + } + } + return revive(v); +} + +const bytes = (v: unknown, what: string): Uint8Array => { + if (v instanceof Uint8Array) return v; + if (typeof v === "string" && (v === "" || isHex(v))) return v === "" ? new Uint8Array() : fromHex(v); + if (v && typeof v === "object" && Object.keys(v).every((k) => /^\d+$/.test(k))) return Uint8Array.from(Object.values(v as Record)); + throw new Error(`bad ${what}`); +}; + +const big = (v: unknown, what: string): bigint => { + if (typeof v === "bigint") return v; + if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v); + if (typeof v === "string" && /^\d+$/.test(v)) return BigInt(v); + throw new Error(`bad ${what}`); +}; + +const int = (v: unknown, what: string): number => { + const n = Number(big(v, what)); + if (n < 0 || n > 0xffffffff) throw new Error(`bad ${what}`); + return n; +}; + +function token(v: unknown): TokenData | undefined { + if (v === undefined || v === null) return undefined; + const t = v as { category?: unknown; amount?: unknown; nft?: { capability?: unknown; commitment?: unknown } }; + const category = bytes(t.category, "token category"); + if (category.length !== 32) throw new Error("bad token category"); + const out: TokenData = { category, amount: t.amount === undefined ? 0n : big(t.amount, "token amount") }; + if (t.nft) { + const cap = t.nft.capability; + if (cap !== "none" && cap !== "mutable" && cap !== "minting") throw new Error("bad nft capability"); + out.nft = { capability: cap, commitment: t.nft.commitment === undefined ? new Uint8Array() : bytes(t.nft.commitment, "nft commitment") }; + } + return out; +} + +function output(v: unknown): TxOutput { + const o = v as { lockingBytecode?: unknown; valueSatoshis?: unknown; token?: unknown }; + const t = token(o.token); + return { locking: bytes(o.lockingBytecode, "locking bytecode"), value: big(o.valueSatoshis, "value"), ...(t ? { token: t } : {}) }; +} + +export interface SourceOutput extends TxOutput { + /** Outpoint this source output is (display-order txid hex + index), to match it to an input. */ + outpoint: string; + /** CashScript contract name, if the app described one (data, shown as the app's claim only). */ + contractName?: string; +} + +/** The transaction from `transaction` (hex, libauth object, or stringified). */ +export function parseTransaction(v: unknown): BchTx { + if (typeof v === "string" && isHex(v)) return decodeTx(fromHex(v)); + const t = parseMaybe(v) as { version?: unknown; inputs?: unknown; outputs?: unknown; locktime?: unknown }; + if (!t || typeof t !== "object" || !Array.isArray(t.inputs) || !Array.isArray(t.outputs) || !t.inputs.length || !t.outputs.length) throw new Error("bad transaction"); + return { + version: int(t.version, "version"), + locktime: int(t.locktime, "locktime"), + inputs: t.inputs.map((x) => { + const i = x as { outpointTransactionHash?: unknown; outpointIndex?: unknown; unlockingBytecode?: unknown; sequenceNumber?: unknown }; + const txid = bytes(i.outpointTransactionHash, "outpoint"); + if (txid.length !== 32) throw new Error("bad outpoint"); + return { txid, vout: int(i.outpointIndex, "outpoint index"), unlocking: bytes(i.unlockingBytecode ?? "", "unlocking bytecode"), sequence: int(i.sequenceNumber, "sequence") }; + }), + outputs: t.outputs.map(output), + }; +} + +export function parseSourceOutputs(v: unknown): SourceOutput[] { + const list = parseMaybe(v); + if (!Array.isArray(list)) throw new Error("bad source outputs"); + return list.map((x) => { + const s = x as { outpointTransactionHash?: unknown; outpointIndex?: unknown; contract?: { artifact?: { contractName?: unknown } } }; + const txid = bytes(s.outpointTransactionHash, "outpoint"); + const name = s.contract?.artifact?.contractName; + return { ...output(x), outpoint: `${hex(txid)}:${int(s.outpointIndex, "outpoint index")}`, ...(typeof name === "string" ? { contractName: name.slice(0, 64) } : {}) }; + }); +} + +/** libauth-shaped source output (for stringifyExtended) of one of our coins. */ +export function sourceOutputJson(txid: Uint8Array, vout: number, sequence: number, locking: Uint8Array, value: bigint) { + return { outpointIndex: vout, outpointTransactionHash: txid, sequenceNumber: sequence, unlockingBytecode: new Uint8Array(), lockingBytecode: locking, valueSatoshis: value }; +} diff --git a/packages/chains-bitcoincash/test/bitcoincash.test.ts b/packages/chains-bitcoincash/test/bitcoincash.test.ts new file mode 100644 index 00000000..57cbdb05 --- /dev/null +++ b/packages/chains-bitcoincash/test/bitcoincash.test.ts @@ -0,0 +1,338 @@ +import { type DappRequest, WALLET_ORIGIN } from "@clip-wallet/core"; +import { + createVirtualMachineBCH, + decodeCashAddress as libDecode, + decodeTransaction as libDecodeTx, + encodeCashAddress as libEncode, + encodeTransaction as libEncodeTx, + generateSigningSerializationBCH, + hash160 as libHash160, + hash256, + stringify, +} from "@bitauth/libauth"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { describe, expect, it } from "vitest"; +import { + BCH_CHIPNET, + BCH_MAINNET, + BCH_METHODS, + BCH_TESTNET4, + type BchTx, + createBitcoinCashModule, + decodeCashAddress, + decodeTx, + encodeCashAddress, + encodeTx, + messageHash, + ownScript, + p2pkhScript, + scripthash, + sighash, + txidOf, +} from "../src/index.js"; +import { fromHex, hash160, hex } from "../src/util.js"; +import { type Handler, ctxFor, fakeElectrum, signer } from "./helpers.js"; +import { FIX } from "./signatures.js"; + +const PUB = fromHex(FIX.publicKey); +const MINE = p2pkhScript(hash160(PUB)); +const OTHER = p2pkhScript(hash160(fromHex(FIX.otherPublicKey))); +const SH = scripthash(MINE); +const CAT = "f0".repeat(16) + "0f".repeat(16); +const TX_A = "a1".repeat(32); +const TX_B = "b2".repeat(32); +const TX_C = "c3".repeat(32); + +const coins = [ + { tx_hash: TX_A, tx_pos: 0, height: 300000, value: 50_000 }, + { tx_hash: TX_B, tx_pos: 1, height: 0, value: 20_000 }, + { tx_hash: TX_C, tx_pos: 2, height: 300001, value: 1000, token_data: { category: CAT, amount: "500" } }, +]; + +function electrum(over: Partial unknown>> = {}, down: string[] = []) { + const handler: Handler = (method, params) => { + const f = over[method]; + if (f) return f(params); + switch (method) { + case "blockchain.scripthash.get_balance": + return { confirmed: 50_000, unconfirmed: 21_000 }; + case "blockchain.scripthash.listunspent": + return coins; + case "blockchain.relayfee": + return 0.00001; + case "blockchain.transaction.broadcast": + return hex(txidOf(fromHex(params[0] as string))); + default: + throw new Error(`unexpected ${method}`); + } + }; + return fakeElectrum(handler, down); +} + +const req = (method: string, params: unknown, extra: Partial = {}): DappRequest => ({ + id: `r-${Math.random().toString(36).slice(2)}`, + origin: "https://tapswap.example", + via: "walletconnect", + family: "bitcoincash", + networkId: BCH_CHIPNET.id, + method, + params, + ...extra, +}); + +async function run(mod: ReturnType, r: DappRequest, ctx = ctxFor()) { + const d = await mod.decode(r, ctx); + const payloads = await mod.prepare(r, ctx, "approval-1"); + const result = await mod.finalize(r, payloads.map((p) => signer.sign(p)), ctx); + return { d, payloads, result }; +} + +/** libauth's BCH 2023 VM: every input's unlocking script runs against its source output. */ +function vmVerify(raw: Uint8Array, sources: { locking: Uint8Array; value: bigint; token?: unknown }[]) { + const transaction = libDecodeTx(raw); + if (typeof transaction === "string") throw new Error(transaction); + const sourceOutputs = sources.map((s) => ({ lockingBytecode: s.locking, valueSatoshis: s.value, ...(s.token ? { token: s.token } : {}) })); + return createVirtualMachineBCH().verify({ transaction, sourceOutputs } as never); +} + +describe("CashAddr", () => { + const mod = createBitcoinCashModule(); + it("derives the P2PKH CashAddr of the abandon vector, as libauth encodes it", () => { + expect(mod.addressFromPublicKey(PUB, BCH_MAINNET)).toBe(FIX.mainnet); + expect(mod.addressFromPublicKey(PUB, BCH_CHIPNET)).toBe(FIX.testnet); + expect(mod.addressFromPublicKey(secp256k1.Point.fromHex(FIX.publicKey).toBytes(false), BCH_TESTNET4)).toBe(FIX.testnet); + expect(libEncode({ prefix: "bitcoincash", type: "p2pkh", payload: libHash160(PUB) }).address).toBe(FIX.mainnet); + expect(mod.derivationPath(2)).toBe("m/44'/145'/0'/0/2"); + }); + + it("decodes with or without prefix, checks the checksum and refuses legacy and mixed case", () => { + expect(mod.isAddress(FIX.mainnet)).toBe(true); + expect(mod.isAddress(FIX.mainnet.split(":")[1]!)).toBe(true); + expect(mod.isAddress(FIX.mainnet.toUpperCase())).toBe(true); + expect(mod.isAddress(FIX.mainnet.slice(0, -1) + "7")).toBe(false); + expect(mod.isAddress("bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahQ3q6")).toBe(false); + expect(mod.isAddress("1BpEi6DfDAUFd7GtittLSdBeYJvcoaVggu")).toBe(false); + expect(mod.networksForAddress(FIX.testnet, [BCH_CHIPNET, BCH_TESTNET4, BCH_MAINNET]).map((n) => n.name)).toEqual(["Bitcoin Cash Chipnet", "Bitcoin Cash Testnet4"]); + expect(mod.networksForAddress(FIX.mainnet.split(":")[1]!, [BCH_CHIPNET, BCH_MAINNET]).map((n) => n.id)).toEqual([BCH_MAINNET.id]); + }); + + it("token-aware and P2SH32 addresses agree with libauth", () => { + const h32 = fromHex("ab".repeat(32)); + const z = libEncode({ prefix: "bchtest", type: "p2pkhWithTokens", payload: hash160(PUB) }).address; + expect(encodeCashAddress("bchtest", 2, hash160(PUB))).toBe(z); + expect(decodeCashAddress(z)).toMatchObject({ kind: "p2pkh", tokenAware: true, prefix: "bchtest" }); + const p2sh32 = libEncode({ prefix: "bitcoincash", type: "p2sh", payload: h32 }).address; + expect(encodeCashAddress("bitcoincash", 1, h32)).toBe(p2sh32); + expect(hex(decodeCashAddress(p2sh32)!.hash)).toBe(hex(h32)); + const lib = libDecode(FIX.otherMainnet); + expect(typeof lib !== "string" && hex(lib.payload)).toBe(hex(hash160(fromHex(FIX.otherPublicKey)))); + }); + + it("uses bip122 fork-block ids that don't collide with Bitcoin's", () => { + expect(BCH_MAINNET.id).toBe("bip122:000000000000000000651ef99cb9fcbe"); + expect(BCH_CHIPNET.id).toBe("bip122:00000000040ba9641ba98a37b2e5ceea"); + expect(BCH_TESTNET4.id).toBe("bip122:000000001dd410c49a788668ce267517"); + expect([BCH_MAINNET.id, BCH_CHIPNET.id, BCH_TESTNET4.id]).not.toContain("bip122:000000000019d6689c085ae165831e93"); + }); +}); + +describe("transactions and sighash match libauth", () => { + const token = { category: fromHex(CAT), amount: 500n, nft: { capability: "mutable" as const, commitment: fromHex("c0ffee") } }; + const tx: BchTx = { + version: 2, + locktime: 123, + inputs: [ + { txid: fromHex(TX_A), vout: 0, unlocking: new Uint8Array(), sequence: 0xfffffffe }, + { txid: fromHex(TX_C), vout: 2, unlocking: new Uint8Array(), sequence: 0xffffffff }, + ], + outputs: [ + { value: 1000n, locking: OTHER, token }, + { value: 40_000n, locking: MINE }, + { value: 0n, locking: fromHex("6a0568656c6c6f") }, + ], + }; + const lib = { + version: 2, + locktime: 123, + inputs: tx.inputs.map((i) => ({ outpointTransactionHash: i.txid, outpointIndex: i.vout, unlockingBytecode: i.unlocking, sequenceNumber: i.sequence })), + outputs: [ + { valueSatoshis: 1000n, lockingBytecode: OTHER, token: { category: token.category, amount: 500n, nft: { capability: "mutable" as const, commitment: token.nft.commitment } } }, + { valueSatoshis: 40_000n, lockingBytecode: MINE }, + { valueSatoshis: 0n, lockingBytecode: fromHex("6a0568656c6c6f") }, + ], + }; + + it("encodes token outputs and round-trips", () => { + expect(hex(encodeTx(tx))).toBe(hex(libEncodeTx(lib))); + expect(hex(encodeTx(decodeTx(encodeTx(tx))))).toBe(hex(encodeTx(tx))); + }); + + it("signing serialization (SIGHASH_ALL|FORKID) including a spent token prefix", () => { + const sources = [ + { lockingBytecode: MINE, valueSatoshis: 50_000n }, + { lockingBytecode: MINE, valueSatoshis: 1000n, token: { category: fromHex(CAT), amount: 500n } }, + ]; + for (const i of [0, 1]) { + const ser = generateSigningSerializationBCH({ inputIndex: i, sourceOutputs: sources, transaction: lib } as never, { coveredBytecode: MINE, signingSerializationType: Uint8Array.of(0x41) }); + const spent = i === 0 ? { value: 50_000n } : { value: 1000n, token: { category: fromHex(CAT), amount: 500n } }; + expect(hex(sighash(tx, i, spent, MINE))).toBe(hex(hash256(ser))); + } + }); +}); + +describe("wallet sends", () => { + it("buildTransfer: largest coins first, never a token coin, change to the same address; VM-valid", async () => { + const e = electrum(); + const mod = createBitcoinCashModule({ factory: e.factory }); + const ctx = ctxFor(); + const r = await mod.buildTransfer({ asset: BCH_CHIPNET.nativeAsset, to: FIX.otherTestnet, amount: "30000" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, method: BCH_METHODS.signTransaction, family: "bitcoincash" }); + const { d, payloads, result } = await run(mod, r, ctx); + expect(d.title).toBe("Send 0.0003 BCH to qp8sfd…q4lg"); + expect(d.lines).toEqual([ + { label: "To", value: FIX.otherTestnet }, + { label: "Amount", value: "0.0003 BCH" }, + { label: "You get back", value: "0.00019774 BCH (qqyx49…zx8x, the same address)" }, + { label: "Network fee", value: "0.00000226 BCH" }, + ]); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "bch" }), delta: "-30226" }]); + expect(d.fee).toMatchObject({ amount: "226" }); + expect(payloads).toHaveLength(1); + const out = result as { signedTransaction: string; signedTransactionHash: string }; + const raw = fromHex(out.signedTransaction); + const signed = decodeTx(raw); + expect(signed.inputs.map((i) => hex(i.txid))).toEqual([TX_A]); + expect(signed.outputs.map((o) => [o.value, hex(o.locking)])).toEqual([ + [30_000n, hex(OTHER)], + [19_774n, hex(MINE)], + ]); + expect(vmVerify(raw, [{ locking: MINE, value: 50_000n }])).toBe(true); + const sent = e.calls.find((c) => c.method === "blockchain.transaction.broadcast"); + expect(sent?.params).toEqual([out.signedTransaction]); + expect(out.signedTransactionHash).toBe(hex(txidOf(raw))); + expect(e.calls.find((c) => c.method === "blockchain.scripthash.listunspent")?.params[0]).toBe(SH); + }); + + it("refuses a bad signature before broadcasting", async () => { + const e = electrum(); + const mod = createBitcoinCashModule({ factory: e.factory }); + const ctx = ctxFor(); + const r = await mod.buildTransfer({ asset: BCH_CHIPNET.nativeAsset, to: FIX.otherTestnet, amount: "30000" }, ctx); + const [p] = await mod.prepare(r, ctx, "a"); + const good = signer.sign(p!); + const bad = good.bytes.slice(); + bad[3] = bad[3]! ^ 1; + await expect(mod.finalize(r, [{ ...good, bytes: bad }], ctx)).rejects.toMatchObject({ code: "bitcoincash/bad-signature" }); + expect(e.calls.some((c) => c.method === "blockchain.transaction.broadcast")).toBe(false); + }); + + it("refuses dust, own address, other-network and legacy addresses, and token coins can't pay", async () => { + const mod = createBitcoinCashModule({ factory: electrum({ "blockchain.scripthash.listunspent": () => [coins[2]] }).factory }); + const ctx = ctxFor(); + await expect(mod.buildTransfer({ asset: BCH_CHIPNET.nativeAsset, to: FIX.otherTestnet, amount: "545" }, ctx)).rejects.toMatchObject({ code: "bitcoincash/too-small" }); + await expect(mod.buildTransfer({ asset: BCH_CHIPNET.nativeAsset, to: FIX.testnet, amount: "1000" }, ctx)).rejects.toMatchObject({ code: "bitcoincash/self-transfer" }); + await expect(mod.buildTransfer({ asset: BCH_CHIPNET.nativeAsset, to: FIX.otherMainnet, amount: "1000" }, ctx)).rejects.toMatchObject({ code: "bitcoincash/network-mismatch" }); + await expect(mod.buildTransfer({ asset: BCH_CHIPNET.nativeAsset, to: "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn", amount: "1000" }, ctx)).rejects.toMatchObject({ code: "bitcoincash/bad-address" }); + await expect(mod.buildTransfer({ asset: BCH_CHIPNET.nativeAsset, to: FIX.otherTestnet, amount: "600" }, ctx)).rejects.toMatchObject({ code: "bitcoincash/insufficient-funds" }); + }); + + it("balances: confirmed + unconfirmed BCH and fungible CashTokens; falls back to the next server", async () => { + const e = electrum({}, [BCH_CHIPNET.rpcUrls[0]!]); + const mod = createBitcoinCashModule({ factory: e.factory }); + const b = await mod.getBalances(ctxFor()); + expect(b.map((x) => [x.asset.key, x.amount])).toEqual([ + ["bch", "71000"], + [`cashtoken:${CAT}`, "500"], + ]); + expect(e.calls.every((c) => c.url === BCH_CHIPNET.rpcUrls[1])).toBe(true); + expect(await mod.receiveAddress(ctxFor())).toBe(FIX.testnet); + expect(await mod.receiveAddress(ctxFor(BCH_MAINNET))).toBe(FIX.mainnet); + }); + + it("all servers down: a plain-words error", async () => { + const mod = createBitcoinCashModule({ factory: electrum({}, BCH_CHIPNET.rpcUrls).factory }); + await expect(mod.getBalances(ctxFor())).rejects.toMatchObject({ code: "bitcoincash/offline" }); + }); +}); + +describe("dapp requests (wc2-bch-bcr)", () => { + const contractInput = { outpointTransactionHash: fromHex(TX_B), outpointIndex: 0, sequenceNumber: 0xfffffffe, unlockingBytecode: fromHex("0051") }; + const appTx = (unlockingOther = contractInput.unlockingBytecode) => ({ + version: 2, + locktime: 0, + inputs: [{ ...contractInput, unlockingBytecode: unlockingOther }, { outpointTransactionHash: fromHex(TX_A), outpointIndex: 0, sequenceNumber: 0xfffffffe, unlockingBytecode: new Uint8Array() }], + outputs: [ + { lockingBytecode: OTHER, valueSatoshis: 5000n, token: { category: fromHex(CAT), amount: 100n } }, + { lockingBytecode: MINE, valueSatoshis: 45_500n }, + ], + }); + const sources = (unlockingOther = contractInput.unlockingBytecode) => [ + { ...contractInput, unlockingBytecode: unlockingOther, lockingBytecode: fromHex("a914" + "11".repeat(20) + "87"), valueSatoshis: 5000n, token: { category: fromHex(CAT), amount: 100n }, contract: { artifact: { contractName: "MarketOrder" } } }, + { outpointTransactionHash: fromHex(TX_A), outpointIndex: 0, sequenceNumber: 0xfffffffe, unlockingBytecode: new Uint8Array(), lockingBytecode: MINE, valueSatoshis: 50_000n }, + ]; + + it("bch_signTransaction (libauth stringify, no broadcast): decodes and signs only our input", async () => { + const e = electrum(); + const mod = createBitcoinCashModule({ factory: e.factory }); + const r = req(BCH_METHODS.signTransaction, { transaction: stringify(appTx()), sourceOutputs: stringify(sources()), broadcast: false, userPrompt: "Buy token" }); + const { d, payloads, result } = await run(mod, r); + // tokens go to the token-aware ("z") spelling of the same P2PKH + const z = encodeCashAddress("bchtest", 2, hash160(fromHex(FIX.otherPublicKey))); + expect(d.title).toBe(`Send to ${z.slice(8, 14)}…${z.slice(-4)}`); + expect(d.lines).toContainEqual({ label: "Amount", value: `0.00005 BCH + 100 × CT-${CAT.slice(0, 6)}` }); + expect(d.lines).toContainEqual({ label: "App contract", value: "MarketOrder" }); + expect(d.lines).toContainEqual({ label: "App says (unverified)", value: "Buy token" }); + expect(d.lines).toContainEqual({ label: "Sent by", value: "tapswap.example (it gets the signed transaction)" }); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "bch" }), delta: "-4500" }]); + expect(d.fee).toMatchObject({ amount: "4500" }); + expect(payloads).toHaveLength(1); + expect(e.calls.some((c) => c.method === "blockchain.transaction.broadcast")).toBe(false); + const signed = decodeTx(fromHex((result as { signedTransaction: string }).signedTransaction)); + expect(hex(signed.inputs[0]!.unlocking)).toBe("0051"); + expect(signed.inputs[1]!.unlocking.length).toBeGreaterThan(100); + }); + + it("refuses CashScript placeholders, someone else's inputs, and other networks; garbage is blind", async () => { + const mod = createBitcoinCashModule({ factory: electrum().factory }); + const ctx = ctxFor(); + const placeholder = new Uint8Array([0x41, ...new Uint8Array(65), 0x21, ...new Uint8Array(33)]); + await expect(mod.decode(req(BCH_METHODS.signTransaction, { transaction: stringify(appTx(placeholder)), sourceOutputs: stringify(sources(placeholder)) }), ctx)).rejects.toMatchObject({ + code: "bitcoincash/unsupported-contract", + }); + const notMine = sources().map((s) => ({ ...s, lockingBytecode: OTHER })); + await expect(mod.decode(req(BCH_METHODS.signTransaction, { transaction: stringify(appTx()), sourceOutputs: stringify(notMine) }), ctx)).rejects.toMatchObject({ code: "bitcoincash/not-a-signer" }); + await expect(mod.decode(req(BCH_METHODS.signMessage, { message: "hi" }, { networkId: BCH_MAINNET.id }), ctx)).rejects.toMatchObject({ code: "bitcoincash/network-mismatch" }); + const garbage = req(BCH_METHODS.signTransaction, { transaction: "0200ff", sourceOutputs: "[]" }); + const d = await mod.decode(garbage, ctx); + expect(d.blind).toBe(true); + await expect(mod.prepare(garbage, ctx, "a")).rejects.toMatchObject({ code: "bitcoincash/bad-transaction" }); + }); + + it("warns when our own CashTokens leave the wallet", async () => { + const mod = createBitcoinCashModule({ factory: electrum().factory }); + const tx = { + version: 2, + locktime: 0, + inputs: [{ outpointTransactionHash: fromHex(TX_C), outpointIndex: 2, sequenceNumber: 0xffffffff, unlockingBytecode: new Uint8Array() }], + outputs: [{ lockingBytecode: OTHER, valueSatoshis: 800n, token: { category: fromHex(CAT), amount: 500n } }], + }; + const src = [{ ...tx.inputs[0]!, lockingBytecode: MINE, valueSatoshis: 1000n, token: { category: fromHex(CAT), amount: 500n } }]; + const d = await mod.decode(req(BCH_METHODS.signTransaction, { transaction: stringify(tx), sourceOutputs: stringify(src) }), ctxFor()); + expect(d.warnings[0]).toMatchObject({ code: "inscribed-utxo", level: "caution" }); + expect(d.warnings[0]!.msg?.id).toBe("bg.bch.spendsTokens"); + expect(d.balanceChanges).toContainEqual({ asset: expect.objectContaining({ key: `cashtoken:${CAT}` }), delta: "-500" }); + }); + + it("bch_signMessage: Electron Cash format, base64, recovers to the account key", async () => { + const mod = createBitcoinCashModule({ factory: electrum().factory }); + const r = req(BCH_METHODS.signMessage, { message: "Clip Wallet dapp matrix: sign-in check (testnet)" }); + const { d, result } = await run(mod, r); + expect(d.title).toBe("Sign a message for tapswap.example"); + const sig = Uint8Array.from(atob(result as string), (c) => c.charCodeAt(0)); + expect(sig[0] === 31 || sig[0] === 32).toBe(true); + const s = secp256k1.Signature.fromBytes(sig.subarray(1), "compact").addRecoveryBit(sig[0]! - 31); + expect(hex(s.recoverPublicKey(messageHash("Clip Wallet dapp matrix: sign-in check (testnet)")).toBytes(true))).toBe(FIX.publicKey); + expect(hex(messageHash("hello"))).toBe(hex(hash256(new Uint8Array([24, ...new TextEncoder().encode("Bitcoin Signed Message:\n"), 5, ...new TextEncoder().encode("hello")])))); + expect(hex(ownScript(ctxFor()))).toBe(hex(MINE)); + }); +}); diff --git a/packages/chains-bitcoincash/test/helpers.ts b/packages/chains-bitcoincash/test/helpers.ts new file mode 100644 index 00000000..d5a3e15c --- /dev/null +++ b/packages/chains-bitcoincash/test/helpers.ts @@ -0,0 +1,65 @@ +import type { Account, ChainContext, SignablePayload, Signature } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { BCH_CHIPNET, type SocketFactory, type SocketLike } from "../src/index.js"; +import { fromHex, hex } from "../src/util.js"; +import { FIX } from "./signatures.js"; + +/** Vault stand-in: answers with a precomputed signature that verifies for the payload (verification only). */ +export function fixtureSigner(publicKeyHex: string, sigs: Record) { + const pub = fromHex(publicKeyHex); + return { + sign(p: SignablePayload): Signature { + for (const v of Object.values(sigs)) { + const [rs, rec] = v.split(":"); + const bytes = fromHex(rs!); + if (secp256k1.verify(bytes, p.bytes, pub, { prehash: false })) return { scheme: "ecdsa-secp256k1", bytes, recovery: Number(rec), publicKey: publicKeyHex }; + } + throw new Error(`no fixture signature for digest ${hex(p.bytes)}`); + }, + }; +} + +export const signer = fixtureSigner(FIX.publicKey, FIX.sigs); + +export function makeAccount(): Account { + return { id: "bitcoincash:0", family: "bitcoincash", index: 0, curve: "secp256k1", derivationPath: "m/44'/145'/0'/0/0", publicKey: FIX.publicKey, address: FIX.mainnet }; +} + +export type Handler = (method: string, params: unknown[], url: string) => unknown; + +/** + * In-memory Electrum server(s): `handler` answers each JSON-RPC call (throw { message } for an RPC error). + * URLs listed in `down` refuse to connect, to exercise the fallback. + */ +export function fakeElectrum(handler: Handler, down: string[] = []) { + const calls: { url: string; method: string; params: unknown[] }[] = []; + const factory: SocketFactory = (url) => { + const s: SocketLike = { + onopen: null, + onmessage: null, + onerror: null, + onclose: null, + send(data: string) { + const m = JSON.parse(data) as { id: number; method: string; params: unknown[] }; + calls.push({ url, method: m.method, params: m.params }); + queueMicrotask(() => { + let reply: unknown; + try { + reply = { jsonrpc: "2.0", id: m.id, result: m.method === "server.version" ? ["Fulcrum 2.1.3", "1.4"] : handler(m.method, m.params, url) }; + } catch (e) { + reply = { jsonrpc: "2.0", id: m.id, error: { code: 1, message: (e as Error).message } }; + } + s.onmessage?.({ data: JSON.stringify(reply) }); + }); + }, + close() {}, + }; + queueMicrotask(() => (down.includes(url) ? s.onerror?.({}) : s.onopen?.({}))); + return s; + }; + return { factory, calls }; +} + +export function ctxFor(network = BCH_CHIPNET): ChainContext { + return { network, account: makeAccount(), fetch: (() => Promise.reject(new Error("no fetch in BCH tests"))) as typeof fetch }; +} diff --git a/packages/chains-bitcoincash/test/signatures.ts b/packages/chains-bitcoincash/test/signatures.ts new file mode 100644 index 00000000..08673449 --- /dev/null +++ b/packages/chains-bitcoincash/test/signatures.ts @@ -0,0 +1,24 @@ +/** + * Signature fixtures, computed once OFFLINE (outside the repo) with the vault's own derivation and signEcdsa + * (packages/vault/src/derive.ts + sign.ts: RFC 6979, low-S) from the public BIP-39 test vector ("abandon" x11 + + * "about", empty passphrase) at m/44'/145'/0'/0/0, this module's derivationPath(0). Each is r ‖ s (hex) ":" recovery + * id over a digest the tests compute (BCH sighashes and the signed-message hash). packages/vault/test/families87.test.ts + * cross-checks the key's CashAddr against libauth. + * + * AGENTS.md rule 1: no key or signing code outside packages/vault, tests included, so only the public key, addresses + * and signatures live here. + */ +export const FIX = { + publicKey: "02bbe7dbcdf8b2261530a867df7180b17a90b482f74f2736b8a30d3f756e42e217", + mainnet: "bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6", + testnet: "bchtest:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnqseeszx8x", + /** Account 1 (m/44'/145'/0'/0/1): somebody else in these tests. */ + otherPublicKey: "0262f8bf1ab1d25111c569da0133c30f8b3fa411e2f8af2565d3f0111cb34ae7f6", + otherTestnet: "bchtest:qp8sfdhgjlq68hlzka9lcsxtcnvuvnd0xqzwvwq4lg", + otherMainnet: "bitcoincash:qp8sfdhgjlq68hlzka9lcsxtcnvuvnd0xqxugfzzc5", + sigs: { + "061c3e26ecd3fb3b": "d42f98751a7a11c7a7bb577c01f8f9b6a9081314635ecefdabbb8db0904b897630b40df6ce4acb0ff7cc1ed66471da2af443aa35f12b8b8765972ba6f36c8041:0", + "2415758ece436187": "3aff570529e371a06bc8d5530d53668d2de9ceb6508637cd88c993290164a08d51fd2386761a3100cdf45aa052c8dc8ef419f1fa1b4f3e1ec3efcd47cd3b9ba0:0", + "7b77e855492ffd3c": "6950dab1c3b282610d55a44b42418ee71f7126d0c6dcc108b35e55382b64be372fc45f1bf2a47b3ac6b24227dfa80bb78bbeb5464d5dca9b085d66b4756db905:0", + } as Record, +}; diff --git a/packages/chains-bitcoincash/tsconfig.json b/packages/chains-bitcoincash/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-bitcoincash/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-cosmos/LICENSE b/packages/chains-cosmos/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-cosmos/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-cosmos/NOTICE b/packages/chains-cosmos/NOTICE new file mode 100644 index 00000000..72782df1 --- /dev/null +++ b/packages/chains-cosmos/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-cosmos +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-cosmos/README.md b/packages/chains-cosmos/README.md new file mode 100644 index 00000000..58466b0e --- /dev/null +++ b/packages/chains-cosmos/README.md @@ -0,0 +1,196 @@ +# @clip-wallet/chains-cosmos + +Cosmos SDK `ChainModule` for Clip Wallet: Osmosis, dYdX, ZIGChain, Provenance, THORChain and Initia. It builds, +decodes and simulates transactions and never touches keys. `prepare()` returns the 32-byte digest a Cosmos +signature covers; `finalize()` checks the vault's signature with `secp256k1.verify` before anything leaves the +wallet, then returns it to the app or broadcasts. + +Everything is hand-written on `@noble/hashes`, `@noble/curves` (verification and public-key maths only) and +`@scure/base`: protobuf (`src/proto.ts`), Amino JSON (`src/amino.ts`) and the REST client (`src/rest.ts`). No cosmjs +in the shipped module; the tests compare every encoding byte for byte with `cosmjs-types` and `@cosmjs/amino`. + +## One package, four families + +Each ecosystem's wallets derive at the chain's own SLIP-44 coin type, so each is its own vault key family and its +own module instance. `createCosmosModule({ family })` answers only that family's networks. + +| family | path (vault `derivationPath`) | key | networks | +|---|---|---|---| +| `cosmos` | `m/44'/118'/0'/0/i` | secp256k1 | Osmosis, dYdX, ZIGChain | +| `provenance` | `m/44'/505'/0'/0/i` | secp256k1 | Provenance | +| `thorchain` | `m/44'/931'/0'/0/i` | secp256k1 | THORChain | +| `initia` | `m/44'/60'/0'/0/i` | ethsecp256k1 | Initia L1 | + +```ts +import { createCosmosModule } from "@clip-wallet/chains-cosmos"; +import { COSMOS_NETWORKS, networksOfFamily } from "@clip-wallet/chains-cosmos/networks"; // no module code +const osmosis = createCosmosModule({ family: "cosmos" }); // also cosmosModule, provenanceModule, thorchainModule, initiaModule +``` + +Options: `simulate` (default true: dapp sign docs are simulated in `decode`), `gasAdjustment` (1.4), +`pollAttempts` (10) and `pollIntervalMs` (1500) for confirming a broadcast. + +## Networks + +Network ids are CAIP-2 in the `cosmos` namespace, `cosmos:`. `rpcUrls` are REST (LCD) endpoints, tried in +order (unreachable, 429 and plain 5xx answers move to the next). + +| id | name | prefix | native | REST endpoints | +|---|---|---|---|---| +| `cosmos:osmo-test-5` | Osmosis Testnet | `osmo` | OSMO (`uosmo`, 6) | lcd.osmotest5.osmosis.zone, lcd.testnet.osmosis.zone | +| `cosmos:osmosis-1` | Osmosis | `osmo` | OSMO | lcd.osmosis.zone, osmosis-rest.publicnode.com, osmosis-api.polkachu.com | +| `cosmos:dydx-testnet-4` | dYdX Testnet | `dydx` | DV4TNT (`adv4tnt`, 18) | dydx-testnet-api.polkachu.com, test-dydx-rest.kingnodes.com | +| `cosmos:dydx-mainnet-1` | dYdX | `dydx` | DYDX (`adydx`, 18) | dydx-rest.publicnode.com, dydx-api.polkachu.com | +| `cosmos:zig-test-2` | ZIGChain Testnet | `zig` | ZIG (`azig`, 18) | zigchain-testnet-api.polkachu.com, public-zigchain-testnet-lcd.numia.xyz, testnet-api.zigchain.com | +| `cosmos:zigchain-1` | ZIGChain | `zig` | ZIG (`azig`, 18) | public-zigchain-lcd.numia.xyz, zigchain-api.polkachu.com | +| `cosmos:pio-testnet-1` | Provenance Testnet | `tp` | HASH (`nhash`, 9) | api.test.provenance.io | +| `cosmos:pio-mainnet-1` | Provenance | `pb` | HASH (`nhash`, 9) | provenance-api.polkachu.com, provenance.api.pocket.network | +| `cosmos:thorchain-1` | THORChain | `thor` | RUNE (`rune`, 8) | gateway.liquify.com/chain/thorchain_api, thorchain.ibs.team/api | +| `cosmos:initiation-2` | Initia Testnet | `init` | INIT (`uinit`, 6) | rest.testnet.initia.xyz | +| `cosmos:interwoven-1` | Initia | `init` | INIT (`uinit`, 6) | rest.initia.xyz, initia-api.polkachu.com | + +Sources: github.com/cosmos/chain-registry (`/chain.json`, `testnets/testnet/chain.json`) and, for +Initia, github.com/initia-labs/initia-registry. Every endpoint answered `GET /cosmos/base/tendermint/v1beta1/node_info` +with the right `network` when checked (Oct 2026) and allows any origin, which matters because the extension background +has no host permission for them. thorchain.ibs.team sends its CORS header twice, which browsers refuse, so it's only +a last resort; `api.provenance.io` sends none and isn't used. None needs a key. Only one public REST endpoint was +found for `pio-testnet-1` and `initiation-2` (`rest-skip.testnet.initia.xyz` needs a key; thornode.ninerealms.com +no longer resolves). + +- **THORChain is mainnet only.** Its stagenet (`thorchain-stagenet-2`, `sthor` prefix) runs on real funds and + THORChain has no public testnet. +- **Provenance testnet keys.** Clip derives Provenance at coin 505 on both networks (`pb…` / `tp…` are the same key). + chain-registry and Keplr list `pio-testnet-1` with slip44 1, so a Keplr testnet account is a different key. +- **Initia testnet keys.** Clip derives Initia at coin 60 (ethsecp256k1) on both networks, as initia-registry + says. Keplr's `initiation-2` config uses coin 118, so a Keplr testnet account is a different key. + +Assets: the native coin (keys `osmo`, `dydx`, `zig`, `hash`, `rune`, `init`) and Circle USDC where it's canonical: +Noble USDC on Osmosis (`ibc/498A0751…` from noble-1; on osmo-test-5 `ibc/DE6792CF…` from Noble's testnet grand-1) +and on dYdX (`ibc/8E27BA2D…` on both dYdX networks, channel-0; also dYdX's collateral and second fee asset, so it's +listed even at 0). Denom traces checked with `/ibc/apps/transfer/v1/denom_traces/{hash}`. Other IBC and factory denoms are hidden from balances: anyone can register bank metadata +for a factory denom, including the name "USDC". In approvals an unknown denom is shown as its raw denom with no +decimals, never scaled by a guess. + +## Addresses + +- secp256k1: `bech32(prefix, RIPEMD-160(SHA-256(compressed key)))` (cosmjs `pubkeyToAddress`). +- Initia: `bech32("init", keccak256(uncompressed key)[12..])`, the EVM address's bytes. +- The vault's `Account.address` is the mainnet-style spelling (`cosmos1…`, `pb1…`, `thor1…`, `init1…`). + `receiveAddress(ctx)` and `addressFromPublicKey(key, network)` give the network's own prefix (`osmo1…`, `dydx1…`, + `zig1…`, `tp1…`). +- `isAddress` checks the bech32 checksum, one of the family's prefixes and a 20-byte (account) or 32-byte + (contract, module, Initia object) payload. `networksForAddress` goes by prefix. + +## Methods + +| method | params | result | +|---|---|---| +| `cosmos_signDirect` | `{ signerAddress, signDoc: { bodyBytes, authInfoBytes (base64), chainId, accountNumber (string) } }` | `{ signed: signDoc, signature: StdSignature }` | +| `cosmos_signAmino` | `{ signerAddress, signDoc: StdSignDoc }` | `{ signed, signature }` | +| `cosmos_signArbitrary` | `{ signer, data (base64) }` | `StdSignature` (ADR-36) | +| `cosmos_signAndBroadcast` | `{ signDoc }`, wallet-built only (`origin: WALLET_ORIGIN`) | `{ txhash, status: "success" \| "pending", height? }` | +| `read("cosmos_sendTx")` | `{ tx (base64 TxRaw), mode: "sync" \| "async" \| "block" }` | `{ txhash }` (no approval, as Keplr `sendTx`) | +| `read("cosmos_verifyArbitrary")` | `{ signer, data, signature }` | `boolean` (no approval) | + +`cosmos_signDirect` and `cosmos_signAmino` are the WalletConnect Cosmos RPC methods +(https://docs.reown.com/advanced/multichain/rpc-reference/cosmos-rpc). `StdSignature` is +`{ pub_key: { type: "tendermint/PubKeySecp256k1", value }, signature }` (base64 r‖s), on Initia too, as Keplr returns it. + +What gets signed (`prepare`): + +- **Direct:** the `SignDoc` protobuf (`body_bytes`, `auth_info_bytes`, `chain_id`, `account_number`). +- **Amino:** the canonical Amino JSON (sorted keys, `<`, `>`, `&` escaped), as cosmjs `serializeSignDoc`. +- **ADR-36:** Keplr's `makeADR36AminoSignDoc` (chain_id "", account 0, sequence 0, fee 0, one `sign/MsgSignData`). + An ADR-36 doc arriving through `cosmos_signAmino` is treated as a message signature. +- **Digest:** SHA-256 of those bytes; Initia (ethsecp256k1): Keccak-256, as `initia/crypto/ethsecp256k1` verifies and + Keplr signs on `eth-key-sign` chains. + +Refused in plain words before anything is signed: a sign doc or request for another network (`cosmos/network-mismatch`), +a `signerAddress`/`signer` that isn't this account on that chain (`cosmos/wrong-account`), a transaction whose signer +keys or messages don't include this account (`cosmos/not-a-signer`), `cosmos_signAndBroadcast` from a site. + +## decode() + +| message | title | notes | +|---|---|---| +| bank `MsgSend` (THORChain `types.MsgSend`) | "Send 1.5 OSMO to osmo1jrkm…4pqs" (or "Receive …", "{from} sends …") | balance changes for this account | +| IBC `MsgTransfer` | "Send 1 OSMO to noble1…" | "Through: IBC channel …", caution `network-matters` | +| staking `MsgDelegate` / `MsgUndelegate` / `MsgBeginRedelegate` (and Initia `mstaking`) | "Stake 2 OSMO with …", "Unstake …", "Move … of stake from … to …" | unbonding explained | +| distribution `MsgWithdrawDelegatorReward` | "Claim staking rewards from …" | | +| CosmWasm `MsgExecuteContract` | "Use swap on contract osmo1…" | contract, the JSON message, funds as balance changes, caution `unknown-call` | +| authz `MsgGrant` / feegrant `MsgGrantAllowance` | "Let osmo1… act for your account", "Let … pay network fees with your OSMO" | **danger `account-takeover`** | +| authz `MsgRevoke` / feegrant `MsgRevokeAllowance` | "Stop … from …" | | +| anything else (or unreadable bytes, extension options, tips) | "Approve a transaction for {host}" | `blind: true`, danger `blind-signing` | +| ADR-36 | "Sign a message for {host}" | text, or hex with a caution when it isn't text | + +Every request shows the memo, "Valid until" (timeout height), the network fee and, for dapp signatures, "Sent by +{host} (it gets the signed transaction)". A fee paid by a granter or another payer is said. Direct sign docs are +simulated (`/cosmos/tx/v1beta1/simulate` with an empty signature): success sets `simulated`, an insufficient-funds +failure is a danger warning, anything else a caution. + +## Sending + +`buildTransfer({ asset, to, amount })` → `cosmos_signAndBroadcast`: + +1. The account from `/cosmos/auth/v1beta1/account_info/{addr}` (SDK ≥ 0.47), falling back to `/accounts/{addr}` + with BaseAccount fields found inside any wrapper (vesting, EthAccount). Not found → "Your account isn't on + {network} yet. Receive some {symbol} first." Module accounts are refused. +2. Balances from `/cosmos/bank/v1beta1/balances/{addr}`. +3. `MsgSend` (THORChain: `/types.MsgSend` with raw 20-byte addresses), a + SIGN_MODE_DIRECT signer info with this key's type URL (`/cosmos.crypto.secp256k1.PubKey`, Initia + `/initia.crypto.v1beta1.ethsecp256k1.PubKey`) and the on-chain sequence. +4. Gas: simulate, × 1.4 (THORChain: at least 6 000 000, as xchainjs; THORNode doesn't charge gas). +5. Fee: gas limit × gas price of the first fee token the account can pay (chain-registry prices; dYdX falls back to + USDC, the asset its own clients default to). + - Osmosis: the x/txfees EIP-1559 base fee (`/osmosis/txfees/v1beta1/cur_eip_base_fee`) × 1.65, the + `defaultBaseFeeMultiplier` of osmosis-frontend `packages/tx/src/gas.ts`. + - Initia: x/dynamicfee's current price (`/initia/tx/v1/gas_prices/uinit`) × 1.05. + - Provenance: x/flatfees charges per message type, not per gas. `POST /provenance/tx/v1/calculate_flat_fee` + (`{ tx_bytes, gas_adjustment }`) gives `total_fees` and `estimated_gas`, used as they are + (provenance `x/flatfees/spec/01_concepts.md`; a plain simulate would report the fee as `gas_used`). + - THORChain: no fee coins; the chain takes its fixed native fee (`native_tx_fee_rune` from `/thorchain/network`, + 0.02 RUNE today) from the balance, and it's checked before sending. Bank `MsgSend` is accepted today only + because mimir `BANKSENDENABLED=1` (x/thorchain/ante.go), so Clip sends `/types.MsgSend` as xchainjs does. +6. Broadcast `BROADCAST_MODE_SYNC` to `/cosmos/tx/v1beta1/txs`, then poll `/cosmos/tx/v1beta1/txs/{hash}`. + SDK error codes become plain words (`plainCosmosError`). + +## Balances + +`getBalances`: the native coin (0 when the account doesn't exist yet) and the curated USDC. `getNfts` returns `[]`. + +## Dapp connectivity + +1Mask exposes a Keplr-compatible provider at `window.clipwallet.cosmos` (never `window.keplr`): +`packages/1mask/src/inpage/cosmos.ts`, `background/cosmos.ts`, `shared/cosmos.ts`. It implements `enable`, `disable`, +`getKey`, `signDirect`, `signAmino`, `signArbitrary`, `verifyArbitrary`, `sendTx`, `getOfflineSigner`, +`getOfflineSignerOnlyAmino`, `getOfflineSignerAuto`, `getChainInfosWithoutEndpoints`, `getChainInfoWithoutEndpoints` +and `experimentalSuggestChain` (refuses chains Clip doesn't ship), checked against `@keplr-wallet/types` 0.13.41 +`src/wallet/keplr.ts` and `@keplr-wallet/provider` 0.13.41 `src/cosmjs.ts` (the GitHub repository is no longer public; +the npm packages ship the source), and https://docs.keplr.app/api/guide/. + +## Testnet faucets + +| network | faucet | gate | +|---|---|---| +| osmo-test-5 | https://faucet.testnet.osmosis.zone | address form + Cloudflare Turnstile captcha, no login | +| dydx-testnet-4 | `POST https://faucet.v4testnet.dydx.exchange/faucet/native-token { address }` (v4-clients `faucet_client.py`) | no captcha in the API; `/faucet/tokens` funds a trading subaccount, not the bank balance | +| zig-test-2 | https://faucet.zigchain.com | behind a Cloudflare challenge; docs say once per day per address | +| initiation-2 | https://app.testnet.initia.xyz/faucet | address form + Turnstile, no login | +| pio-testnet-1 | none: the explorer faucet (explorer.test.provenance.io/faucet) reached end of life | | +| thorchain-1 | none (mainnet only) | | + +## Known gaps + +- No Ledger, multisig or fee-grant-aware sending. Dapp fees are signed as given (Keplr's `preferNoSetFee` fee editing + isn't offered). +- Initia `mstaking`/distribution messages are explained; other Initia Move messages (`/initia.move.v1.MsgExecute`) + are blind. +- THORChain `MsgDeposit` (swaps, memos) is blind. +- `ethereumHexAddress` in `getKey` is lower-case hex (a valid EIP-55 spelling); Keplr checksums it. + +## Tests + +`test/proto.test.ts` (encodings against cosmjs-types / @cosmjs/amino) and `test/cosmos.test.ts` (addresses, +balances, decode/prepare/finalize, transfers, refusals) use no network: REST answers are fixtures shaped like the +live nodes. Signatures in `test/signatures.ts` were computed offline with the vault's signing code from the public +"abandon … about" account; only public keys, addresses and signatures are in the repo. diff --git a/packages/chains-cosmos/package.json b/packages/chains-cosmos/package.json new file mode 100644 index 00000000..98a8bd17 --- /dev/null +++ b/packages/chains-cosmos/package.json @@ -0,0 +1,88 @@ +{ + "name": "@clip-wallet/chains-cosmos", + "version": "0.1.0", + "description": "Cosmos SDK ChainModule for Clip Wallet (Osmosis, dYdX, ZIGChain, Provenance, THORChain, Initia): builds and decodes transactions in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "cosmos", + "osmosis", + "dydx", + "provenance", + "thorchain", + "initia", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-cosmos#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-cosmos" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "devDependencies": { + "@cosmjs/amino": "^0.39.0", + "@cosmjs/crypto": "^0.39.0", + "@cosmjs/proto-signing": "^0.39.0", + "@types/node": "^22.20.5", + "cosmjs-types": "^0.11.0" + } +} diff --git a/packages/chains-cosmos/src/address.ts b/packages/chains-cosmos/src/address.ts new file mode 100644 index 00000000..b299595b --- /dev/null +++ b/packages/chains-cosmos/src/address.ts @@ -0,0 +1,49 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { ripemd160 } from "@noble/hashes/legacy.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { bech32 } from "@scure/base"; + +/** + * Account addresses, matching the vault (packages/vault/src/encodings87.ts): + * - secp256k1 (cosmos, provenance, thorchain): bech32(prefix, RIPEMD-160(SHA-256(compressed key))) — ADR-028 legacy + * accounts, cosmjs `pubkeyToAddress`. + * - ethsecp256k1 (initia): bech32("init", keccak256(uncompressed x‖y)[12..]) — the EVM address's 20 bytes + * (initia crypto/ethsecp256k1 PubKey.Address()). + */ +export type KeyKind = "secp256k1" | "ethsecp256k1"; + +export function compressedKey(publicKey: Uint8Array): Uint8Array { + if (publicKey.length !== 33 && publicKey.length !== 65) throw new Error("expected a secp256k1 public key"); + return secp256k1.Point.fromBytes(publicKey).toBytes(true); +} + +export function addressBytes(publicKey: Uint8Array, kind: KeyKind): Uint8Array { + if (kind === "ethsecp256k1") { + const full = secp256k1.Point.fromBytes(compressedKey(publicKey)).toBytes(false); + return keccak_256(full.subarray(1)).subarray(12); + } + return ripemd160(sha256(compressedKey(publicKey))); +} + +export function bech32Address(prefix: string, data: Uint8Array): string { + return bech32.encode(prefix, bech32.toWords(data)); +} + +/** { prefix, data } for a valid bech32 (not bech32m) string in lower case, else null. */ +export function decodeBech32(value: string): { prefix: string; data: Uint8Array } | null { + const v = value.trim(); + if (v !== v.toLowerCase()) return null; + try { + const d = bech32.decode(v as `${string}1${string}`, 90); + return { prefix: d.prefix, data: bech32.fromWords(d.words) }; + } catch { + return null; + } +} + +/** Account (20 bytes) or contract/module (32 bytes) address with one of `prefixes`. */ +export function isAccountAddress(value: string, prefixes: readonly string[]): boolean { + const d = decodeBech32(value); + return !!d && prefixes.includes(d.prefix) && (d.data.length === 20 || d.data.length === 32); +} diff --git a/packages/chains-cosmos/src/amino.ts b/packages/chains-cosmos/src/amino.ts new file mode 100644 index 00000000..a19f56ef --- /dev/null +++ b/packages/chains-cosmos/src/amino.ts @@ -0,0 +1,101 @@ +import { utf8 } from "./util.js"; + +/** + * Legacy Amino JSON signing (SIGN_MODE_LEGACY_AMINO_JSON) and ADR-36 arbitrary-message signing. + * + * - StdSignDoc and its canonical bytes: cosmjs `serializeSignDoc` = UTF-8 of `sortedJsonStringify(doc)` with "&", "<" + * and ">" escaped as \u0026, \u003c, \u003e (Go's encoding/json HTML escaping, which the SDK's legacytx + * StdSignBytes → sdk.MustSortJSON produces). https://github.com/cosmos/cosmjs/blob/main/packages/amino/src/signdoc.ts + * - ADR-36: https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-036-arbitrary-signature.md and Keplr + * `makeADR36AminoSignDoc` (packages/cosmos/src/adr-36/amino.ts): chain_id "", account_number "0", sequence "0", + * fee { gas "0", amount [] }, memo "", one msg { type "sign/MsgSignData", value { signer, data: base64(bytes) } }. + */ + +export interface AminoCoin { + denom: string; + amount: string; +} + +export interface AminoMsg { + type: string; + value: unknown; +} + +export interface StdFee { + amount: readonly AminoCoin[]; + gas: string; + payer?: string; + granter?: string; +} + +export interface StdSignDoc { + chain_id: string; + account_number: string; + sequence: string; + fee: StdFee; + msgs: readonly AminoMsg[]; + memo: string; + timeout_height?: string; +} + +function sortedObject(obj: unknown): unknown { + if (obj === null || typeof obj !== "object") return obj; + if (Array.isArray(obj)) return obj.map(sortedObject); + const out: Record = {}; + for (const k of Object.keys(obj as Record).sort()) out[k] = sortedObject((obj as Record)[k]); + return out; +} + +/** JSON with sorted keys and Go's HTML escaping (cosmjs sortedJsonStringify + escapeCharacters). */ +export function sortedJson(obj: unknown): string { + return JSON.stringify(sortedObject(obj)).replace(/&/g, "\\u0026").replace(//g, "\\u003e"); +} + +export function serializeSignDoc(doc: StdSignDoc): Uint8Array { + return utf8(sortedJson(doc)); +} + +/** Shape check for a StdSignDoc from a dapp (strings where the SDK has strings). */ +export function isStdSignDoc(x: unknown): x is StdSignDoc { + if (!x || typeof x !== "object") return false; + const d = x as Record; + const fee = d.fee as Record | undefined; + return ( + typeof d.chain_id === "string" && + typeof d.account_number === "string" && + typeof d.sequence === "string" && + typeof d.memo === "string" && + Array.isArray(d.msgs) && + d.msgs.every((m) => !!m && typeof m === "object" && typeof (m as AminoMsg).type === "string") && + !!fee && + typeof fee === "object" && + typeof fee.gas === "string" && + Array.isArray(fee.amount) && + (fee.amount as unknown[]).every((c) => !!c && typeof (c as AminoCoin).denom === "string" && typeof (c as AminoCoin).amount === "string") + ); +} + +export const ADR36_MSG_TYPE = "sign/MsgSignData"; + +/** Keplr makeADR36AminoSignDoc: `data` is base64 of the bytes (a string is UTF-8 encoded first). */ +export function makeAdr36SignDoc(signer: string, dataBase64: string): StdSignDoc { + return { + chain_id: "", + account_number: "0", + sequence: "0", + fee: { gas: "0", amount: [] }, + msgs: [{ type: ADR36_MSG_TYPE, value: { signer, data: dataBase64 } }], + memo: "", + }; +} + +/** True for an ADR-36 sign doc exactly as Keplr builds it (one MsgSignData, nothing else set). */ +export function isAdr36SignDoc(doc: StdSignDoc): { signer: string; data: string } | null { + if (doc.chain_id !== "" || doc.account_number !== "0" || doc.sequence !== "0" || doc.memo !== "") return null; + if (doc.fee.gas !== "0" || doc.fee.amount.length !== 0 || doc.msgs.length !== 1) return null; + const m = doc.msgs[0]!; + const v = m.value as { signer?: unknown; data?: unknown } | null; + if (m.type !== ADR36_MSG_TYPE || !v || typeof v.signer !== "string" || typeof v.data !== "string") return null; + if (Object.keys(v).length !== 2) return null; + return { signer: v.signer, data: v.data }; +} diff --git a/packages/chains-cosmos/src/describe.ts b/packages/chains-cosmos/src/describe.ts new file mode 100644 index 00000000..0e7efdb9 --- /dev/null +++ b/packages/chains-cosmos/src/describe.ts @@ -0,0 +1,184 @@ +import { type AssetRef, type BalanceChange, type Msg, type Warning, msg } from "@clip-wallet/core"; +import type { CosmosChainSpec } from "./networks.js"; +import { assetOf } from "./networks.js"; +import type { CosmosMsg } from "./msgs.js"; +import type { Coin } from "./proto.js"; +import { formatUnits, short } from "./util.js"; + +/** + * Plain words for understood Cosmos messages. Titles come from the shared catalog where one fits + * ("bg.req.sendTo", "bg.req.stakeWith", "bg.req.unstakeFrom", "bg.req.useFnOnContract") and from the + * chains-cosmos section ("bg.cosmos.*") otherwise. + */ + +export interface Line { + label: string; + value: string; + labelMsg?: Msg; + valueMsg?: Msg; +} + +export interface Described { + title: string; + titleMsg?: Msg; + lines: Line[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + blind: boolean; +} + +export function amountText(spec: CosmosChainSpec, coins: readonly Coin[]): string { + if (!coins.length) return `0 ${spec.native.symbol}`; + return coins.map((c) => coinText(spec, c)).join(" + "); +} + +export function coinText(spec: CosmosChainSpec, c: Coin): string { + const a = assetOf(spec, c.denom); + return `${formatUnits(BigInt(c.amount), a.decimals)} ${a.symbol}`; +} + +const line = (label: string, value: string, labelMsg?: Msg, valueMsg?: Msg): Line => ({ label, value, ...(labelMsg ? { labelMsg } : {}), ...(valueMsg ? { valueMsg } : {}) }); + +const titled = (m: Msg): { title: string; titleMsg: Msg } => ({ title: m.fallback, titleMsg: m }); + +/** Adds `delta` (signed) of each coin to the running totals. */ +function add(totals: Map, coins: readonly Coin[], sign: 1n | -1n): void { + for (const c of coins) totals.set(c.denom, (totals.get(c.denom) ?? 0n) + sign * BigInt(c.amount)); +} + +/** Messages that move or lock the user's funds, or hand over control, explained one by one. */ +export function describeMsgs(spec: CosmosChainSpec, msgs: CosmosMsg[], me: string, host: string): Described { + const totals = new Map(); + const lines: Line[] = []; + const warnings: Warning[] = []; + let blind = false; + const parts: { title: string; titleMsg?: Msg }[] = []; + + for (const m of msgs) { + switch (m.kind) { + case "send": { + const amount = amountText(spec, m.amount); + if (m.from === me) add(totals, m.amount, -1n); + if (m.to === me) add(totals, m.amount, 1n); + if (m.from === me) parts.push(titled(msg("bg.req.sendTo", { amount, to: short(m.to) }))); + else if (m.to === me) parts.push(titled(msg("bg.req.receiveFrom", { amount, from: short(m.from) }))); + else parts.push(titled(msg("bg.req.othersSend", { from: short(m.from), amount, to: short(m.to) }))); + lines.push(line("To", m.to)); + break; + } + case "ibc-transfer": { + const amount = coinText(spec, m.token); + if (m.sender === me) add(totals, [m.token], -1n); + parts.push(titled(msg("bg.req.sendTo", { amount, to: short(m.receiver) }))); + lines.push(line("To", m.receiver)); + const through = msg("bg.cosmos.ibcChannel", { channel: m.channel }); + lines.push(line("Through", through.fallback, undefined, through)); + if (m.memo) lines.push(line("Memo", m.memo)); + const w = msg("bg.cosmos.ibcLeaves", { channel: m.channel }); + warnings.push({ level: "caution", code: "network-matters", message: w.fallback, msg: w }); + break; + } + case "delegate": { + const amount = amountText(spec, m.amount); + if (m.delegator === me) add(totals, m.amount, -1n); + parts.push(titled(msg("bg.req.stakeWith", { amount, validator: short(m.validator) }))); + lines.push(line("Validator", m.validator)); + break; + } + case "undelegate": { + const amount = amountText(spec, m.amount); + parts.push(titled(msg("bg.req.unstakeFrom", { amount, validator: short(m.validator) }))); + lines.push(line("Validator", m.validator)); + const w = msg("bg.cosmos.unbonding", { symbol: spec.native.symbol }); + lines.push(line("What happens", w.fallback, undefined, w)); + break; + } + case "redelegate": { + const amount = amountText(spec, m.amount); + parts.push(titled(msg("bg.cosmos.moveStake", { amount, from: short(m.from), to: short(m.to) }))); + lines.push(line("Validator", m.to)); + break; + } + case "withdraw-rewards": + parts.push(titled(msg("bg.cosmos.claimRewardsFrom", { validator: short(m.validator) }))); + lines.push(line("Validator", m.validator)); + break; + case "execute": { + const fn = m.msg && typeof m.msg === "object" && !Array.isArray(m.msg) && Object.keys(m.msg).length === 1 ? Object.keys(m.msg)[0]! : ""; + if (m.sender === me) add(totals, m.funds, -1n); + parts.push(fn ? titled(msg("bg.req.useFnOnContract", { fn, contract: short(m.contract) })) : titled(msg("bg.req.contractActionFor", { host }))); + lines.push(line("Contract", m.contract)); + lines.push(line("Message", pretty(m.msg, m.msgText))); + if (m.funds.length) lines.push(line("Also sends", amountText(spec, m.funds))); + warnings.push({ level: "caution", code: "unknown-call", message: "We can't read what this message tells the contract to do. Only continue if you fully trust this app." }); + break; + } + case "authz-grant": { + const all = isSendLike(m.authorization); + parts.push(titled(msg("bg.cosmos.grantAuthz", { grantee: short(m.grantee) }))); + lines.push(line("Allowed", m.grantee)); + lines.push(line("Permission", m.authorization)); + if (m.expiration !== undefined) lines.push(line("Expires", new Date(Number(m.expiration) * 1000).toISOString().slice(0, 10))); + const w = all ? msg("bg.cosmos.authzTakeAll", { grantee: short(m.grantee) }) : msg("bg.cosmos.authzActs", { grantee: short(m.grantee) }); + warnings.push({ level: "danger", code: "account-takeover", message: w.fallback, msg: w }); + break; + } + case "authz-revoke": + parts.push(titled(msg("bg.cosmos.revokeAuthz", { grantee: short(m.grantee) }))); + lines.push(line("Permission", m.msgTypeUrl)); + break; + case "feegrant": { + parts.push(titled(msg("bg.cosmos.grantFees", { grantee: short(m.grantee), symbol: spec.native.symbol }))); + lines.push(line("Allowed", m.grantee)); + if (m.spendLimit.length) lines.push(line("Limit", amountText(spec, m.spendLimit))); + const w = m.spendLimit.length + ? msg("bg.cosmos.feegrantUpTo", { grantee: short(m.grantee), amount: amountText(spec, m.spendLimit) }) + : msg("bg.cosmos.feegrantAll", { grantee: short(m.grantee), symbol: spec.native.symbol }); + warnings.push({ level: "danger", code: "account-takeover", message: w.fallback, msg: w }); + break; + } + case "feegrant-revoke": + parts.push(titled(msg("bg.cosmos.revokeFees", { grantee: short(m.grantee) }))); + break; + case "unknown": + blind = true; + lines.push(line("Action", m.type)); + break; + } + } + + if (blind) { + warnings.unshift({ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }); + } + const balanceChanges: BalanceChange[] = [...totals.entries()] + .filter(([, d]) => d !== 0n) + .map(([denom, d]) => ({ asset: assetOf(spec, denom), delta: d.toString() })); + + let head: { title: string; titleMsg?: Msg }; + if (blind) head = titled(msg("bg.req.approveTxFor", { host })); + else if (parts.length === 1) head = parts[0]!; + else if (parts.length > 1) { + head = parts[0]!; + for (const p of parts.slice(1)) lines.push(line("Also", p.title, undefined, p.titleMsg)); + } else head = titled(msg("bg.req.approveTxFor", { host })); + return { ...head, lines, balanceChanges, warnings, blind }; +} + +function isSendLike(authorization: string): boolean { + return /MsgSend$|SendAuthorization$|MsgExec$|MsgTransfer$|MsgExecuteContract$|MsgGrant$/.test(authorization); +} + +function pretty(v: unknown, text: string): string { + const s = v === null ? text : JSON.stringify(v, null, 2); + return s.length > 2000 ? `${s.slice(0, 2000)}…` : s; +} + +export function feeLine(spec: CosmosChainSpec, fee: readonly Coin[]): Line { + return line("Network fee", fee.length ? amountText(spec, fee) : `0 ${spec.native.symbol}`); +} + +/** Fee for DecodedRequest.fee: the first coin (Cosmos fees are almost always one coin). */ +export function feeOf(spec: CosmosChainSpec, fee: readonly Coin[]): { asset: AssetRef; amount: string } { + const c = fee[0]; + return c ? { asset: assetOf(spec, c.denom), amount: c.amount } : { asset: assetOf(spec, spec.native.denom), amount: "0" }; +} diff --git a/packages/chains-cosmos/src/index.ts b/packages/chains-cosmos/src/index.ts new file mode 100644 index 00000000..3f40c963 --- /dev/null +++ b/packages/chains-cosmos/src/index.ts @@ -0,0 +1,22 @@ +export { + COSMOS_METHODS, + COSMOS_READ_METHODS, + type CosmosModuleOptions, + type CosmosReadMethod, + type StdSignature, + type WireSignDoc, + createCosmosModule, +} from "./module.js"; +export * from "./networks.js"; +export { type StdSignDoc, type AminoMsg, makeAdr36SignDoc, serializeSignDoc, sortedJson } from "./amino.js"; +export { type CosmosMsg, fromAmino, fromAny } from "./msgs.js"; +export { addressBytes, bech32Address, decodeBech32, isAccountAddress } from "./address.js"; +export { plainCosmosError } from "./rest.js"; + +import { createCosmosModule } from "./module.js"; + +/** One instance per family (each answers only its own networks). */ +export const cosmosModule = createCosmosModule({ family: "cosmos" }); +export const provenanceModule = createCosmosModule({ family: "provenance" }); +export const thorchainModule = createCosmosModule({ family: "thorchain" }); +export const initiaModule = createCosmosModule({ family: "initia" }); diff --git a/packages/chains-cosmos/src/module.ts b/packages/chains-cosmos/src/module.ts new file mode 100644 index 00000000..1ba66440 --- /dev/null +++ b/packages/chains-cosmos/src/module.ts @@ -0,0 +1,613 @@ +import { + type AssetRef, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + type Warning, + WALLET_ORIGIN, + isWalletOrigin, + msg, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { addressBytes, bech32Address, compressedKey, decodeBech32, isAccountAddress } from "./address.js"; +import { type StdSignDoc, isAdr36SignDoc, isStdSignDoc, makeAdr36SignDoc, serializeSignDoc } from "./amino.js"; +import { type Described, amountText, describeMsgs, feeLine, feeOf } from "./describe.js"; +import { type CosmosMsg, fromAmino, fromAny, signerOf } from "./msgs.js"; +import { type CosmosChainSpec, type CosmosFamily, assetOf, chainsOf, derivationPathOf, nativeAsset, networksOfFamily, specOf } from "./networks.js"; +import { + type Coin, + ProtoError, + SIGN_MODE_DIRECT, + TYPE, + decodeAuthInfo, + decodePubKey, + decodeTxBody, + decodeTxRaw, + encodeAuthInfo, + encodeMsgSend, + encodePubKey, + encodeSignDoc, + encodeThorMsgSend, + encodeTxBody, + encodeTxRaw, +} from "./proto.js"; +import { RestError, type Rest, broadcastSync, getAccount, getAllBalances, getTx, plainCosmosError, restFor, simulate } from "./rest.js"; +import { b64decode, b64encode, equalBytes, fromHex, hex, hostOf, isObj, mulCeil, parseDecimal, randomId, sleep } from "./util.js"; + +/** + * Request methods (DappRequest.method). The first two are the WalletConnect Cosmos RPC methods + * (https://docs.reown.com/advanced/multichain/rpc-reference/cosmos-rpc): params carry `signerAddress` and a sign doc + * whose bytes are base64, results are `{ signed, signature: StdSignature }`. The rest are Clip Wallet's own: + * - cosmos_signArbitrary: ADR-36 (Keplr signArbitrary), params { signer, data: base64 }, result StdSignature. + * - cosmos_signAndBroadcast: wallet-built transactions only (buildTransfer); params { signDoc }, result { txhash }. + * Read-only, no approval (`read()`): cosmos_sendTx (Keplr sendTx: broadcast already-signed bytes) and + * cosmos_verifyArbitrary (Keplr verifyArbitrary). + */ +export const COSMOS_METHODS = { + signDirect: "cosmos_signDirect", + signAmino: "cosmos_signAmino", + signArbitrary: "cosmos_signArbitrary", + signAndBroadcast: "cosmos_signAndBroadcast", + sendTx: "cosmos_sendTx", + verifyArbitrary: "cosmos_verifyArbitrary", +} as const; + +export const COSMOS_READ_METHODS = [COSMOS_METHODS.sendTx, COSMOS_METHODS.verifyArbitrary] as const; +export type CosmosReadMethod = (typeof COSMOS_READ_METHODS)[number]; + +export interface CosmosModuleOptions { + family: CosmosFamily; + /** Run /cosmos/tx/v1beta1/simulate in decode() for direct sign docs (default true). */ + simulate?: boolean; + /** Gas limit = simulated gas × this (default 1.4). */ + gasAdjustment?: number; + /** Polls of /cosmos/tx/v1beta1/txs/{hash} after a sync broadcast (default 10, 1.5 s apart). */ + pollAttempts?: number; + pollIntervalMs?: number; +} + +/** StdSignature (Keplr / cosmjs): pub_key is amino JSON, signature base64 r‖s. */ +export interface StdSignature { + pub_key: { type: string; value: string }; + signature: string; +} + +/** A sign doc as WalletConnect / 1Mask carry it: bytes base64, account number a decimal string. */ +export interface WireSignDoc { + bodyBytes: string; + authInfoBytes: string; + chainId: string; + accountNumber: string; +} + +const AMINO_PUBKEY = "tendermint/PubKeySecp256k1"; + +type Normalized = + | { method: "direct"; spec: CosmosChainSpec; me: string; wire: WireSignDoc; signBytes: Uint8Array; body: ReturnType | null; auth: ReturnType | null; broadcast: boolean } + | { method: "amino"; spec: CosmosChainSpec; me: string; doc: StdSignDoc; signBytes: Uint8Array } + | { method: "arbitrary"; spec: CosmosChainSpec; me: string; doc: StdSignDoc; data: Uint8Array; signBytes: Uint8Array }; + +function bad(message = "This request from the app is malformed, so we stopped it."): ClipError { + return new ClipError(message, "cosmos/bad-request"); +} + +function bytesParam(v: unknown, what: string): Uint8Array { + if (typeof v === "string") { + try { + return b64decode(v); + } catch { + throw bad(); + } + } + if (v instanceof Uint8Array) return v; + if (Array.isArray(v) && v.every((x) => Number.isInteger(x) && x >= 0 && x < 256)) return Uint8Array.from(v as number[]); + if (isObj(v) && Object.keys(v).every((k) => /^\d+$/.test(k))) return Uint8Array.from(Object.values(v) as number[]); + throw bad(`This request from the app is malformed (${what}), so we stopped it.`); +} + +function wireSignDoc(v: unknown): WireSignDoc { + if (!isObj(v)) throw bad(); + const accountNumber = typeof v.accountNumber === "number" || typeof v.accountNumber === "bigint" ? String(v.accountNumber) : v.accountNumber; + if (typeof v.chainId !== "string" || typeof accountNumber !== "string" || !/^\d+$/.test(accountNumber)) throw bad(); + return { bodyBytes: b64encode(bytesParam(v.bodyBytes, "bodyBytes")), authInfoBytes: b64encode(bytesParam(v.authInfoBytes, "authInfoBytes")), chainId: v.chainId, accountNumber }; +} + +export function createCosmosModule(options: CosmosModuleOptions): ChainModule & { + /** The family's networks (Network objects), as in ./networks. */ + networks: Network[]; + read(method: CosmosReadMethod, params: unknown, ctx: ChainContext): Promise; + receiveAddress(ctx: ChainContext): Promise; + /** This account's address on `network` (bech32 with that network's prefix). */ + addressOn(publicKeyHex: string, network: Network | string): string; +} { + const family = options.family; + if (!["cosmos", "provenance", "thorchain", "initia"].includes(family)) throw new Error(`unknown cosmos family ${String(family)}`); + const specs = chainsOf(family); + const prefixes = [...new Set(specs.map((s) => s.prefix))]; + const doSimulate = options.simulate ?? true; + const gasAdj = options.gasAdjustment ?? 1.4; + const pollAttempts = options.pollAttempts ?? 10; + const pollMs = options.pollIntervalMs ?? 1500; + + const specFor = (networkId: string): CosmosChainSpec | null => { + const s = specOf(networkId); + return s && s.family === family ? s : null; + }; + + const addressOnSpec = (spec: CosmosChainSpec, publicKey: Uint8Array) => bech32Address(spec.prefix, addressBytes(publicKey, spec.keyKind)); + const pubOf = (ctx: ChainContext) => compressedKey(fromHex(ctx.account.publicKey)); + const digestOf = (spec: CosmosChainSpec, bytes: Uint8Array) => (spec.keyKind === "ethsecp256k1" ? keccak_256(bytes) : sha256(bytes)); + + function requireSpec(request: DappRequest, ctx: ChainContext): CosmosChainSpec { + const spec = specFor(request.networkId); + if (!spec || request.networkId !== ctx.network.id || request.family !== family) { + throw new ClipError("This app is asking for a different network than the one it's connected to.", "cosmos/network-mismatch"); + } + return spec; + } + + function requireSigner(signer: unknown, me: string): void { + if (typeof signer !== "string" || signer !== me) { + throw new ClipError("This request is for a different account than the one you connected.", "cosmos/wrong-account"); + } + } + + function normalize(request: DappRequest, ctx: ChainContext): Normalized { + const spec = requireSpec(request, ctx); + const me = addressOnSpec(spec, pubOf(ctx)); + const p = isObj(request.params) ? request.params : {}; + switch (request.method) { + case COSMOS_METHODS.signDirect: + case COSMOS_METHODS.signAndBroadcast: { + const broadcast = request.method === COSMOS_METHODS.signAndBroadcast; + if (broadcast && !isWalletOrigin(request.origin)) throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "cosmos/unsupported-method"); + if (!broadcast) requireSigner(p.signerAddress, me); + const wire = wireSignDoc(p.signDoc); + if (wire.chainId !== spec.chainId) throw new ClipError("This app is asking for a different network than the one it's connected to.", "cosmos/network-mismatch"); + const bodyBytes = b64decode(wire.bodyBytes); + const authInfoBytes = b64decode(wire.authInfoBytes); + let body: ReturnType | null = null; + let auth: ReturnType | null = null; + try { + body = decodeTxBody(bodyBytes); + auth = decodeAuthInfo(authInfoBytes); + } catch (e) { + if (!(e instanceof ProtoError)) throw e; + body = null; + auth = null; + } + // Some signer key must be ours (any key type URL: the bytes decide). Signer infos without keys can't be checked here. + if (auth) { + const keys = auth.signerInfos.filter((s) => s.publicKey).map((s) => s.publicKey!); + const mine = pubOf(ctx); + if (keys.length && !keys.some((k) => safeEq(decodePubKeySafe(k.value), mine))) { + throw new ClipError("This transaction doesn't need your signature.", "cosmos/not-a-signer"); + } + } + const signBytes = encodeSignDoc({ bodyBytes, authInfoBytes, chainId: wire.chainId, accountNumber: BigInt(wire.accountNumber) }); + return { method: "direct", spec, me, wire, signBytes, body, auth, broadcast }; + } + case COSMOS_METHODS.signAmino: { + requireSigner(p.signerAddress, me); + if (!isStdSignDoc(p.signDoc)) throw bad(); + const doc = p.signDoc; + const adr36 = isAdr36SignDoc(doc); + if (adr36) { + requireSigner(adr36.signer, me); + return { method: "arbitrary", spec, me, doc, data: b64decode(adr36.data), signBytes: serializeSignDoc(doc) }; + } + if (doc.chain_id !== spec.chainId) throw new ClipError("This app is asking for a different network than the one it's connected to.", "cosmos/network-mismatch"); + return { method: "amino", spec, me, doc, signBytes: serializeSignDoc(doc) }; + } + case COSMOS_METHODS.signArbitrary: { + requireSigner(p.signer, me); + const data = bytesParam(p.data, "data"); + if (data.length === 0) throw bad(); + const doc = makeAdr36SignDoc(me, b64encode(data)); + return { method: "arbitrary", spec, me, doc, data, signBytes: serializeSignDoc(doc) }; + } + default: + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "cosmos/unsupported-method"); + } + } + + /* ------------------------------------------------------------------ decode */ + + async function decode(request: DappRequest, ctx: ChainContext): Promise { + const n = normalize(request, ctx); + const host = isWalletOrigin(request.origin) ? "Clip Wallet" : hostOf(request.origin); + const base = { requestId: request.id, networkId: request.networkId }; + + if (n.method === "arbitrary") { + const text = readableText(n.data); + const lines = text !== null ? [{ label: "Message", value: text }] : [{ label: "Message (not text)", value: `0x${hex(n.data)}` }]; + const warnings: Warning[] = text !== null ? [] : [{ level: "caution", code: "blind-signing", message: "This message isn't readable text. It can't move funds by itself, but only sign it if you trust the site." }]; + const t = msg("bg.req.signMessage", { host }); + return { ...base, title: t.fallback, titleMsg: t, lines, balanceChanges: [], simulated: false, blind: false, warnings }; + } + + if (n.method === "amino") { + const msgs = n.doc.msgs.map(fromAmino); + const d = describeMsgs(n.spec, msgs, n.me, host); + notSigner(msgs, n.me, d); + const fee = n.doc.fee.amount.map((c) => ({ denom: c.denom, amount: c.amount })); + if (n.doc.memo) d.lines.push({ label: "Memo", value: n.doc.memo }); + d.lines.push(feeLine(n.spec, fee)); + d.lines.push(sentBy(host)); + addFee(d, fee, n.me, n.doc.fee.granter, n.doc.fee.payer); + return { ...base, ...titleOf(d), lines: d.lines, balanceChanges: d.balanceChanges, fee: feeOf(n.spec, fee), simulated: false, blind: d.blind, warnings: d.warnings }; + } + + // direct + if (!n.body || !n.auth) { + const t = msg("bg.req.approveTxFor", { host }); + return { + ...base, + title: t.fallback, + titleMsg: t, + lines: [], + balanceChanges: [], + simulated: false, + blind: true, + warnings: [{ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }], + }; + } + const msgs = n.body.messages.map((a) => fromAny(a, n.spec.prefix)); + const d = describeMsgs(n.spec, msgs, n.me, host); + notSigner(msgs, n.me, d); + if (n.body.extensionOptions.length || n.auth.hasTip) { + d.blind = true; + d.warnings.unshift({ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }); + } + if (n.body.memo) d.lines.push({ label: "Memo", value: n.body.memo }); + if (n.body.timeoutHeight > 0n) { + const v = msg("bg.cosmos.block", { height: n.body.timeoutHeight.toString() }); + d.lines.push({ label: "Valid until", value: v.fallback, valueMsg: v }); + } + const fee = n.auth.fee.amount; + d.lines.push(feeLine(n.spec, fee)); + if (!n.broadcast) d.lines.push(sentBy(host)); + addFee(d, fee, n.me, n.auth.fee.granter, n.auth.fee.payer); + + let simulated = false; + if (doSimulate && !d.blind) { + try { + const tx = encodeTxRaw({ bodyBytes: b64decode(n.wire.bodyBytes), authInfoBytes: b64decode(n.wire.authInfoBytes), signatures: n.auth.signerInfos.map(() => new Uint8Array()) }); + await simulate(restFor(ctx), tx); + simulated = true; + } catch (e) { + const reason = e instanceof RestError ? e.message : ""; + if (/insufficient funds|insufficient balance/i.test(reason)) { + d.warnings.push({ level: "danger", code: "simulation-failed", message: "This is expected to fail and would still cost a fee." }); + } else { + d.warnings.push({ level: "caution", code: "simulation-failed", message: "We couldn't preview the exact result of this on the network. Check the details before you approve." }); + } + } + } + return { ...base, ...titleOf(d), lines: d.lines, balanceChanges: d.balanceChanges, fee: feeOf(n.spec, fee), simulated, blind: d.blind, warnings: d.warnings }; + } + + /** Refuses requests our key isn't a signer of (every message names someone else). */ + function notSigner(msgs: CosmosMsg[], me: string, d: Described): void { + if (d.blind) return; + const signers = msgs.map(signerOf); + if (signers.every((s) => s !== undefined && s !== me)) throw new ClipError("This transaction doesn't need your signature.", "cosmos/not-a-signer"); + } + + /** Fee paid by a granter or another payer: say so (our balance doesn't pay it). */ + function addFee(d: Described, _fee: readonly Coin[], me: string, granter?: string, payer?: string): void { + if (granter || (payer && payer !== me)) { + d.warnings.push({ level: "info", code: "network-matters", message: "Another account pays the network fee for this transaction." }); + } + } + + function titleOf(d: Described): { title: string; titleMsg?: DecodedRequest["titleMsg"] } { + return d.titleMsg ? { title: d.title, titleMsg: d.titleMsg } : { title: d.title }; + } + + /* ------------------------------------------------------------------ prepare / finalize */ + + async function prepare(request: DappRequest, ctx: ChainContext, approvalId: string): Promise { + const n = normalize(request, ctx); + return [{ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes: digestOf(n.spec, n.signBytes), approvalId }]; + } + + function stdSignature(ctx: ChainContext, sig: Uint8Array): StdSignature { + return { pub_key: { type: AMINO_PUBKEY, value: b64encode(pubOf(ctx)) }, signature: b64encode(sig) }; + } + + async function finalize(request: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const n = normalize(request, ctx); + const sig = signatures[0]; + if (!sig || signatures.length !== 1) throw new ClipError("Some signatures are missing. Nothing was sent.", "cosmos/missing-signature"); + if (sig.scheme !== "ecdsa-secp256k1" || sig.bytes.length !== 64 || !secp256k1.verify(sig.bytes, digestOf(n.spec, n.signBytes), pubOf(ctx), { prehash: false, lowS: true })) { + throw new ClipError("The signature didn't match. Nothing was sent.", "cosmos/bad-signature"); + } + const signature = stdSignature(ctx, sig.bytes); + if (n.method === "arbitrary") return request.method === COSMOS_METHODS.signAmino ? { signed: n.doc, signature } : signature; + if (n.method === "amino") return { signed: n.doc, signature }; + if (!n.broadcast) return { signed: n.wire, signature }; + + const tx = encodeTxRaw({ bodyBytes: b64decode(n.wire.bodyBytes), authInfoBytes: b64decode(n.wire.authInfoBytes), signatures: [sig.bytes] }); + return send(restFor(ctx), n.spec, tx); + } + + async function send(rest: Rest, spec: CosmosChainSpec, tx: Uint8Array): Promise<{ txhash: string; status: "success" | "pending"; height?: string }> { + let r; + try { + r = await broadcastSync(rest, tx); + } catch (e) { + if (e instanceof RestError) throw new ClipError(plainCosmosError(-1, undefined, e.message, spec.native.symbol), "cosmos/send-failed", e); + throw e; + } + if (r.code !== 0) throw new ClipError(plainCosmosError(r.code, r.codespace, r.raw_log, spec.native.symbol), "cosmos/send-failed"); + const txhash = r.txhash.toUpperCase(); + for (let k = 0; k < pollAttempts; k++) { + if (pollMs > 0) await sleep(pollMs); + const t = await getTx(rest, txhash).catch(() => null); + if (!t) continue; + if (t.code !== 0) throw new ClipError(plainCosmosError(t.code, t.codespace, t.raw_log, spec.native.symbol), "cosmos/send-failed"); + return { txhash, status: "success", ...(t.height ? { height: t.height } : {}) }; + } + return { txhash, status: "pending" }; + } + + /* ------------------------------------------------------------------ read-only (no approval) */ + + async function read(method: CosmosReadMethod, params: unknown, ctx: ChainContext): Promise { + const spec = specFor(ctx.network.id); + if (!spec) throw new ClipError("This app is asking for a different network than the one it's connected to.", "cosmos/network-mismatch"); + const p = isObj(params) ? params : {}; + if (method === COSMOS_METHODS.sendTx) { + const tx = bytesParam(p.tx, "tx"); + try { + decodeTxRaw(tx); + } catch { + throw new ClipError("This transaction can't be read.", "cosmos/bad-transaction"); + } + const mode = p.mode === "async" ? "BROADCAST_MODE_ASYNC" : "BROADCAST_MODE_SYNC"; + const rest = restFor(ctx); + let r; + try { + r = await rest.post<{ tx_response?: { code: number; codespace?: string; raw_log?: string; txhash: string } }>("/cosmos/tx/v1beta1/txs", { tx_bytes: b64encode(tx), mode }); + } catch (e) { + if (e instanceof RestError) throw new ClipError(plainCosmosError(-1, undefined, e.message, spec.native.symbol), "cosmos/send-failed", e); + throw e; + } + const t = r.tx_response; + if (!t) throw new ClipError("The network rejected this. Nothing was sent.", "cosmos/send-failed"); + if (t.code !== 0) throw new ClipError(plainCosmosError(t.code, t.codespace, t.raw_log, spec.native.symbol), "cosmos/send-failed"); + return { txhash: t.txhash.toUpperCase() }; + } + if (method === COSMOS_METHODS.verifyArbitrary) { + return verifyArbitrary(spec, p.signer, p.data, p.signature); + } + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "cosmos/unsupported-method"); + } + + /** Keplr verifyArbitrary: the key in the signature must be the signer's, and verify over the ADR-36 doc. */ + function verifyArbitrary(spec: CosmosChainSpec, signer: unknown, data: unknown, signature: unknown): boolean { + if (typeof signer !== "string" || !isObj(signature) || !isObj(signature.pub_key) || typeof signature.signature !== "string") return false; + if (signature.pub_key.type !== AMINO_PUBKEY || typeof signature.pub_key.value !== "string") return false; + try { + const pub = compressedKey(b64decode(signature.pub_key.value)); + const d = decodeBech32(signer); + if (!d || d.prefix !== spec.prefix || !equalBytes(d.data, addressBytes(pub, spec.keyKind))) return false; + const bytes = bytesParam(data, "data"); + const doc = makeAdr36SignDoc(signer, b64encode(bytes)); + const sig = b64decode(signature.signature); + return sig.length === 64 && secp256k1.verify(sig, digestOf(spec, serializeSignDoc(doc)), pub, { prehash: false, lowS: false }); + } catch { + return false; + } + } + + /* ------------------------------------------------------------------ balances */ + + async function getBalances(ctx: ChainContext): Promise { + const spec = specFor(ctx.network.id); + if (!spec) throw new ClipError("That network isn't available in this wallet.", "cosmos/network-mismatch"); + const me = addressOnSpec(spec, pubOf(ctx)); + const all = await getAllBalances(restFor(ctx), me); + const of = (denom: string) => all.find((b) => b.denom === denom)?.amount ?? "0"; + const out: TokenBalance[] = [{ asset: nativeAsset(spec), amount: of(spec.native.denom) }]; + for (const t of spec.tokens ?? []) { + const amount = of(t.denom); + if (amount !== "0" || t.alwaysShow) out.push({ asset: assetOf(spec, t.denom), amount }); + } + return out; + } + + /* ------------------------------------------------------------------ transfers */ + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + const spec = specFor(ctx.network.id); + if (!spec) throw new ClipError("That network isn't available in this wallet.", "cosmos/network-mismatch"); + const pub = pubOf(ctx); + const me = addressOnSpec(spec, pub); + const to = p.to.trim(); + const dec = decodeBech32(to); + if (!dec || (dec.data.length !== 20 && dec.data.length !== 32)) throw new ClipError("That address isn't valid. Check it and try again.", "cosmos/bad-address"); + if (dec.prefix !== spec.prefix) { + throw new ClipError(msg("bg.cosmos.wrongPrefix", { prefix: `${spec.prefix}1…`, network: spec.name }), "cosmos/bad-address"); + } + if (to === me) throw new ClipError("That's your own address.", "cosmos/self-transfer"); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "cosmos/bad-amount"); + const denom = p.asset.address ?? (p.asset.key === spec.native.key ? spec.native.denom : undefined); + if (!denom || p.asset.networkId !== ctx.network.id) throw new ClipError("That token couldn't be found.", "cosmos/unknown-asset"); + const amount = BigInt(p.amount); + + const rest = restFor(ctx); + const account = await getAccount(rest, me); + if (!account) throw new ClipError(msg("bg.cosmos.noAccount", { network: spec.name, symbol: spec.native.symbol }), "cosmos/insufficient-funds"); + const balances = await getAllBalances(rest, me); + const bal = (d: string) => BigInt(balances.find((b) => b.denom === d)?.amount ?? "0"); + if (bal(denom) < amount) throw new ClipError(msg("bg.err.notEnough", { symbol: assetOf(spec, denom).symbol }), "cosmos/insufficient-funds"); + + const message = + spec.sendMsg === "thorchain" + ? { typeUrl: TYPE.thorMsgSend, value: encodeThorMsgSend({ fromAddress: addressBytes(pub, spec.keyKind), toAddress: dec.data, amount: [{ denom, amount: p.amount }] }) } + : { typeUrl: TYPE.msgSend, value: encodeMsgSend({ fromAddress: me, toAddress: to, amount: [{ denom, amount: p.amount }] }) }; + const bodyBytes = encodeTxBody({ messages: [message], memo: "" }); + const signer = { publicKey: { typeUrl: spec.pubKeyTypeUrl, value: encodePubKey(pub) }, mode: SIGN_MODE_DIRECT, sequence: account.sequence }; + + // Gas and fee: simulate with no fee coins, then gas × adjustment and the network's fee model. + const simTx = encodeTxRaw({ bodyBytes, authInfoBytes: encodeAuthInfo({ signerInfos: [signer], fee: { amount: [], gasLimit: 0n } }), signatures: [new Uint8Array()] }); + const failed = (e: unknown): never => { + if (e instanceof RestError) throw new ClipError(plainCosmosError(-1, undefined, e.message, assetOf(spec, denom).symbol), "cosmos/simulation-failed", e); + throw e; + }; + let gasLimit: bigint; + let fee: Coin[]; + if (spec.feeModel === "provenance-flatfee") { + // Provenance x/flatfees: the fee is per message type, not gas × price (x/flatfees/spec/01_concepts.md). + const r = await rest + .post<{ total_fees?: Coin[]; estimated_gas?: string }>("/provenance/tx/v1/calculate_flat_fee", { tx_bytes: b64encode(simTx), gas_adjustment: gasAdj }) + .catch(failed); + if (!r.total_fees || !/^\d+$/.test(r.estimated_gas ?? "")) throw new ClipError("We couldn't work out the network fee. Try again.", "cosmos/simulation-failed"); + fee = r.total_fees.filter((c) => /^\d+$/.test(c.amount) && c.amount !== "0"); + gasLimit = maxBig(BigInt(r.estimated_gas!), spec.minGas ?? 0n); + } else { + const gasUsed = await simulate(rest, simTx).catch(failed); + gasLimit = maxBig(BigInt(Math.ceil(Number(gasUsed) * gasAdj)), spec.minGas ?? 0n); + fee = await feeFor(spec, rest, gasLimit, bal); + } + const feeTotal = (d: string) => fee.filter((c) => c.denom === d).reduce((a, c) => a + BigInt(c.amount), 0n); + const fixed = spec.feeModel === "fixed-native" && spec.fixedFee?.denom === denom ? await nativeTxFee(spec, rest) : 0n; + if (bal(denom) < amount + feeTotal(denom) + fixed) { + throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: assetOf(spec, denom).symbol }), "cosmos/insufficient-funds"); + } + for (const c of fee) if (c.denom !== denom && bal(c.denom) < BigInt(c.amount)) throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: assetOf(spec, c.denom).symbol }), "cosmos/insufficient-funds"); + + const authInfoBytes = encodeAuthInfo({ signerInfos: [signer], fee: { amount: fee, gasLimit } }); + const signDoc: WireSignDoc = { bodyBytes: b64encode(bodyBytes), authInfoBytes: b64encode(authInfoBytes), chainId: spec.chainId, accountNumber: account.accountNumber.toString() }; + return { id: randomId(), origin: WALLET_ORIGIN, via: "injected", family, networkId: ctx.network.id, method: COSMOS_METHODS.signAndBroadcast, params: { signDoc } }; + } + + /** The fee in the first fee token the account can pay with (gas limit × gas price, rounded up). */ + async function feeFor(spec: CosmosChainSpec, rest: Rest, gasLimit: bigint, bal: (denom: string) => bigint): Promise { + if (spec.feeModel === "fixed-native") return []; + const options: Coin[] = []; + for (const t of spec.feeTokens) { + let price = parseDecimal(t.average); + if (t.denom === spec.native.denom && (spec.feeModel === "osmosis-txfees" || spec.feeModel === "initia-dynamic")) { + const live = await liveGasPrice(spec, rest); + if (live) price = live; + } + options.push({ denom: t.denom, amount: mulCeil(gasLimit, price.num, price.den).toString() }); + } + const pick = options.find((c) => bal(c.denom) >= BigInt(c.amount)) ?? options[0]; + return pick ? [pick] : []; + } + + /** + * The chain's current native gas price with headroom, or null (then the registry price is used): + * - Osmosis x/txfees EIP-1559 base fee × 1.65 (osmosis-frontend packages/tx/src/gas.ts). + * - Initia x/dynamicfee price (/initia/tx/v1/gas_prices/{denom}) × 1.05 (chainapsis oko cosmos_selectable_fees.ts). + */ + async function liveGasPrice(spec: CosmosChainSpec, rest: Rest): Promise<{ num: bigint; den: bigint } | null> { + let value: string | undefined; + let mul: [bigint, bigint]; + if (spec.feeModel === "osmosis-txfees") { + value = (await rest.get<{ base_fee?: string }>("/osmosis/txfees/v1beta1/cur_eip_base_fee").catch(() => null))?.base_fee; + mul = [165n, 100n]; + } else { + value = (await rest.get<{ gas_price?: { amount?: string } }>(`/initia/tx/v1/gas_prices/${spec.native.denom}`).catch(() => null))?.gas_price?.amount; + mul = [105n, 100n]; + } + if (!value || !/^\d+(\.\d+)?$/.test(value)) return null; + const p = parseDecimal(value); + return p.num === 0n ? null : { num: p.num * mul[0], den: p.den * mul[1] }; + } + + /** THORChain's fixed native fee (/thorchain/network native_tx_fee_rune), else the spec's fallback. */ + async function nativeTxFee(spec: CosmosChainSpec, rest: Rest): Promise { + const r = await rest.get<{ native_tx_fee_rune?: string }>("/thorchain/network").catch(() => null); + return r?.native_tx_fee_rune && /^\d+$/.test(r.native_tx_fee_rune) ? BigInt(r.native_tx_fee_rune) : BigInt(spec.fixedFee?.amount ?? "0"); + } + + async function receiveAddress(ctx: ChainContext): Promise { + const spec = specFor(ctx.network.id); + if (!spec) throw new ClipError("That network isn't available in this wallet.", "cosmos/network-mismatch"); + return addressOnSpec(spec, pubOf(ctx)); + } + + const networks = networksOfFamily(family); + + return { + family, + curve: "secp256k1", + derivationPath: (index: number) => derivationPathOf(family, index), + networks, + addressFromPublicKey(publicKey: Uint8Array, network: Network): string { + const spec = specFor(network.id); + if (!spec) throw new Error(`${network.id} isn't a ${family} network`); + return addressOnSpec(spec, publicKey); + }, + addressOn(publicKeyHex: string, network: Network | string): string { + const spec = specFor(typeof network === "string" ? network : network.id); + if (!spec) throw new Error(`not a ${family} network`); + return addressOnSpec(spec, fromHex(publicKeyHex)); + }, + isAddress: (value: string) => isAccountAddress(value, prefixes), + networksForAddress(value: string, candidates: Network[]): Network[] { + const d = decodeBech32(value); + if (!d || !isAccountAddress(value, prefixes)) return []; + return candidates.filter((c) => specFor(c.id)?.prefix === d.prefix); + }, + getBalances, + getNfts: async (): Promise => [], + decode, + prepare, + finalize, + buildTransfer, + receiveAddress, + read, + }; + +} + +function sentBy(host: string): { label: string; value: string; valueMsg: ReturnType } { + const v = msg("bg.cosmos.appGetsSigned", { host }); + return { label: "Sent by", value: v.fallback, valueMsg: v }; +} + +const maxBig = (a: bigint, b: bigint) => (a > b ? a : b); + +function decodePubKeySafe(b: Uint8Array): Uint8Array { + try { + return decodePubKey(b); + } catch { + return new Uint8Array(); + } +} + +function safeEq(a: Uint8Array, b: Uint8Array): boolean { + if (a.length === 33) return equalBytes(a, b); + try { + return equalBytes(compressedKey(a), b); + } catch { + return false; + } +} + +/** UTF-8 text without control characters (other than line breaks and tabs), else null. */ +function readableText(b: Uint8Array): string | null { + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(b); + return /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s) ? null : s; + } catch { + return null; + } +} + +export { amountText }; diff --git a/packages/chains-cosmos/src/msgs.ts b/packages/chains-cosmos/src/msgs.ts new file mode 100644 index 00000000..17852933 --- /dev/null +++ b/packages/chains-cosmos/src/msgs.ts @@ -0,0 +1,270 @@ +import type { AminoMsg } from "./amino.js"; +import { bech32Address } from "./address.js"; +import { + type Any, + type Coin, + ProtoError, + TYPE, + decodeGenericAuthorization, + decodeMsgBeginRedelegate, + decodeMsgDelegate, + decodeMsgExecuteContract, + decodeMsgGrant, + decodeMsgGrantAllowance, + decodeMsgRevoke, + decodeMsgSend, + decodeMsgTransfer, + decodeMsgWithdrawReward, + decodeThorMsgSend, + Msg as ProtoMsg, + decodeCoin, +} from "./proto.js"; +import { isObj } from "./util.js"; + +/** + * One understood message, from either signing mode. SIGN_MODE_DIRECT gives protobuf Anys (type URL), Amino JSON gives + * { type, value } with the SDK's amino names (cosmos-sdk x//types/codec.go `legacy.RegisterAminoMsg`). + * Initia replaces x/staking with "mstaking" (amounts are Coins) and has its own distribution module + * (github.com/initia-labs/initia proto/initia/mstaking/v1/tx.proto, proto/initia/distribution/v1/tx.proto). + */ +export type CosmosMsg = + | { kind: "send"; from: string; to: string; amount: Coin[] } + | { kind: "ibc-transfer"; sender: string; receiver: string; token: Coin; port: string; channel: string; memo: string; timeoutTimestamp: bigint } + | { kind: "delegate"; delegator: string; validator: string; amount: Coin[] } + | { kind: "undelegate"; delegator: string; validator: string; amount: Coin[] } + | { kind: "redelegate"; delegator: string; from: string; to: string; amount: Coin[] } + | { kind: "withdraw-rewards"; delegator: string; validator: string } + | { kind: "execute"; sender: string; contract: string; msg: unknown; msgText: string; funds: Coin[] } + | { kind: "authz-grant"; granter: string; grantee: string; authorization: string; expiration?: bigint } + | { kind: "authz-revoke"; granter: string; grantee: string; msgTypeUrl: string } + | { kind: "feegrant"; granter: string; grantee: string; allowance: string; spendLimit: Coin[] } + | { kind: "feegrant-revoke"; granter: string; grantee: string } + | { kind: "unknown"; type: string }; + +const INITIA = { + delegate: "/initia.mstaking.v1.MsgDelegate", + undelegate: "/initia.mstaking.v1.MsgUndelegate", + redelegate: "/initia.mstaking.v1.MsgBeginRedelegate", + withdraw: "/initia.distribution.v1.MsgWithdrawDelegatorReward", +} as const; + +/** MsgDelegate-shaped with `repeated Coin amount = 3` (initia mstaking). */ +function delegateCoins(b: Uint8Array): { delegator: string; validator: string; amount: Coin[] } { + const m = new ProtoMsg(b); + return { delegator: m.string(1), validator: m.string(2), amount: m.repeated(3).map(decodeCoin) }; +} + +function redelegateCoins(b: Uint8Array): { delegator: string; from: string; to: string; amount: Coin[] } { + const m = new ProtoMsg(b); + return { delegator: m.string(1), from: m.string(2), to: m.string(3), amount: m.repeated(4).map(decodeCoin) }; +} + +/** Decodes the authorization Any of an authz grant into words-for-the-UI input. */ +function authorizationOf(a: Any | undefined): string { + if (!a) return "unknown"; + if (a.typeUrl === TYPE.genericAuthorization) return decodeGenericAuthorization(a.value) || "unknown"; + return a.typeUrl; +} + +function allowanceOf(a: Any | undefined): { allowance: string; spendLimit: Coin[] } { + if (!a) return { allowance: "unknown", spendLimit: [] }; + if (a.typeUrl === TYPE.basicAllowance) return { allowance: a.typeUrl, spendLimit: new ProtoMsg(a.value).repeated(1).map(decodeCoin) }; + return { allowance: a.typeUrl, spendLimit: [] }; +} + +function parseJson(bytes: Uint8Array): { msg: unknown; text: string } { + const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes); + try { + return { msg: JSON.parse(text), text }; + } catch { + return { msg: null, text }; + } +} + +/** SIGN_MODE_DIRECT message → CosmosMsg. `prefix` turns THORChain's raw-bytes addresses into thor1… */ +export function fromAny(a: Any, prefix: string): CosmosMsg { + try { + switch (a.typeUrl) { + case TYPE.msgSend: { + const m = decodeMsgSend(a.value); + return { kind: "send", from: m.fromAddress, to: m.toAddress, amount: m.amount }; + } + case TYPE.thorMsgSend: { + const m = decodeThorMsgSend(a.value); + if (m.fromAddress.length !== 20 || m.toAddress.length !== 20) return { kind: "unknown", type: a.typeUrl }; + return { kind: "send", from: bech32Address(prefix, m.fromAddress), to: bech32Address(prefix, m.toAddress), amount: m.amount }; + } + case TYPE.msgTransfer: { + const m = decodeMsgTransfer(a.value); + return { kind: "ibc-transfer", sender: m.sender, receiver: m.receiver, token: m.token, port: m.sourcePort, channel: m.sourceChannel, memo: m.memo, timeoutTimestamp: m.timeoutTimestamp }; + } + case TYPE.msgDelegate: + case TYPE.msgUndelegate: { + const m = decodeMsgDelegate(a.value); + return { kind: a.typeUrl === TYPE.msgDelegate ? "delegate" : "undelegate", delegator: m.delegator, validator: m.validator, amount: [m.amount] }; + } + case INITIA.delegate: + case INITIA.undelegate: { + const m = delegateCoins(a.value); + return { kind: a.typeUrl === INITIA.delegate ? "delegate" : "undelegate", ...m }; + } + case TYPE.msgBeginRedelegate: { + const m = decodeMsgBeginRedelegate(a.value); + return { kind: "redelegate", delegator: m.delegator, from: m.from, to: m.to, amount: [m.amount] }; + } + case INITIA.redelegate: + return { kind: "redelegate", ...redelegateCoins(a.value) }; + case TYPE.msgWithdrawReward: + case INITIA.withdraw: { + const m = decodeMsgWithdrawReward(a.value); + return { kind: "withdraw-rewards", delegator: m.delegator, validator: m.validator }; + } + case TYPE.msgExecuteContract: { + const m = decodeMsgExecuteContract(a.value); + const j = parseJson(m.msg); + return { kind: "execute", sender: m.sender, contract: m.contract, msg: j.msg, msgText: j.text, funds: m.funds }; + } + case TYPE.msgGrant: { + const m = decodeMsgGrant(a.value); + const out: CosmosMsg = { kind: "authz-grant", granter: m.granter, grantee: m.grantee, authorization: authorizationOf(m.authorization) }; + if (m.expiration !== undefined) out.expiration = m.expiration; + return out; + } + case TYPE.msgRevoke: { + const m = decodeMsgRevoke(a.value); + return { kind: "authz-revoke", granter: m.granter, grantee: m.grantee, msgTypeUrl: m.msgTypeUrl }; + } + case TYPE.msgGrantAllowance: { + const m = decodeMsgGrantAllowance(a.value); + return { kind: "feegrant", granter: m.granter, grantee: m.grantee, ...allowanceOf(m.allowance) }; + } + case TYPE.msgRevokeAllowance: { + const m = decodeMsgRevoke(a.value); + return { kind: "feegrant-revoke", granter: m.granter, grantee: m.grantee }; + } + } + } catch (e) { + if (e instanceof ProtoError) return { kind: "unknown", type: a.typeUrl }; + throw e; + } + return { kind: "unknown", type: a.typeUrl }; +} + +/* ------------------------------------------------------------------ amino */ + +const str = (v: unknown): string => (typeof v === "string" ? v : ""); + +function coins(v: unknown): Coin[] | null { + if (!Array.isArray(v)) return null; + const out: Coin[] = []; + for (const c of v) { + if (!isObj(c) || typeof c.denom !== "string" || typeof c.amount !== "string" || !/^\d+$/.test(c.amount)) return null; + out.push({ denom: c.denom, amount: c.amount }); + } + return out; +} + +function coin(v: unknown): Coin | null { + const c = coins([v]); + return c ? c[0]! : null; +} + +/** + * Amino JSON message → CosmosMsg. Names from cosmos-sdk codecs (bank "cosmos-sdk/MsgSend", staking + * "cosmos-sdk/MsgDelegate" / "cosmos-sdk/MsgUndelegate" / "cosmos-sdk/MsgBeginRedelegate", distribution + * "cosmos-sdk/MsgWithdrawDelegationReward", authz "cosmos-sdk/MsgGrant" / "cosmos-sdk/MsgRevoke", feegrant + * "cosmos-sdk/MsgGrantAllowance" / "cosmos-sdk/MsgRevokeAllowance"), ibc-go "cosmos-sdk/MsgTransfer", wasmd + * "wasm/MsgExecuteContract" (msg is inline JSON), THORChain "thorchain/MsgSend" (bech32 strings in amino JSON), + * Initia "mstaking/MsgDelegate" / "mstaking/MsgUndelegate" / "mstaking/MsgBeginRedelegate" and + * "distribution/MsgWithdrawDelegatorReward". + */ +export function fromAmino(m: AminoMsg): CosmosMsg { + const v = isObj(m.value) ? m.value : {}; + const unknown: CosmosMsg = { kind: "unknown", type: m.type }; + switch (m.type) { + case "cosmos-sdk/MsgSend": + case "thorchain/MsgSend": + case "bank/MsgSend": { + const amount = coins(v.amount); + if (!amount) return unknown; + return { kind: "send", from: str(v.from_address), to: str(v.to_address), amount }; + } + case "cosmos-sdk/MsgTransfer": { + const token = coin(v.token); + if (!token) return unknown; + return { + kind: "ibc-transfer", + sender: str(v.sender), + receiver: str(v.receiver), + token, + port: str(v.source_port), + channel: str(v.source_channel), + memo: str(v.memo), + timeoutTimestamp: /^\d+$/.test(str(v.timeout_timestamp)) ? BigInt(str(v.timeout_timestamp)) : 0n, + }; + } + case "cosmos-sdk/MsgDelegate": + case "cosmos-sdk/MsgUndelegate": + case "mstaking/MsgDelegate": + case "mstaking/MsgUndelegate": { + const amount = Array.isArray(v.amount) ? coins(v.amount) : coin(v.amount) ? [coin(v.amount)!] : null; + if (!amount) return unknown; + return { kind: /Undelegate$/.test(m.type) ? "undelegate" : "delegate", delegator: str(v.delegator_address), validator: str(v.validator_address), amount }; + } + case "cosmos-sdk/MsgBeginRedelegate": + case "mstaking/MsgBeginRedelegate": { + const amount = Array.isArray(v.amount) ? coins(v.amount) : coin(v.amount) ? [coin(v.amount)!] : null; + if (!amount) return unknown; + return { kind: "redelegate", delegator: str(v.delegator_address), from: str(v.validator_src_address), to: str(v.validator_dst_address), amount }; + } + case "cosmos-sdk/MsgWithdrawDelegationReward": + case "distribution/MsgWithdrawDelegatorReward": + return { kind: "withdraw-rewards", delegator: str(v.delegator_address), validator: str(v.validator_address) }; + case "wasm/MsgExecuteContract": { + const funds = coins(v.funds ?? []); + if (!funds) return unknown; + return { kind: "execute", sender: str(v.sender), contract: str(v.contract), msg: v.msg ?? null, msgText: JSON.stringify(v.msg ?? null), funds }; + } + case "cosmos-sdk/MsgGrant": { + const g = isObj(v.grant) ? v.grant : {}; + const a = isObj(g.authorization) ? g.authorization : {}; + const av = isObj(a.value) ? a.value : {}; + const authorization = a.type === "cosmos-sdk/GenericAuthorization" ? str(av.msg) || "unknown" : str(a.type) || "unknown"; + return { kind: "authz-grant", granter: str(v.granter), grantee: str(v.grantee), authorization }; + } + case "cosmos-sdk/MsgRevoke": + return { kind: "authz-revoke", granter: str(v.granter), grantee: str(v.grantee), msgTypeUrl: str(v.msg_type_url) }; + case "cosmos-sdk/MsgGrantAllowance": { + const a = isObj(v.allowance) ? v.allowance : {}; + const av = isObj(a.value) ? a.value : {}; + return { kind: "feegrant", granter: str(v.granter), grantee: str(v.grantee), allowance: str(a.type) || "unknown", spendLimit: coins(av.spend_limit ?? []) ?? [] }; + } + case "cosmos-sdk/MsgRevokeAllowance": + return { kind: "feegrant-revoke", granter: str(v.granter), grantee: str(v.grantee) }; + } + return unknown; +} + +/** The account a message needs a signature from (its first signer, cosmos.msg.v1.signer), when known. */ +export function signerOf(m: CosmosMsg): string | undefined { + switch (m.kind) { + case "send": + return m.from; + case "ibc-transfer": + return m.sender; + case "delegate": + case "undelegate": + case "redelegate": + case "withdraw-rewards": + return m.delegator; + case "execute": + return m.sender; + case "authz-grant": + case "authz-revoke": + case "feegrant": + case "feegrant-revoke": + return m.granter; + default: + return undefined; + } +} diff --git a/packages/chains-cosmos/src/networks.ts b/packages/chains-cosmos/src/networks.ts new file mode 100644 index 00000000..9daf9cca --- /dev/null +++ b/packages/chains-cosmos/src/networks.ts @@ -0,0 +1,375 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * Cosmos SDK networks Clip Wallet ships, by key family (each ecosystem's wallets derive at the chain's own SLIP-44 + * coin type, so each family is its own vault key): + * - "cosmos" (m/44'/118'/0'/0/i): Osmosis, dYdX, ZIGChain + * - "provenance" (m/44'/505'/0'/0/i): Provenance (pb… mainnet, tp… testnet) + * - "thorchain" (m/44'/931'/0'/0/i): THORChain (mainnet only: its stagenet runs on real funds) + * - "initia" (m/44'/60'/0'/0/i, ethsecp256k1): Initia L1 + * + * Network ids are CAIP-2 in the `cosmos` namespace: "cosmos:" + * (https://github.com/ChainAgnostic/namespaces/blob/main/cosmos/caip2.md). + * + * Chain facts (bech32 prefix, slip44, denoms, decimals, gas prices, endpoints) come from github.com/cosmos/chain-registry + * (/chain.json, assetlist.json; testnets/…) and, for Initia, github.com/initia-labs/initia-registry. Every REST + * endpoint here answered GET /cosmos/base/tendermint/v1beta1/node_info with the right `network` (checked with curl, + * Oct 2026) and allows any origin (the extension background has no host permission for them), except + * thorchain.ibs.team, which sends the CORS header twice (browsers refuse that; it stays as a last resort). No endpoint + * needs a key. Only one public REST endpoint was found for pio-testnet-1 and initiation-2. + */ + +export type CosmosFamily = "cosmos" | "provenance" | "thorchain" | "initia"; +export const COSMOS_FAMILIES: readonly CosmosFamily[] = ["cosmos", "provenance", "thorchain", "initia"]; + +export interface FeeToken { + denom: string; + /** Gas prices (base units per gas), decimal strings as chain-registry gives them. */ + low: string; + average: string; + high: string; +} + +export interface KnownToken { + denom: string; + key: string; + symbol: string; + name: string; + decimals: number; + /** Listed in getBalances even at 0 (dYdX: USDC is the collateral and a fee asset). */ + alwaysShow?: boolean; +} + +export interface CosmosChainSpec { + chainId: string; + family: CosmosFamily; + name: string; + testnet: boolean; + prefix: string; + /** SLIP-44 coin type of the family's derivation path. */ + coinType: number; + keyKind: "secp256k1" | "ethsecp256k1"; + /** Any type URL of the account's public key in AuthInfo.signer_infos. */ + pubKeyTypeUrl: string; + /** REST (LCD) endpoints, tried in order. */ + rest: string[]; + /** CometBFT RPC endpoints (not used by the module; listed for dapps and the provider's chain info). */ + rpc: string[]; + explorer: string; + /** Explorer page of a transaction: `{hash}` is replaced. */ + explorerTx: string; + native: { denom: string; symbol: string; name: string; decimals: number; key: string }; + tokens?: KnownToken[]; + /** Fee tokens in order of preference, with gas prices. */ + feeTokens: FeeToken[]; + /** + * "gas-price": fee = gas limit × gas price of the first fee token the account can pay. + * "osmosis-txfees": OSMO's gas price is the txfees module's EIP-1559 base fee × 1.65 (osmosis-frontend + * packages/tx/src/gas.ts defaultBaseFeeMultiplier). + * "initia-dynamic": INIT's gas price is x/dynamicfee's current price (/initia/tx/v1/gas_prices/{denom}) × 1.05. + * "provenance-flatfee": x/flatfees charges a flat fee per message type; /provenance/tx/v1/calculate_flat_fee gives + * the fee and the gas (gas price 1nhash, as Provenance asks every client to use). + * "fixed-native": THORChain charges a fixed native fee from the balance; the tx's Fee carries no coins. + */ + feeModel: "gas-price" | "osmosis-txfees" | "initia-dynamic" | "provenance-flatfee" | "fixed-native"; + /** THORChain: the fixed native fee taken by the chain (read live from /thorchain/network native_tx_fee_rune; this is the fallback). */ + fixedFee?: { denom: string; amount: string }; + /** Smallest gas limit used for a transfer (THORChain: xchainjs's 6 000 000; gas isn't charged there). */ + minGas?: bigint; + /** "bank": cosmos.bank.v1beta1.MsgSend. "thorchain": types.MsgSend (bank sends are disabled on THORChain). */ + sendMsg: "bank" | "thorchain"; +} + +const SECP = "/cosmos.crypto.secp256k1.PubKey"; +/** initia crypto/ethsecp256k1 (proto/initia/crypto/v1beta1/ethsecp256k1/keys.proto). */ +const ETHSECP_INITIA = "/initia.crypto.v1beta1.ethsecp256k1.PubKey"; + +const usdc = (denom: string, alwaysShow = false): KnownToken => ({ denom, key: "usdc", symbol: "USDC", name: "USD Coin", decimals: 6, ...(alwaysShow ? { alwaysShow } : {}) }); + +export const COSMOS_CHAINS: readonly CosmosChainSpec[] = [ + /* ---------------------------------------------------------------- cosmos (118) */ + { + chainId: "osmo-test-5", + family: "cosmos", + name: "Osmosis Testnet", + testnet: true, + prefix: "osmo", + coinType: 118, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://lcd.osmotest5.osmosis.zone", "https://lcd.testnet.osmosis.zone"], + rpc: ["https://rpc.osmotest5.osmosis.zone", "https://rpc.testnet.osmosis.zone"], + explorer: "https://www.mintscan.io/osmosis-testnet", + explorerTx: "https://www.mintscan.io/osmosis-testnet/tx/{hash}", + native: { denom: "uosmo", symbol: "OSMO", name: "Osmosis", decimals: 6, key: "osmo" }, + // Circle's testnet USDC from Noble grand-1 (transfer/channel-4280). + tokens: [usdc("ibc/DE6792CF9E521F6AD6E9A4BDF6225C9571A3B74ACC0A529F92BC5122A39D2E58")], + feeTokens: [{ denom: "uosmo", low: "0.0025", average: "0.025", high: "0.04" }], + feeModel: "osmosis-txfees", + sendMsg: "bank", + }, + { + chainId: "osmosis-1", + family: "cosmos", + name: "Osmosis", + testnet: false, + prefix: "osmo", + coinType: 118, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://lcd.osmosis.zone", "https://osmosis-rest.publicnode.com", "https://osmosis-api.polkachu.com"], + rpc: ["https://rpc.osmosis.zone", "https://osmosis-rpc.publicnode.com"], + explorer: "https://www.mintscan.io/osmosis", + explorerTx: "https://www.mintscan.io/osmosis/tx/{hash}", + native: { denom: "uosmo", symbol: "OSMO", name: "Osmosis", decimals: 6, key: "osmo" }, + // Circle USDC from Noble (channel-750 on Osmosis), the USDC Osmosis lists as "USDC". + tokens: [usdc("ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4")], + feeTokens: [{ denom: "uosmo", low: "0.03", average: "0.1", high: "0.16" }], + feeModel: "osmosis-txfees", + sendMsg: "bank", + }, + { + chainId: "dydx-testnet-4", + family: "cosmos", + name: "dYdX Testnet", + testnet: true, + prefix: "dydx", + coinType: 118, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://dydx-testnet-api.polkachu.com", "https://test-dydx-rest.kingnodes.com"], + rpc: ["https://dydx-testnet-rpc.polkachu.com", "https://test-dydx-rpc.kingnodes.com"], + explorer: "https://www.mintscan.io/dydx-testnet", + explorerTx: "https://www.mintscan.io/dydx-testnet/tx/{hash}", + native: { denom: "adv4tnt", symbol: "DV4TNT", name: "dYdX testnet token", decimals: 18, key: "dydx" }, + tokens: [usdc("ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", true)], + feeTokens: [ + { denom: "adv4tnt", low: "25000000000", average: "25000000000", high: "50000000000" }, + { denom: "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", low: "0.025", average: "0.025", high: "0.03" }, + ], + feeModel: "gas-price", + sendMsg: "bank", + }, + { + chainId: "dydx-mainnet-1", + family: "cosmos", + name: "dYdX", + testnet: false, + prefix: "dydx", + coinType: 118, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://dydx-rest.publicnode.com", "https://dydx-api.polkachu.com"], + rpc: ["https://dydx-rpc.publicnode.com", "https://dydx-rpc.polkachu.com"], + explorer: "https://www.mintscan.io/dydx", + explorerTx: "https://www.mintscan.io/dydx/tx/{hash}", + native: { denom: "adydx", symbol: "DYDX", name: "dYdX", decimals: 18, key: "dydx" }, + tokens: [usdc("ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", true)], + feeTokens: [ + { denom: "adydx", low: "12500000000", average: "12500000000", high: "20000000000" }, + { denom: "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", low: "0.025", average: "0.025", high: "0.03" }, + ], + feeModel: "gas-price", + sendMsg: "bank", + }, + { + chainId: "zig-test-2", + family: "cosmos", + name: "ZIGChain Testnet", + testnet: true, + prefix: "zig", + coinType: 118, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://zigchain-testnet-api.polkachu.com", "https://public-zigchain-testnet-lcd.numia.xyz", "https://testnet-api.zigchain.com"], + rpc: ["https://zigchain-testnet-rpc.polkachu.com", "https://testnet-rpc.zigchain.com"], + explorer: "https://testnet.zigscan.org", + explorerTx: "https://testnet.zigscan.org/tx/{hash}", + native: { denom: "azig", symbol: "ZIG", name: "ZIGChain", decimals: 18, key: "zig" }, + feeTokens: [{ denom: "azig", low: "2500000000", average: "25000000000", high: "50000000000" }], + feeModel: "gas-price", + sendMsg: "bank", + }, + { + chainId: "zigchain-1", + family: "cosmos", + name: "ZIGChain", + testnet: false, + prefix: "zig", + coinType: 118, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://public-zigchain-lcd.numia.xyz", "https://zigchain-api.polkachu.com"], + rpc: ["https://public-zigchain-rpc.numia.xyz", "https://zigchain-rpc.polkachu.com"], + explorer: "https://www.zigscan.org", + explorerTx: "https://www.zigscan.org/tx/{hash}", + native: { denom: "azig", symbol: "ZIG", name: "ZIGChain", decimals: 18, key: "zig" }, + feeTokens: [{ denom: "azig", low: "2500000000", average: "25000000000", high: "50000000000" }], + feeModel: "gas-price", + sendMsg: "bank", + }, + + /* ---------------------------------------------------------------- provenance (505) */ + { + chainId: "pio-testnet-1", + family: "provenance", + name: "Provenance Testnet", + testnet: true, + prefix: "tp", + coinType: 505, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://api.test.provenance.io"], + rpc: ["https://rpc.test.provenance.io"], + explorer: "https://explorer.test.provenance.io", + explorerTx: "https://explorer.test.provenance.io/tx/{hash}", + native: { denom: "nhash", symbol: "HASH", name: "Hash", decimals: 9, key: "hash" }, + feeTokens: [{ denom: "nhash", low: "1", average: "1", high: "1" }], + feeModel: "provenance-flatfee", + sendMsg: "bank", + }, + { + chainId: "pio-mainnet-1", + family: "provenance", + name: "Provenance", + testnet: false, + prefix: "pb", + coinType: 505, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://provenance-api.polkachu.com", "https://provenance.api.pocket.network"], + rpc: ["https://provenance-rpc.polkachu.com", "https://rpc.provenance.io"], + explorer: "https://explorer.provenance.io", + explorerTx: "https://explorer.provenance.io/tx/{hash}", + native: { denom: "nhash", symbol: "HASH", name: "Hash", decimals: 9, key: "hash" }, + feeTokens: [{ denom: "nhash", low: "1", average: "1", high: "1" }], + feeModel: "provenance-flatfee", + sendMsg: "bank", + }, + + /* ---------------------------------------------------------------- thorchain (931) */ + { + chainId: "thorchain-1", + family: "thorchain", + name: "THORChain", + testnet: false, + prefix: "thor", + coinType: 931, + keyKind: "secp256k1", + pubKeyTypeUrl: SECP, + rest: ["https://gateway.liquify.com/chain/thorchain_api", "https://thorchain.ibs.team/api"], + rpc: ["https://gateway.liquify.com/chain/thorchain_rpc", "https://thorchain.ibs.team/rpc"], + explorer: "https://runescan.io", + explorerTx: "https://runescan.io/tx/{hash}", + native: { denom: "rune", symbol: "RUNE", name: "THORChain", decimals: 8, key: "rune" }, + feeTokens: [{ denom: "rune", low: "0", average: "0", high: "0" }], + feeModel: "fixed-native", + fixedFee: { denom: "rune", amount: "2000000" }, + minGas: 6_000_000n, + sendMsg: "thorchain", + }, + + /* ---------------------------------------------------------------- initia (60, ethsecp256k1) */ + { + chainId: "initiation-2", + family: "initia", + name: "Initia Testnet", + testnet: true, + prefix: "init", + coinType: 60, + keyKind: "ethsecp256k1", + pubKeyTypeUrl: ETHSECP_INITIA, + rest: ["https://rest.testnet.initia.xyz"], + rpc: ["https://rpc.testnet.initia.xyz"], + explorer: "https://scan.testnet.initia.xyz/initiation-2", + explorerTx: "https://scan.testnet.initia.xyz/initiation-2/txs/{hash}", + native: { denom: "uinit", symbol: "INIT", name: "Initia", decimals: 6, key: "init" }, + feeTokens: [{ denom: "uinit", low: "0.015", average: "0.015", high: "0.04" }], + feeModel: "initia-dynamic", + sendMsg: "bank", + }, + { + chainId: "interwoven-1", + family: "initia", + name: "Initia", + testnet: false, + prefix: "init", + coinType: 60, + keyKind: "ethsecp256k1", + pubKeyTypeUrl: ETHSECP_INITIA, + rest: ["https://rest.initia.xyz", "https://initia-api.polkachu.com"], + rpc: ["https://rpc.initia.xyz", "https://initia-rpc.polkachu.com"], + explorer: "https://scan.initia.xyz/interwoven-1", + explorerTx: "https://scan.initia.xyz/interwoven-1/txs/{hash}", + native: { denom: "uinit", symbol: "INIT", name: "Initia", decimals: 6, key: "init" }, + feeTokens: [{ denom: "uinit", low: "0.015", average: "0.015", high: "0.04" }], + feeModel: "initia-dynamic", + sendMsg: "bank", + }, +]; + +/** The vault's path for each family (packages/vault/src/derive.ts `derivationPath`). */ +export function derivationPathOf(family: CosmosFamily, index: number): string { + if (!Number.isInteger(index) || index < 0 || index >= 2 ** 31) throw new RangeError(`bad account index ${index}`); + const coin = { cosmos: 118, provenance: 505, thorchain: 931, initia: 60 }[family]; + return `m/44'/${coin}'/0'/0/${index}`; +} + +export function chainsOf(family: CosmosFamily): CosmosChainSpec[] { + return COSMOS_CHAINS.filter((c) => c.family === family); +} + +export function specOf(networkId: NetworkId): CosmosChainSpec | null { + const id = networkId.startsWith("cosmos:") ? networkId.slice("cosmos:".length) : null; + return (id && COSMOS_CHAINS.find((c) => c.chainId === id)) || null; +} + +export function nativeAsset(spec: CosmosChainSpec): AssetRef { + return { key: spec.native.key, symbol: spec.native.symbol, name: spec.native.name, decimals: spec.native.decimals, networkId: `cosmos:${spec.chainId}` }; +} + +/** + * The AssetRef of a denom on this chain: the native coin, a curated token, or (anything else) the raw denom with 0 + * decimals, so an amount is never shown scaled by a guess. Unknown denoms never appear in getBalances. + */ +export function assetOf(spec: CosmosChainSpec, denom: string): AssetRef { + if (denom === spec.native.denom) return nativeAsset(spec); + const t = spec.tokens?.find((x) => x.denom === denom); + const networkId = `cosmos:${spec.chainId}`; + if (t) return { key: t.key, symbol: t.symbol, name: t.name, decimals: t.decimals, networkId, address: denom }; + const symbol = denom.length > 16 ? `${denom.slice(0, 10)}…${denom.slice(-4)}` : denom; + return { key: `cosmos:${spec.chainId}/${denom}`, symbol, name: denom, decimals: 0, networkId, address: denom }; +} + +export function explorerTxUrl(spec: CosmosChainSpec, hash: string): string { + return spec.explorerTx.replace("{hash}", hash); +} + +function networkOf(s: CosmosChainSpec): Network { + return { + id: `cosmos:${s.chainId}`, + family: s.family, + name: s.name, + nativeAsset: nativeAsset(s), + testnet: s.testnet, + rpcUrls: s.rest, + explorerUrl: s.explorer, + }; +} + +/** Every network of every Cosmos family, as core Networks (rpcUrls = REST endpoints). */ +export const COSMOS_NETWORKS: Network[] = COSMOS_CHAINS.map(networkOf); + +export const networksOfFamily = (family: CosmosFamily): Network[] => COSMOS_NETWORKS.filter((n) => n.family === family); + +export const OSMOSIS_TESTNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:osmo-test-5")!; +export const OSMOSIS_MAINNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:osmosis-1")!; +export const DYDX_TESTNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:dydx-testnet-4")!; +export const DYDX_MAINNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:dydx-mainnet-1")!; +export const ZIGCHAIN_TESTNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:zig-test-2")!; +export const ZIGCHAIN_MAINNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:zigchain-1")!; +export const PROVENANCE_TESTNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:pio-testnet-1")!; +export const PROVENANCE_MAINNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:pio-mainnet-1")!; +export const THORCHAIN_MAINNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:thorchain-1")!; +export const INITIA_TESTNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:initiation-2")!; +export const INITIA_MAINNET = COSMOS_NETWORKS.find((n) => n.id === "cosmos:interwoven-1")!; diff --git a/packages/chains-cosmos/src/proto.ts b/packages/chains-cosmos/src/proto.ts new file mode 100644 index 00000000..58607781 --- /dev/null +++ b/packages/chains-cosmos/src/proto.ts @@ -0,0 +1,506 @@ +/** + * The protobuf this module needs, by hand (proto3 wire format: https://protobuf.dev/programming-guides/encoding/). + * Writers emit fields in field-number order and skip proto3 defaults (0, "", empty bytes), exactly as gogoproto / + * cosmjs-types do, so the bytes match what the chain re-encodes (tests compare against cosmjs-types). + * + * Messages (cosmos-sdk proto, https://github.com/cosmos/cosmos-sdk/tree/main/proto): + * - cosmos.tx.v1beta1: TxBody, AuthInfo, SignerInfo, ModeInfo, Fee, SignDoc, TxRaw (tx.proto) + * - cosmos.base.v1beta1.Coin, google.protobuf.Any + * - cosmos.bank.v1beta1.MsgSend, cosmos.staking.v1beta1.MsgDelegate/MsgUndelegate/MsgBeginRedelegate, + * cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward, ibc.applications.transfer.v1.MsgTransfer, + * cosmwasm.wasm.v1.MsgExecuteContract, cosmos.authz.v1beta1.MsgGrant/MsgExec/MsgRevoke, + * cosmos.feegrant.v1beta1.MsgGrantAllowance/MsgRevokeAllowance + * - THORChain types.MsgSend / types.MsgDeposit (thornode proto/thorchain/v1/types/msg_send.proto, msg_deposit.proto) + */ + +/* ------------------------------------------------------------------ writer */ + +export class Writer { + private parts: number[] = []; + + private varint(v: bigint): void { + if (v < 0n) v &= (1n << 64n) - 1n; // int64 negative: two's complement, 10 bytes + while (v > 0x7fn) { + this.parts.push(Number(v & 0x7fn) | 0x80); + v >>= 7n; + } + this.parts.push(Number(v)); + } + + private tag(field: number, wire: number): void { + this.varint(BigInt((field << 3) | wire)); + } + + /** uint64/int64/uint32/bool/enum; skipped when 0 (proto3 default). */ + uint(field: number, v: bigint | number | boolean | undefined): this { + const n = typeof v === "boolean" ? (v ? 1n : 0n) : BigInt(v ?? 0); + if (n === 0n) return this; + this.tag(field, 0); + this.varint(n); + return this; + } + + bytes(field: number, v: Uint8Array | undefined): this { + if (!v || v.length === 0) return this; + this.tag(field, 2); + this.varint(BigInt(v.length)); + for (const b of v) this.parts.push(b); + return this; + } + + /** Embedded message: written even when empty if `always` (e.g. a ModeInfo's `single {}`). */ + message(field: number, v: Uint8Array | undefined, always = false): this { + if (!v) return this; + if (v.length === 0 && !always) return this; + this.tag(field, 2); + this.varint(BigInt(v.length)); + for (const b of v) this.parts.push(b); + return this; + } + + string(field: number, v: string | undefined): this { + return this.bytes(field, v ? new TextEncoder().encode(v) : undefined); + } + + finish(): Uint8Array { + return Uint8Array.from(this.parts); + } +} + +/* ------------------------------------------------------------------ reader */ + +export interface Field { + no: number; + wire: number; + /** varint value (wire 0) */ + int?: bigint; + /** length-delimited payload (wire 2) */ + bytes?: Uint8Array; +} + +export class ProtoError extends Error {} + +/** Splits a message into fields. Throws ProtoError on anything malformed (groups, truncation, bad wire types). */ +export function readFields(buf: Uint8Array): Field[] { + const out: Field[] = []; + let at = 0; + const varint = (): bigint => { + let v = 0n; + let shift = 0n; + for (;;) { + if (at >= buf.length) throw new ProtoError("truncated varint"); + const b = buf[at++]!; + v |= BigInt(b & 0x7f) << shift; + if (!(b & 0x80)) return v; + shift += 7n; + if (shift > 63n) throw new ProtoError("varint too long"); + } + }; + while (at < buf.length) { + const key = varint(); + const no = Number(key >> 3n); + const wire = Number(key & 7n); + if (no === 0) throw new ProtoError("field 0"); + switch (wire) { + case 0: + out.push({ no, wire, int: varint() }); + break; + case 1: + if (at + 8 > buf.length) throw new ProtoError("truncated fixed64"); + out.push({ no, wire, bytes: buf.subarray(at, at + 8) }); + at += 8; + break; + case 2: { + const len = Number(varint()); + if (at + len > buf.length) throw new ProtoError("truncated bytes"); + out.push({ no, wire, bytes: buf.subarray(at, at + len) }); + at += len; + break; + } + case 5: + if (at + 4 > buf.length) throw new ProtoError("truncated fixed32"); + out.push({ no, wire, bytes: buf.subarray(at, at + 4) }); + at += 4; + break; + default: + throw new ProtoError(`wire type ${wire}`); + } + } + return out; +} + +const dec = new TextDecoder("utf-8", { fatal: true }); + +/** A read view over one message's fields. Last value wins for scalars (proto3), all values for repeated. */ +export class Msg { + readonly fields: Field[]; + constructor(buf: Uint8Array) { + this.fields = readFields(buf); + } + private last(no: number, wire: number): Field | undefined { + let f: Field | undefined; + for (const x of this.fields) if (x.no === no) { + if (x.wire !== wire) throw new ProtoError(`field ${no}: wire ${x.wire}, expected ${wire}`); + f = x; + } + return f; + } + uint(no: number): bigint { + return this.last(no, 0)?.int ?? 0n; + } + bool(no: number): boolean { + return this.uint(no) !== 0n; + } + bytes(no: number): Uint8Array { + return this.last(no, 2)?.bytes ?? new Uint8Array(); + } + has(no: number): boolean { + return this.fields.some((f) => f.no === no); + } + string(no: number): string { + try { + return dec.decode(this.bytes(no)); + } catch { + throw new ProtoError(`field ${no}: not UTF-8`); + } + } + repeated(no: number): Uint8Array[] { + return this.fields.filter((f) => f.no === no).map((f) => { + if (f.wire !== 2) throw new ProtoError(`field ${no}: wire ${f.wire}`); + return f.bytes!; + }); + } + /** Field numbers present that aren't in `known` (used to refuse what we can't show). */ + unknown(known: number[]): number[] { + return [...new Set(this.fields.map((f) => f.no).filter((n) => !known.includes(n)))]; + } +} + +/* ------------------------------------------------------------------ common types */ + +export interface Coin { + denom: string; + amount: string; +} + +export interface Any { + typeUrl: string; + value: Uint8Array; +} + +export const encodeCoin = (c: Coin): Uint8Array => new Writer().string(1, c.denom).string(2, c.amount).finish(); +export function decodeCoin(b: Uint8Array): Coin { + const m = new Msg(b); + const amount = m.string(2); + if (amount && !/^\d+$/.test(amount)) throw new ProtoError("bad coin amount"); + return { denom: m.string(1), amount: amount || "0" }; +} + +export const encodeAny = (a: Any): Uint8Array => new Writer().string(1, a.typeUrl).bytes(2, a.value).finish(); +export function decodeAny(b: Uint8Array): Any { + const m = new Msg(b); + return { typeUrl: m.string(1), value: m.bytes(2) }; +} + +/* ------------------------------------------------------------------ tx */ + +export interface TxBody { + messages: Any[]; + memo: string; + timeoutHeight: bigint; + /** extension_options (1023) / non_critical_extension_options (2047): kept so a re-encode is exact. */ + extensionOptions: Any[]; + nonCriticalExtensionOptions: Any[]; + /** SDK 0.50+: unordered (4), timeout_timestamp (5). Present = shown, not understood. */ + unordered: boolean; + timeoutTimestamp?: Uint8Array; +} + +export function encodeTxBody(b: Pick & Partial): Uint8Array { + const w = new Writer(); + for (const m of b.messages) w.message(1, encodeAny(m), true); + w.string(2, b.memo); + w.uint(3, b.timeoutHeight ?? 0n); + for (const e of b.extensionOptions ?? []) w.message(1023, encodeAny(e), true); + for (const e of b.nonCriticalExtensionOptions ?? []) w.message(2047, encodeAny(e), true); + return w.finish(); +} + +export function decodeTxBody(bytes: Uint8Array): TxBody { + const m = new Msg(bytes); + const body: TxBody = { + messages: m.repeated(1).map(decodeAny), + memo: m.string(2), + timeoutHeight: m.uint(3), + extensionOptions: m.repeated(1023).map(decodeAny), + nonCriticalExtensionOptions: m.repeated(2047).map(decodeAny), + unordered: m.has(4) ? m.bool(4) : false, + }; + if (m.has(5)) body.timeoutTimestamp = m.bytes(5); + const extra = m.unknown([1, 2, 3, 4, 5, 1023, 2047]); + if (extra.length) throw new ProtoError(`TxBody field ${extra[0]}`); + return body; +} + +export const SIGN_MODE_DIRECT = 1; +export const SIGN_MODE_LEGACY_AMINO_JSON = 127; + +export interface SignerInfo { + publicKey?: Any; + /** single.mode, or undefined for multisig mode info */ + mode?: number; + multi: boolean; + sequence: bigint; +} + +export interface Fee { + amount: Coin[]; + gasLimit: bigint; + payer: string; + granter: string; +} + +export interface AuthInfo { + signerInfos: SignerInfo[]; + fee: Fee; + /** AuthInfo.tip (3), deprecated: present = refused. */ + hasTip: boolean; +} + +export function encodeSignerInfo(s: { publicKey: Any; mode: number; sequence: bigint }): Uint8Array { + const single = new Writer().uint(1, s.mode).finish(); + const modeInfo = new Writer().message(1, single, true).finish(); + return new Writer().message(1, encodeAny(s.publicKey), true).message(2, modeInfo, true).uint(3, s.sequence).finish(); +} + +export function encodeFee(f: { amount: Coin[]; gasLimit: bigint; payer?: string; granter?: string }): Uint8Array { + const w = new Writer(); + for (const c of f.amount) w.message(1, encodeCoin(c), true); + w.uint(2, f.gasLimit); + w.string(3, f.payer); + w.string(4, f.granter); + return w.finish(); +} + +export function encodeAuthInfo(a: { signerInfos: { publicKey: Any; mode: number; sequence: bigint }[]; fee: { amount: Coin[]; gasLimit: bigint; payer?: string; granter?: string } }): Uint8Array { + const w = new Writer(); + for (const s of a.signerInfos) w.message(1, encodeSignerInfo(s), true); + w.message(2, encodeFee(a.fee), true); + return w.finish(); +} + +export function decodeAuthInfo(bytes: Uint8Array): AuthInfo { + const m = new Msg(bytes); + const signerInfos = m.repeated(1).map((b): SignerInfo => { + const s = new Msg(b); + const info: SignerInfo = { multi: false, sequence: s.uint(3) }; + if (s.has(1)) info.publicKey = decodeAny(s.bytes(1)); + if (s.has(2)) { + const mi = new Msg(s.bytes(2)); + if (mi.has(1)) info.mode = Number(new Msg(mi.bytes(1)).uint(1)); + else if (mi.has(2)) info.multi = true; + } + return info; + }); + const f = new Msg(m.bytes(2)); + return { + signerInfos, + fee: { amount: f.repeated(1).map(decodeCoin), gasLimit: f.uint(2), payer: f.string(3), granter: f.string(4) }, + hasTip: m.has(3), + }; +} + +export interface SignDoc { + bodyBytes: Uint8Array; + authInfoBytes: Uint8Array; + chainId: string; + accountNumber: bigint; +} + +/** cosmos.tx.v1beta1.SignDoc: what SIGN_MODE_DIRECT signs. */ +export function encodeSignDoc(d: SignDoc): Uint8Array { + return new Writer().bytes(1, d.bodyBytes).bytes(2, d.authInfoBytes).string(3, d.chainId).uint(4, d.accountNumber).finish(); +} + +export function encodeTxRaw(t: { bodyBytes: Uint8Array; authInfoBytes: Uint8Array; signatures: Uint8Array[] }): Uint8Array { + const w = new Writer().bytes(1, t.bodyBytes).bytes(2, t.authInfoBytes); + for (const s of t.signatures) w.message(3, s, true); + return w.finish(); +} + +export function decodeTxRaw(bytes: Uint8Array): { bodyBytes: Uint8Array; authInfoBytes: Uint8Array; signatures: Uint8Array[] } { + const m = new Msg(bytes); + return { bodyBytes: m.bytes(1), authInfoBytes: m.bytes(2), signatures: m.repeated(3) }; +} + +/* ------------------------------------------------------------------ public keys */ + +/** cosmos.crypto.secp256k1.PubKey and initia.crypto.v1beta1.ethsecp256k1.PubKey both are `bytes key = 1`. */ +export const encodePubKey = (key: Uint8Array): Uint8Array => new Writer().bytes(1, key).finish(); +export const decodePubKey = (b: Uint8Array): Uint8Array => new Msg(b).bytes(1); + +/* ------------------------------------------------------------------ messages */ + +export const TYPE = { + msgSend: "/cosmos.bank.v1beta1.MsgSend", + msgMultiSend: "/cosmos.bank.v1beta1.MsgMultiSend", + msgTransfer: "/ibc.applications.transfer.v1.MsgTransfer", + msgDelegate: "/cosmos.staking.v1beta1.MsgDelegate", + msgUndelegate: "/cosmos.staking.v1beta1.MsgUndelegate", + msgBeginRedelegate: "/cosmos.staking.v1beta1.MsgBeginRedelegate", + msgWithdrawReward: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + msgExecuteContract: "/cosmwasm.wasm.v1.MsgExecuteContract", + msgGrant: "/cosmos.authz.v1beta1.MsgGrant", + msgExec: "/cosmos.authz.v1beta1.MsgExec", + msgRevoke: "/cosmos.authz.v1beta1.MsgRevoke", + msgGrantAllowance: "/cosmos.feegrant.v1beta1.MsgGrantAllowance", + msgRevokeAllowance: "/cosmos.feegrant.v1beta1.MsgRevokeAllowance", + thorMsgSend: "/types.MsgSend", + thorMsgDeposit: "/types.MsgDeposit", + genericAuthorization: "/cosmos.authz.v1beta1.GenericAuthorization", + sendAuthorization: "/cosmos.bank.v1beta1.SendAuthorization", + stakeAuthorization: "/cosmos.staking.v1beta1.StakeAuthorization", + basicAllowance: "/cosmos.feegrant.v1beta1.BasicAllowance", + periodicAllowance: "/cosmos.feegrant.v1beta1.PeriodicAllowance", + allowedMsgAllowance: "/cosmos.feegrant.v1beta1.AllowedMsgAllowance", +} as const; + +export interface MsgSendValue { + fromAddress: string; + toAddress: string; + amount: Coin[]; +} + +export function encodeMsgSend(m: MsgSendValue): Uint8Array { + const w = new Writer().string(1, m.fromAddress).string(2, m.toAddress); + for (const c of m.amount) w.message(3, encodeCoin(c), true); + return w.finish(); +} + +export function decodeMsgSend(b: Uint8Array): MsgSendValue { + const m = new Msg(b); + return { fromAddress: m.string(1), toAddress: m.string(2), amount: m.repeated(3).map(decodeCoin) }; +} + +/** + * THORChain types.MsgSend { bytes from_address = 1 (AccAddress); bytes to_address = 2; repeated Coin amount = 3; + * bytes signer = 4? } — thornode proto/thorchain/v1/types/msg_send.proto: from_address and to_address are + * `bytes` cast to cosmos AccAddress (raw 20 bytes), amount is cosmos.base.v1beta1.Coin. + */ +export interface ThorMsgSendValue { + fromAddress: Uint8Array; + toAddress: Uint8Array; + amount: Coin[]; +} + +export function encodeThorMsgSend(m: ThorMsgSendValue): Uint8Array { + const w = new Writer().bytes(1, m.fromAddress).bytes(2, m.toAddress); + for (const c of m.amount) w.message(3, encodeCoin(c), true); + return w.finish(); +} + +export function decodeThorMsgSend(b: Uint8Array): ThorMsgSendValue { + const m = new Msg(b); + return { fromAddress: m.bytes(1), toAddress: m.bytes(2), amount: m.repeated(3).map(decodeCoin) }; +} + +/** + * THORChain types.MsgDeposit { repeated common.Coin coins = 1; string memo = 2; bytes signer = 3 }, common.Coin + * { common.Asset asset = 1; string amount = 2; int64 decimals = 3 }, common.Asset { chain = 1; symbol = 2; ticker = 3; + * synth = 4; trade = 5; secured = 6 } (thornode proto/thorchain/v1/types/msg_deposit.proto, common/common.proto). + */ +export function decodeThorMsgDeposit(b: Uint8Array): { coins: { asset: string; amount: string }[]; memo: string; signer: Uint8Array } { + const m = new Msg(b); + const coins = m.repeated(1).map((cb) => { + const c = new Msg(cb); + const a = new Msg(c.bytes(1)); + const chain = a.string(1); + const symbol = a.string(2); + const sep = a.bool(4) ? "/" : a.bool(5) ? "~" : a.bool(6) ? "-" : "."; + return { asset: chain && symbol ? `${chain}${sep}${symbol}` : symbol || chain, amount: c.string(2) || "0" }; + }); + return { coins, memo: m.string(2), signer: m.bytes(3) }; +} + +export interface MsgTransferValue { + sourcePort: string; + sourceChannel: string; + token: Coin; + sender: string; + receiver: string; + timeoutHeight: { revisionNumber: bigint; revisionHeight: bigint }; + timeoutTimestamp: bigint; + memo: string; +} + +export function decodeMsgTransfer(b: Uint8Array): MsgTransferValue { + const m = new Msg(b); + const h = new Msg(m.bytes(6)); + return { + sourcePort: m.string(1), + sourceChannel: m.string(2), + token: decodeCoin(m.bytes(3)), + sender: m.string(4), + receiver: m.string(5), + timeoutHeight: { revisionNumber: h.uint(1), revisionHeight: h.uint(2) }, + timeoutTimestamp: m.uint(7), + memo: m.string(8), + }; +} + +/** MsgDelegate / MsgUndelegate { delegator_address = 1; validator_address = 2; Coin amount = 3 }. */ +export function decodeMsgDelegate(b: Uint8Array): { delegator: string; validator: string; amount: Coin } { + const m = new Msg(b); + return { delegator: m.string(1), validator: m.string(2), amount: decodeCoin(m.bytes(3)) }; +} + +/** MsgBeginRedelegate { delegator = 1; validator_src = 2; validator_dst = 3; Coin amount = 4 }. */ +export function decodeMsgBeginRedelegate(b: Uint8Array): { delegator: string; from: string; to: string; amount: Coin } { + const m = new Msg(b); + return { delegator: m.string(1), from: m.string(2), to: m.string(3), amount: decodeCoin(m.bytes(4)) }; +} + +/** MsgWithdrawDelegatorReward { delegator_address = 1; validator_address = 2 }. */ +export function decodeMsgWithdrawReward(b: Uint8Array): { delegator: string; validator: string } { + const m = new Msg(b); + return { delegator: m.string(1), validator: m.string(2) }; +} + +/** cosmwasm.wasm.v1.MsgExecuteContract { sender = 1; contract = 2; bytes msg = 3 (JSON); repeated Coin funds = 5 }. */ +export function decodeMsgExecuteContract(b: Uint8Array): { sender: string; contract: string; msg: Uint8Array; funds: Coin[] } { + const m = new Msg(b); + return { sender: m.string(1), contract: m.string(2), msg: m.bytes(3), funds: m.repeated(5).map(decodeCoin) }; +} + +/** authz MsgGrant { granter = 1; grantee = 2; Grant grant = 3 { Any authorization = 1; Timestamp expiration = 2 } }. */ +export function decodeMsgGrant(b: Uint8Array): { granter: string; grantee: string; authorization?: Any; expiration?: bigint } { + const m = new Msg(b); + const g = new Msg(m.bytes(3)); + const out: { granter: string; grantee: string; authorization?: Any; expiration?: bigint } = { granter: m.string(1), grantee: m.string(2) }; + if (g.has(1)) out.authorization = decodeAny(g.bytes(1)); + if (g.has(2)) out.expiration = new Msg(g.bytes(2)).uint(1); + return out; +} + +/** authz GenericAuthorization { string msg = 1 }. */ +export const decodeGenericAuthorization = (b: Uint8Array): string => new Msg(b).string(1); + +/** feegrant MsgGrantAllowance { granter = 1; grantee = 2; Any allowance = 3 }. */ +export function decodeMsgGrantAllowance(b: Uint8Array): { granter: string; grantee: string; allowance?: Any } { + const m = new Msg(b); + const out: { granter: string; grantee: string; allowance?: Any } = { granter: m.string(1), grantee: m.string(2) }; + if (m.has(3)) out.allowance = decodeAny(m.bytes(3)); + return out; +} + +/** authz MsgRevoke { granter = 1; grantee = 2; msg_type_url = 3 }; feegrant MsgRevokeAllowance { granter = 1; grantee = 2 }. */ +export function decodeMsgRevoke(b: Uint8Array): { granter: string; grantee: string; msgTypeUrl: string } { + const m = new Msg(b); + return { granter: m.string(1), grantee: m.string(2), msgTypeUrl: m.string(3) }; +} + +/** authz MsgExec { grantee = 1; repeated Any msgs = 2 }. */ +export function decodeMsgExec(b: Uint8Array): { grantee: string; msgs: Any[] } { + const m = new Msg(b); + return { grantee: m.string(1), msgs: m.repeated(2).map(decodeAny) }; +} diff --git a/packages/chains-cosmos/src/rest.ts b/packages/chains-cosmos/src/rest.ts new file mode 100644 index 00000000..a5b1bc60 --- /dev/null +++ b/packages/chains-cosmos/src/rest.ts @@ -0,0 +1,230 @@ +import { ClipError, type ChainContext } from "@clip-wallet/core"; +import { b64encode, isObj } from "./util.js"; + +/** `url` without trailing slashes (a scan, not a regex: linear on any input). */ +function withoutTrailingSlashes(url: string): string { + let end = url.length; + while (end > 0 && url[end - 1] === "/") end--; + return url.slice(0, end); +} + +/** + * Cosmos SDK REST (gRPC-gateway "LCD") over ctx.fetch, trying each of the network's endpoints in order when one is + * unreachable or answers 5xx/429. Routes (cosmos-sdk proto/cosmos/{auth,bank,tx}/…/query.proto, service.proto): + * - GET /cosmos/auth/v1beta1/account_info/{address} (SDK ≥ 0.47; uniform BaseAccount fields for every account type) + * - GET /cosmos/auth/v1beta1/accounts/{address} (fallback; the account Any is unwrapped below) + * - GET /cosmos/bank/v1beta1/balances/{address} + * - POST /cosmos/tx/v1beta1/simulate { tx_bytes } + * - POST /cosmos/tx/v1beta1/txs { tx_bytes, mode: "BROADCAST_MODE_SYNC" } + * - GET /cosmos/tx/v1beta1/txs/{hash} + */ + +export class RestError extends Error { + constructor( + readonly status: number, + readonly body: unknown, + message: string, + ) { + super(message); + } + /** gRPC status code inside the gateway's error body ({ code, message, details }). */ + get grpcCode(): number | undefined { + return isObj(this.body) && typeof this.body.code === "number" ? this.body.code : undefined; + } + get notFound(): boolean { + return this.status === 404 || this.grpcCode === 5 || /not found/i.test(this.message); + } +} + +export interface Rest { + get(path: string): Promise; + post(path: string, body: unknown): Promise; +} + +export function restFor(ctx: ChainContext): Rest { + const bases = ctx.network.rpcUrls.map(withoutTrailingSlashes); + if (!bases.length) throw new ClipError("No connection is set up for this network.", "cosmos/no-endpoint"); + + async function call(path: string, init?: RequestInit): Promise { + let last: unknown; + for (const base of bases) { + let res: Response; + try { + res = await ctx.fetch(base + path, init); + } catch (e) { + last = e; + continue; + } + const text = await res.text().catch(() => ""); + let body: unknown = text; + try { + body = text ? JSON.parse(text) : null; + } catch { + /* keep text */ + } + if (res.ok) return body as T; + if (res.status >= 500 && !(isObj(body) && typeof body.code === "number" && body.code !== 13 && body.code !== 14)) { + // 500 with a gRPC code (5 not found, 3 invalid argument) is the node's real answer; other 5xx: next endpoint + last = new RestError(res.status, body, isObj(body) && typeof body.message === "string" ? body.message : `HTTP ${res.status}`); + continue; + } + if (res.status === 429) { + last = new RestError(res.status, body, "rate limited"); + continue; + } + throw new RestError(res.status, body, isObj(body) && typeof body.message === "string" ? body.message : `HTTP ${res.status}`); + } + if (last instanceof RestError) throw last; + throw new ClipError("We couldn't reach the network. Check your connection and try again.", "cosmos/offline", last); + } + + return { + get: (path) => call(path), + post: (path, body) => call(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }), + }; +} + +/* ------------------------------------------------------------------ accounts */ + +export interface OnChainAccount { + address: string; + accountNumber: bigint; + sequence: bigint; + /** Any JSON from the node ({ "@type", key }), when the account has signed before. */ + pubKey?: { "@type"?: string; key?: string }; +} + +/** Finds the BaseAccount fields in any account JSON (BaseAccount, vesting accounts, ModuleAccount, EthAccount…). */ +function findBase(x: unknown, depth = 0): Record | null { + if (!isObj(x) || depth > 5) return null; + if (typeof x.account_number === "string" && typeof x.address === "string") return x; + for (const v of Object.values(x)) { + const f = findBase(v, depth + 1); + if (f) return f; + } + return null; +} + +/** Module accounts hold no key: nobody can sign for them. */ +const MODULE_ACCOUNT = /ModuleAccount$/; + +/** The account, or null when it doesn't exist on chain yet (never received anything). */ +export async function getAccount(rest: Rest, address: string): Promise { + let raw: unknown; + try { + raw = (await rest.get<{ info?: unknown }>(`/cosmos/auth/v1beta1/account_info/${address}`)).info; + } catch (e) { + if (!(e instanceof RestError)) throw e; + if (e.notFound && !/Not Implemented|unknown service|501/i.test(e.message) && e.status !== 501) return null; + try { + const r = await rest.get<{ account?: unknown }>(`/cosmos/auth/v1beta1/accounts/${address}`); + raw = r.account; + if (isObj(raw) && typeof raw["@type"] === "string" && MODULE_ACCOUNT.test(raw["@type"])) { + throw new ClipError("This is a module account. Nobody can sign for it.", "cosmos/module-account"); + } + } catch (e2) { + if (e2 instanceof RestError && e2.notFound) return null; + throw e2; + } + } + const b = findBase(raw); + if (!b) return null; + const out: OnChainAccount = { + address: String(b.address), + accountNumber: BigInt(String(b.account_number ?? "0")), + sequence: BigInt(String(b.sequence ?? "0")), + }; + if (isObj(b.pub_key)) out.pubKey = b.pub_key as OnChainAccount["pubKey"]; + return out; +} + +/* ------------------------------------------------------------------ balances */ + +export async function getAllBalances(rest: Rest, address: string): Promise<{ denom: string; amount: string }[]> { + const out: { denom: string; amount: string }[] = []; + let key = ""; + for (let page = 0; page < 10; page++) { + const q = `pagination.limit=200${key ? `&pagination.key=${encodeURIComponent(key)}` : ""}`; + let r: { balances?: { denom: string; amount: string }[]; pagination?: { next_key?: string | null } }; + try { + r = await rest.get(`/cosmos/bank/v1beta1/balances/${address}?${q}`); + } catch (e) { + if (e instanceof RestError && e.notFound) return out; + throw e; + } + out.push(...(r.balances ?? [])); + key = r.pagination?.next_key ?? ""; + if (!key) break; + } + return out; +} + +/* ------------------------------------------------------------------ simulate / broadcast */ + +export async function simulate(rest: Rest, txBytes: Uint8Array): Promise { + const r = await rest.post<{ gas_info?: { gas_used?: string } }>("/cosmos/tx/v1beta1/simulate", { tx_bytes: b64encode(txBytes) }); + const used = r.gas_info?.gas_used; + if (!used || !/^\d+$/.test(used)) throw new RestError(200, r, "simulate returned no gas"); + return BigInt(used); +} + +export interface TxResponse { + txhash: string; + code: number; + codespace?: string; + raw_log?: string; + height?: string; +} + +export async function broadcastSync(rest: Rest, txBytes: Uint8Array): Promise { + const r = await rest.post<{ tx_response?: TxResponse }>("/cosmos/tx/v1beta1/txs", { tx_bytes: b64encode(txBytes), mode: "BROADCAST_MODE_SYNC" }); + if (!r.tx_response) throw new RestError(200, r, "broadcast returned no tx_response"); + return r.tx_response; +} + +export async function getTx(rest: Rest, hash: string): Promise { + try { + const r = await rest.get<{ tx_response?: TxResponse }>(`/cosmos/tx/v1beta1/txs/${hash}`); + return r.tx_response ?? null; + } catch (e) { + if (e instanceof RestError && (e.notFound || e.status === 400)) return null; + throw e; + } +} + +/* ------------------------------------------------------------------ errors in plain words */ + +/** + * SDK ABCI codes (codespace "sdk", cosmos-sdk types/errors/errors.go) → plain words. Anything else: the network + * refused, nothing was sent. + */ +export function plainCosmosError(code: number, codespace: string | undefined, log: string | undefined, symbol: string): string { + const l = log ?? ""; + if (!codespace || codespace === "sdk") { + switch (code) { + case 4: + return "The network didn't accept the signature. Nothing was sent."; + case 5: + return `You don't have enough ${symbol} for this and its network fee.`; + case 7: + return "That address isn't valid on this network."; + case 10: + return "The amount isn't valid."; + case 11: + return "The network ran out of gas for this. Nothing was sent; try again."; + case 13: + return "The network fee was too low. Try again."; + case 19: + return "This transaction is already on its way."; + case 20: + return "The network is busy right now. Try again in a minute."; + case 30: + return "This transaction expired. Try again."; + case 32: + return "Another transaction from this account went first. Try again."; + } + } + if (/insufficient funds|insufficient balance/i.test(l)) return `You don't have enough ${symbol} for this and its network fee.`; + if (/insufficient fee/i.test(l)) return "The network fee was too low. Try again."; + return "The network rejected this. Nothing was sent."; +} diff --git a/packages/chains-cosmos/src/util.ts b/packages/chains-cosmos/src/util.ts new file mode 100644 index 00000000..fd5c0d8e --- /dev/null +++ b/packages/chains-cosmos/src/util.ts @@ -0,0 +1,94 @@ +export const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s); + +export function b64encode(bytes: Uint8Array): string { + let s = ""; + for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + return btoa(s); +} + +/** Standard or URL-safe base64, padding optional. Throws on anything else. */ +export function b64decode(text: string): Uint8Array { + const clean = text.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/"); + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(clean)) throw new Error("not base64"); + const s = atob(clean.padEnd(clean.length + ((4 - (clean.length % 4)) % 4), "=")); + const out = new Uint8Array(s.length); + for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); + return out; +} + +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.replace(/^0x/, ""); + if (clean.length % 2 || !/^[0-9a-fA-F]*$/.test(clean)) throw new Error("not hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function equalBytes(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + let d = 0; + for (let i = 0; i < a.length; i++) d |= a[i]! ^ b[i]!; + return d === 0; +} + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +/** "0.025" → { num: 25n, den: 1000n } (gas prices are decimals). */ +export function parseDecimal(s: string): { num: bigint; den: bigint } { + const m = /^(\d+)(?:\.(\d+))?$/.exec(s.trim()); + if (!m) throw new Error(`bad decimal: ${s}`); + const frac = m[2] ?? ""; + return { num: BigInt(m[1]! + frac), den: 10n ** BigInt(frac.length) }; +} + +/** ceil(a * num / den) */ +export function mulCeil(a: bigint, num: bigint, den: bigint): bigint { + return (a * num + den - 1n) / den; +} + +export function short(address: string): string { + return address.length > 14 ? `${address.slice(0, address.indexOf("1") + 5)}…${address.slice(-4)}` : address; +} + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +export function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} + +export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms)); + +export const isObj = (x: unknown): x is Record => !!x && typeof x === "object" && !Array.isArray(x); diff --git a/packages/chains-cosmos/test/cosmos.test.ts b/packages/chains-cosmos/test/cosmos.test.ts new file mode 100644 index 00000000..9cfbebb1 --- /dev/null +++ b/packages/chains-cosmos/test/cosmos.test.ts @@ -0,0 +1,461 @@ +import { ClipError, WALLET_ORIGIN, type DappRequest } from "@clip-wallet/core"; +import { makeSignDoc, pubkeyToAddress, serializeSignDoc as cosmjsSerialize } from "@cosmjs/amino"; +import { Secp256k1, Secp256k1Signature, keccak256, sha256 } from "@cosmjs/crypto"; +import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; +import { AuthInfo, TxBody, TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { bech32 } from "@scure/base"; +import { describe, expect, it } from "vitest"; +import { + COSMOS_CHAINS, + COSMOS_METHODS, + COSMOS_NETWORKS, + DYDX_TESTNET, + INITIA_TESTNET, + OSMOSIS_MAINNET, + OSMOSIS_TESTNET, + PROVENANCE_MAINNET, + PROVENANCE_TESTNET, + THORCHAIN_MAINNET, + ZIGCHAIN_TESTNET, + createCosmosModule, + nativeAsset, + specOf, +} from "../src/index.js"; +import { TYPE, decodeAuthInfo, decodeThorMsgSend, decodeTxBody } from "../src/proto.js"; +import { b64decode, fromHex, hex } from "../src/util.js"; +import { ADDR, NOT_FOUND, PUB, VAULT_ADDRESS, accountInfo, balances, broadcastFail, ctxFor, fixtureSigner, makeAccount, mockFetch, reply, simulated } from "./helpers.js"; +import { MESSAGE, TXHASH, chainRoutes, dappAminoDoc, dappDirectDoc, dappRequest, initiaTransfer, opts, osmoTransfer, provenanceTransfer, thorTransfer } from "./scenarios.js"; + +const cosmos = createCosmosModule({ family: "cosmos", ...opts }); +const initia = createCosmosModule({ family: "initia", ...opts }); +const thor = createCosmosModule({ family: "thorchain", ...opts }); +const prov = createCosmosModule({ family: "provenance", ...opts }); + +async function sign(mod: ReturnType, req: DappRequest, ctx: ReturnType) { + const payloads = await mod.prepare(req, ctx, "approval-1"); + return mod.finalize(req, payloads.map((p) => fixtureSigner(mod.family as "cosmos").sign(p)), ctx); +} + +describe("families, derivation, addresses", () => { + it("matches the vault's paths and curve per family", () => { + expect(cosmos.derivationPath(0)).toBe("m/44'/118'/0'/0/0"); + expect(prov.derivationPath(3)).toBe("m/44'/505'/0'/0/3"); + expect(thor.derivationPath(1)).toBe("m/44'/931'/0'/0/1"); + expect(initia.derivationPath(2)).toBe("m/44'/60'/0'/0/2"); + for (const m of [cosmos, initia, thor, prov]) expect(m.curve).toBe("secp256k1"); + expect(() => createCosmosModule({ family: "evm" as never })).toThrow(); + }); + + it("each instance answers only its family's networks", () => { + expect(cosmos.networks.map((n) => n.id)).toEqual(["cosmos:osmo-test-5", "cosmos:osmosis-1", "cosmos:dydx-testnet-4", "cosmos:dydx-mainnet-1", "cosmos:zig-test-2", "cosmos:zigchain-1"]); + expect(prov.networks.map((n) => n.id)).toEqual(["cosmos:pio-testnet-1", "cosmos:pio-mainnet-1"]); + expect(thor.networks.map((n) => n.id)).toEqual(["cosmos:thorchain-1"]); + expect(initia.networks.map((n) => n.id)).toEqual(["cosmos:initiation-2", "cosmos:interwoven-1"]); + expect(COSMOS_NETWORKS.every((n) => n.id === `cosmos:${specOf(n.id)!.chainId}` && n.rpcUrls.length >= 1)).toBe(true); + expect(() => cosmos.addressFromPublicKey(fromHex(PUB.cosmos), INITIA_TESTNET)).toThrow(); + }); + + it("known answers for the abandon … about account, cross-checked with cosmjs pubkeyToAddress", () => { + const key = (f: keyof typeof PUB) => ({ type: "tendermint/PubKeySecp256k1", value: Buffer.from(fromHex(PUB[f])).toString("base64") }); + expect(cosmos.addressFromPublicKey(fromHex(PUB.cosmos), OSMOSIS_TESTNET)).toBe(ADDR.osmo); + expect(pubkeyToAddress(key("cosmos"), "osmo")).toBe(ADDR.osmo); + expect(cosmos.addressFromPublicKey(fromHex(PUB.cosmos), DYDX_TESTNET)).toBe(pubkeyToAddress(key("cosmos"), "dydx")); + expect(cosmos.addressFromPublicKey(fromHex(PUB.cosmos), ZIGCHAIN_TESTNET)).toBe(pubkeyToAddress(key("cosmos"), "zig")); + expect(prov.addressFromPublicKey(fromHex(PUB.provenance), PROVENANCE_TESTNET)).toBe(ADDR.tp); + expect(prov.addressFromPublicKey(fromHex(PUB.provenance), PROVENANCE_MAINNET)).toBe(pubkeyToAddress(key("provenance"), "pb")); + expect(prov.addressFromPublicKey(fromHex(PUB.provenance), PROVENANCE_MAINNET)).toBe(VAULT_ADDRESS.provenance); + expect(thor.addressFromPublicKey(fromHex(PUB.thorchain), THORCHAIN_MAINNET)).toBe(pubkeyToAddress(key("thorchain"), "thor")); + expect(thor.addressFromPublicKey(fromHex(PUB.thorchain), THORCHAIN_MAINNET)).toBe(VAULT_ADDRESS.thorchain); + // Initia: the EVM address of m/44'/60'/0'/0/0 of this phrase is the well-known 0x9858EfFD232B4033E47d90003D41EC34EcaEda94. + expect(initia.addressFromPublicKey(fromHex(PUB.initia), INITIA_TESTNET)).toBe(ADDR.init); + const evm = Buffer.from(keccak256(Secp256k1.uncompressPubkey(fromHex(PUB.initia)).slice(1)).slice(12)).toString("hex"); + expect(evm).toBe("9858effd232b4033e47d90003d41ec34ecaeda94"); + expect(hex(bech32.fromWords(bech32.decode(ADDR.init as `${string}1${string}`).words))).toBe(evm); + }); + + it("receiveAddress spells the account per network", async () => { + const f = mockFetch([]).fetch; + expect(await cosmos.receiveAddress(ctxFor(OSMOSIS_TESTNET, f))).toBe(ADDR.osmo); + expect(await cosmos.receiveAddress(ctxFor(DYDX_TESTNET, f))).toBe(ADDR.dydx); + expect(await cosmos.receiveAddress(ctxFor(ZIGCHAIN_TESTNET, f))).toBe(ADDR.zig); + expect(await prov.receiveAddress(ctxFor(PROVENANCE_TESTNET, f))).toBe(ADDR.tp); + expect(await prov.receiveAddress(ctxFor(PROVENANCE_MAINNET, f))).toBe(ADDR.pb); + expect(await initia.receiveAddress(ctxFor(INITIA_TESTNET, f))).toBe(ADDR.init); + }); + + it("isAddress checks the bech32 checksum and the family's prefixes; networksForAddress goes by prefix", () => { + expect(cosmos.isAddress(ADDR.osmo)).toBe(true); + expect(cosmos.isAddress(ADDR.dydx)).toBe(true); + expect(cosmos.isAddress(ADDR.zig)).toBe(true); + expect(cosmos.isAddress(ADDR.osmo.slice(0, -1) + "9")).toBe(false); // checksum + expect(cosmos.isAddress(ADDR.osmo.toUpperCase())).toBe(false); + expect(cosmos.isAddress(ADDR.tp)).toBe(false); // another family + expect(cosmos.isAddress(VAULT_ADDRESS.cosmos)).toBe(false); // cosmoshub isn't shipped + expect(prov.isAddress(ADDR.tp) && prov.isAddress(ADDR.pb)).toBe(true); + expect(thor.isAddress(ADDR.thor)).toBe(true); + expect(initia.isAddress(ADDR.init)).toBe(true); + // 32-byte contract addresses are accounts too (CosmWasm, Initia objects). + expect(cosmos.isAddress("osmo1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrqvlx82r")).toBe(true); + expect(cosmos.networksForAddress(ADDR.osmo, COSMOS_NETWORKS).map((n) => n.id)).toEqual(["cosmos:osmo-test-5", "cosmos:osmosis-1"]); + expect(cosmos.networksForAddress(ADDR.dydx, COSMOS_NETWORKS).map((n) => n.id)).toEqual(["cosmos:dydx-testnet-4", "cosmos:dydx-mainnet-1"]); + expect(prov.networksForAddress(ADDR.tp, COSMOS_NETWORKS).map((n) => n.id)).toEqual(["cosmos:pio-testnet-1"]); + expect(cosmos.networksForAddress(ADDR.init, COSMOS_NETWORKS)).toEqual([]); + }); +}); + +describe("balances", () => { + it("native + canonical USDC; unknown denoms hidden", async () => { + const usdc = "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5"; + const m = mockFetch([[/balances\/dydx1/, balances([{ denom: "adv4tnt", amount: "5000000000000000000" }, { denom: "ibc/DEADBEEF", amount: "99" }])]]); + const out = await cosmos.getBalances(ctxFor(DYDX_TESTNET, m.fetch)); + expect(out).toEqual([ + { asset: nativeAsset(specOf(DYDX_TESTNET.id)!), amount: "5000000000000000000" }, + { asset: { key: "usdc", symbol: "USDC", name: "USD Coin", decimals: 6, networkId: DYDX_TESTNET.id, address: usdc }, amount: "0" }, + ]); + expect(m.calls[0]!.url).toContain(`/cosmos/bank/v1beta1/balances/${ADDR.dydx}`); + }); + + it("an account that never received anything has 0; endpoints fail over", async () => { + const m = mockFetch([ + [/lcd\.osmotest5/, reply(503, "down")], + [/balances/, balances([])], + ]); + expect(await cosmos.getBalances(ctxFor(OSMOSIS_TESTNET, m.fetch))).toEqual([{ asset: nativeAsset(specOf(OSMOSIS_TESTNET.id)!), amount: "0" }]); + expect(m.calls.map((c) => new URL(c.url).host)).toEqual(["lcd.osmotest5.osmosis.zone", "lcd.testnet.osmosis.zone"]); + }); +}); + +describe("wallet transfers: build → decode → prepare → finalize", () => { + it("osmo-test-5: MsgSend, simulated gas × 1.4, fee from the txfees base fee × 1.65, sync broadcast, polled", async () => { + const s = await osmoTransfer(); + expect(s.request).toMatchObject({ origin: WALLET_ORIGIN, family: "cosmos", networkId: "cosmos:osmo-test-5", method: COSMOS_METHODS.signAndBroadcast }); + const doc = (s.request.params as { signDoc: { bodyBytes: string; authInfoBytes: string; chainId: string; accountNumber: string } }).signDoc; + expect(doc.chainId).toBe("osmo-test-5"); + expect(doc.accountNumber).toBe("4242"); + const body = TxBody.decode(b64decode(doc.bodyBytes)); + expect(MsgSend.decode(body.messages[0]!.value)).toEqual({ fromAddress: ADDR.osmo, toAddress: ADDR.osmoBob, amount: [{ denom: "uosmo", amount: "1500000" }] }); + const auth = AuthInfo.decode(b64decode(doc.authInfoBytes)); + expect(auth.fee!.gasLimit).toBe(112000n); // 80 000 × 1.4 + expect(auth.fee!.amount).toEqual([{ denom: "uosmo", amount: "4620" }]); // 112 000 × 0.025 × 1.65 + expect(auth.signerInfos[0]!.sequence).toBe(7n); + + const d = await cosmos.decode(s.request, s.ctx); + expect(d.title).toBe("Send 1.5 OSMO to osmo1jrkm…4pqs"); + expect(d.titleMsg?.id).toBe("bg.req.sendTo"); + expect(d.blind).toBe(false); + expect(d.balanceChanges).toEqual([{ asset: nativeAsset(specOf(OSMOSIS_TESTNET.id)!), delta: "-1500000" }]); + expect(d.fee).toEqual({ asset: nativeAsset(specOf(OSMOSIS_TESTNET.id)!), amount: "4620" }); + expect(d.lines).toContainEqual({ label: "To", value: ADDR.osmoBob }); + expect(d.lines).toContainEqual({ label: "Network fee", value: "0.00462 OSMO" }); + + const [p] = await cosmos.prepare(s.request, s.ctx, "approval-1"); + expect(p!.scheme).toBe("ecdsa-secp256k1"); + expect(p!.bytes.length).toBe(32); + const res = await sign(cosmos, s.request, s.ctx); + expect(res).toEqual({ txhash: TXHASH, status: "success", height: "123456" }); + const sent = s.calls.find((c) => c.method === "POST" && c.url.endsWith("/cosmos/tx/v1beta1/txs"))!; + const body2 = JSON.parse(sent.body!) as { tx_bytes: string; mode: string }; + expect(body2.mode).toBe("BROADCAST_MODE_SYNC"); + const raw = TxRaw.decode(b64decode(body2.tx_bytes)); + expect(raw.signatures[0]!.length).toBe(64); + // The digest is sha256(SignDoc) and the signature verifies with cosmjs too. + expect(await Secp256k1.verifySignature(Secp256k1Signature.fromFixedLength(raw.signatures[0]!), p!.bytes, fromHex(PUB.cosmos))).toBe(true); + }); + + it("initiation-2: ethsecp256k1 key type URL and a keccak256 digest", async () => { + const s = await initiaTransfer(); + const doc = (s.request.params as { signDoc: { authInfoBytes: string; bodyBytes: string } }).signDoc; + const auth = decodeAuthInfo(b64decode(doc.authInfoBytes)); + expect(auth.signerInfos[0]!.publicKey!.typeUrl).toBe("/initia.crypto.v1beta1.ethsecp256k1.PubKey"); + expect(auth.fee.amount).toEqual([{ denom: "uinit", amount: "1764" }]); // 112 000 × x/dynamicfee 0.015 × 1.05 + const d = await initia.decode(s.request, s.ctx); + expect(d.title).toBe("Send 0.5 INIT to init1d7ky…4swa"); + const [p] = await initia.prepare(s.request, s.ctx, "a"); + const { encodeSignDoc } = await import("../src/proto.js"); + const signDoc = encodeSignDoc({ bodyBytes: b64decode(doc.bodyBytes), authInfoBytes: b64decode(doc.authInfoBytes), chainId: "initiation-2", accountNumber: 4242n }); + expect(hex(p!.bytes)).toBe(hex(keccak256(signDoc))); + expect(await sign(initia, s.request, s.ctx)).toMatchObject({ txhash: TXHASH, status: "success" }); + }); + + it("thorchain-1: types.MsgSend with raw address bytes, no fee coins (fixed native fee), checked against the balance", async () => { + const s = await thorTransfer(); + const doc = (s.request.params as { signDoc: { authInfoBytes: string; bodyBytes: string } }).signDoc; + const body = decodeTxBody(b64decode(doc.bodyBytes)); + expect(body.messages[0]!.typeUrl).toBe(TYPE.thorMsgSend); + const m = decodeThorMsgSend(body.messages[0]!.value); + expect(m.amount).toEqual([{ denom: "rune", amount: "100000000" }]); + expect(decodeAuthInfo(b64decode(doc.authInfoBytes)).fee.amount).toEqual([]); + const d = await thor.decode(s.request, s.ctx); + expect(d.title).toBe("Send 1 RUNE to thor1lq2u…9g5j"); + expect(await sign(thor, s.request, s.ctx)).toMatchObject({ txhash: TXHASH }); + + // 4.99 RUNE + 0.02 fixed fee > 5 RUNE. + const m2 = mockFetch(chainRoutes(ADDR.thor, [{ denom: "rune", amount: "500000000" }])); + await expect(thor.buildTransfer({ asset: nativeAsset(specOf(THORCHAIN_MAINNET.id)!), to: ADDR.thorBob, amount: "499000000" }, ctxFor(THORCHAIN_MAINNET, m2.fetch))).rejects.toMatchObject({ + code: "cosmos/insufficient-funds", + }); + }); + + it("pio-testnet-1: tp… addresses, the flat fee from x/flatfees calculate_flat_fee", async () => { + const s = await provenanceTransfer(); + const doc = (s.request.params as { signDoc: { authInfoBytes: string } }).signDoc; + expect(decodeAuthInfo(b64decode(doc.authInfoBytes)).fee).toMatchObject({ amount: [{ denom: "nhash", amount: "7799095305" }], gasLimit: 80304n }); + const calc = s.calls.find((c) => c.url.endsWith("/provenance/tx/v1/calculate_flat_fee"))!; + expect(JSON.parse(calc.body!)).toMatchObject({ gas_adjustment: 1.4 }); + expect(s.calls.some((c) => c.url.endsWith("/cosmos/tx/v1beta1/simulate"))).toBe(false); + const d = await prov.decode(s.request, s.ctx); + expect(d.title).toBe("Send 2 HASH to tp1wqcz…nc8n"); + expect(d.lines).toContainEqual({ label: "Network fee", value: "7.799095305 HASH" }); + expect(await sign(prov, s.request, s.ctx)).toMatchObject({ txhash: TXHASH }); + }); + + it("refuses bad sends in plain words", async () => { + const m = mockFetch(chainRoutes(ADDR.osmo, [{ denom: "uosmo", amount: "1000" }])); + const ctx = ctxFor(OSMOSIS_TESTNET, m.fetch); + const osmo = nativeAsset(specOf(OSMOSIS_TESTNET.id)!); + await expect(cosmos.buildTransfer({ asset: osmo, to: ADDR.dydx, amount: "1" }, ctx)).rejects.toMatchObject({ code: "cosmos/bad-address", userMessage: "That address is for another network. Addresses on Osmosis Testnet start with osmo1…." }); + await expect(cosmos.buildTransfer({ asset: osmo, to: ADDR.osmo, amount: "1" }, ctx)).rejects.toMatchObject({ code: "cosmos/self-transfer" }); + await expect(cosmos.buildTransfer({ asset: osmo, to: "osmo1nope", amount: "1" }, ctx)).rejects.toMatchObject({ code: "cosmos/bad-address" }); + await expect(cosmos.buildTransfer({ asset: osmo, to: ADDR.osmoBob, amount: "0" }, ctx)).rejects.toMatchObject({ code: "cosmos/bad-amount" }); + await expect(cosmos.buildTransfer({ asset: osmo, to: ADDR.osmoBob, amount: "2000" }, ctx)).rejects.toMatchObject({ code: "cosmos/insufficient-funds", userMessage: "You don't have enough OSMO." }); + await expect(cosmos.buildTransfer({ asset: osmo, to: ADDR.osmoBob, amount: "900" }, ctx)).rejects.toMatchObject({ code: "cosmos/insufficient-funds", userMessage: "You don't have enough OSMO to pay for this and its fee." }); + const fresh = mockFetch([[/account_info/, NOT_FOUND]]); + await expect(cosmos.buildTransfer({ asset: osmo, to: ADDR.osmoBob, amount: "1" }, ctxFor(OSMOSIS_TESTNET, fresh.fetch))).rejects.toMatchObject({ + code: "cosmos/insufficient-funds", + userMessage: "Your account isn't on Osmosis Testnet yet. Receive some OSMO first.", + }); + }); + + it("a broadcast the chain rejects becomes plain words", async () => { + const s = await osmoTransfer(); + const m = mockFetch([[/\/cosmos\/tx\/v1beta1\/txs$/, broadcastFail(32, "account sequence mismatch, expected 8, got 7: incorrect account sequence")]]); + const ctx = { ...s.ctx, fetch: m.fetch }; + await expect(sign(cosmos, s.request, ctx)).rejects.toMatchObject({ code: "cosmos/send-failed", userMessage: "Another transaction from this account went first. Try again." }); + }); + + it("a signature that doesn't verify is refused before anything is broadcast", async () => { + const s = await osmoTransfer(); + const other = await provenanceTransfer(); + const [p] = await prov.prepare(other.request, other.ctx, "x"); + const wrong = fixtureSigner("provenance").sign(p!); // a real signature, but by another key over another digest + const before = s.calls.length; + await expect(cosmos.finalize(s.request, [{ ...wrong, publicKey: PUB.cosmos }], s.ctx)).rejects.toMatchObject({ code: "cosmos/bad-signature" }); + await expect(cosmos.finalize(s.request, [], s.ctx)).rejects.toMatchObject({ code: "cosmos/missing-signature" }); + expect(s.calls.slice(before).some((c) => c.method === "POST")).toBe(false); + }); + + it("signAndBroadcast is the wallet's own: a site can't use it", async () => { + const s = await osmoTransfer(); + await expect(cosmos.decode({ ...s.request, origin: "https://evil.example" }, s.ctx)).rejects.toMatchObject({ code: "cosmos/unsupported-method" }); + }); +}); + +describe("dapp requests", () => { + const ctx = () => ctxFor(OSMOSIS_TESTNET, mockFetch(chainRoutes(ADDR.osmo, [])).fetch); + + it("signDirect: decodes the cosmjs sign doc and returns { signed, signature } without broadcasting", async () => { + const req = dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmo, signDoc: dappDirectDoc() }); + const c = ctx(); + const d = await cosmos.decode(req, c); + expect(d.title).toBe("Send 0.000001 OSMO to osmo19rl4…7df8"); + expect(d.balanceChanges).toEqual([]); // to yourself + expect(d.lines).toContainEqual({ label: "Memo", value: "matrix" }); + expect(d.lines.find((l) => l.label === "Sent by")?.value).toBe("app.osmosis.example (it gets the signed transaction)"); + const res = (await sign(cosmos, req, c)) as { signed: unknown; signature: { pub_key: { type: string; value: string }; signature: string } }; + expect(res.signed).toEqual(dappDirectDoc()); + expect(res.signature.pub_key).toEqual({ type: "tendermint/PubKeySecp256k1", value: Buffer.from(fromHex(PUB.cosmos)).toString("base64") }); + expect(b64decode(res.signature.signature).length).toBe(64); + }); + + it("signDirect simulates when asked and warns when the preview fails", async () => { + const live = createCosmosModule({ family: "cosmos", simulate: true }); + const req = dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmo, signDoc: dappDirectDoc() }); + const ok = mockFetch([[/simulate$/, simulated("70000")]]); + expect((await live.decode(req, ctxFor(OSMOSIS_TESTNET, ok.fetch))).simulated).toBe(true); + const sent = JSON.parse(ok.calls[0]!.body!) as { tx_bytes: string }; + expect(TxRaw.decode(b64decode(sent.tx_bytes)).signatures).toEqual([new Uint8Array()]); + const bad = mockFetch([[/simulate$/, reply(400, { code: 5, message: "0uosmo is smaller than 5000uosmo: insufficient funds" })]]); + const d = await live.decode(req, ctxFor(OSMOSIS_TESTNET, bad.fetch)); + expect(d.simulated).toBe(false); + expect(d.warnings).toContainEqual({ level: "danger", code: "simulation-failed", message: "This is expected to fail and would still cost a fee." }); + }); + + it("signAmino: the cosmjs serialization is what gets hashed", async () => { + const doc = dappAminoDoc(); + const req = dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signAmino, { signerAddress: ADDR.osmo, signDoc: doc }); + const c = ctx(); + const d = await cosmos.decode(req, c); + expect(d.title).toBe("Send 0.25 OSMO to osmo1jrkm…4pqs"); + expect(d.fee).toEqual({ asset: nativeAsset(specOf(OSMOSIS_TESTNET.id)!), amount: "5000" }); + const [p] = await cosmos.prepare(req, c, "a"); + expect(hex(p!.bytes)).toBe(hex(sha256(cosmjsSerialize(doc)))); + const res = (await sign(cosmos, req, c)) as { signed: unknown }; + expect(res.signed).toEqual(doc); + }); + + it("ADR-36 signArbitrary: 'Sign a message for {host}', verifiable by cosmjs over Keplr's sign doc", async () => { + const req = dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signArbitrary, { signer: ADDR.osmo, data: btoa(MESSAGE) }); + const c = ctx(); + const d = await cosmos.decode(req, c); + expect(d.title).toBe("Sign a message for app.osmosis.example"); + expect(d.titleMsg?.id).toBe("bg.req.signMessage"); + expect(d.lines).toEqual([{ label: "Message", value: MESSAGE }]); + expect(d.blind).toBe(false); + const sig = (await sign(cosmos, req, c)) as { pub_key: { value: string }; signature: string }; + // Keplr makeADR36AminoSignDoc, rebuilt with cosmjs makeSignDoc: chain_id "", account 0, sequence 0, fee 0. + const doc = makeSignDoc([{ type: "sign/MsgSignData", value: { signer: ADDR.osmo, data: btoa(MESSAGE) } }], { gas: "0", amount: [] }, "", "", 0, 0); + expect(await Secp256k1.verifySignature(Secp256k1Signature.fromFixedLength(b64decode(sig.signature)), sha256(cosmjsSerialize(doc)), fromHex(PUB.cosmos))).toBe(true); + // verifyArbitrary (read, no approval) agrees, and refuses another message or signer. + expect(await cosmos.read(COSMOS_METHODS.verifyArbitrary, { signer: ADDR.osmo, data: btoa(MESSAGE), signature: sig }, c)).toBe(true); + expect(await cosmos.read(COSMOS_METHODS.verifyArbitrary, { signer: ADDR.osmo, data: btoa("other"), signature: sig }, c)).toBe(false); + expect(await cosmos.read(COSMOS_METHODS.verifyArbitrary, { signer: ADDR.osmoBob, data: btoa(MESSAGE), signature: sig }, c)).toBe(false); + // The same doc through signAmino (as Keplr routes it) is the same message request. + const viaAmino = dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signAmino, { signerAddress: ADDR.osmo, signDoc: doc }); + expect((await cosmos.decode(viaAmino, c)).title).toBe("Sign a message for app.osmosis.example"); + }); + + it("ADR-36 on Initia hashes with keccak256 (Keplr verifyADR36Amino 'ethsecp256k1')", async () => { + const c = ctxFor(INITIA_TESTNET, mockFetch([]).fetch); + const req = dappRequest(INITIA_TESTNET, COSMOS_METHODS.signArbitrary, { signer: ADDR.init, data: btoa(MESSAGE) }); + const sig = (await sign(initia, req, c)) as { signature: string }; + const doc = makeSignDoc([{ type: "sign/MsgSignData", value: { signer: ADDR.init, data: btoa(MESSAGE) } }], { gas: "0", amount: [] }, "", "", 0, 0); + expect(await Secp256k1.verifySignature(Secp256k1Signature.fromFixedLength(b64decode(sig.signature)), keccak256(cosmjsSerialize(doc)), fromHex(PUB.initia))).toBe(true); + }); + + it("a message that isn't text is shown as hex with a caution", async () => { + const req = dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signArbitrary, { signer: ADDR.osmo, data: btoa(String.fromCharCode(0, 1, 2, 255)) }); + const d = await cosmos.decode(req, ctx()); + expect(d.lines).toEqual([{ label: "Message (not text)", value: "0x000102ff" }]); + expect(d.warnings[0]).toMatchObject({ code: "blind-signing", level: "caution" }); + }); + + it("explains IBC, staking, contracts and grants; anything else is blind", async () => { + const c = ctx(); + const amino = (msgs: unknown[]) => + dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signAmino, { signerAddress: ADDR.osmo, signDoc: { ...dappAminoDoc(), msgs } }); + const ibc = await cosmos.decode( + amino([{ type: "cosmos-sdk/MsgTransfer", value: { source_port: "transfer", source_channel: "channel-4156", token: { denom: "uosmo", amount: "1000000" }, sender: ADDR.osmo, receiver: "noble1abc", timeout_timestamp: "1" } }]), + c, + ); + expect(ibc.title).toBe("Send 1 OSMO to noble1abc"); + expect(ibc.warnings[0]).toMatchObject({ code: "network-matters", msg: { id: "bg.cosmos.ibcLeaves" } }); + expect(ibc.balanceChanges[0]!.delta).toBe("-1000000"); + + const stake = await cosmos.decode(amino([{ type: "cosmos-sdk/MsgDelegate", value: { delegator_address: ADDR.osmo, validator_address: "osmovaloper1clpqr4nrk4khgkxj78fcwwh6dl3uw4ep88n0y4", amount: { denom: "uosmo", amount: "2000000" } } }]), c); + expect(stake.title).toBe("Stake 2 OSMO with osmovaloper1clpq…n0y4"); + const unstake = await cosmos.decode(amino([{ type: "cosmos-sdk/MsgUndelegate", value: { delegator_address: ADDR.osmo, validator_address: "osmovaloper1clpqr4nrk4khgkxj78fcwwh6dl3uw4ep88n0y4", amount: { denom: "uosmo", amount: "2000000" } } }]), c); + expect(unstake.title).toBe("Unstake 2 OSMO from osmovaloper1clpq…n0y4"); + const claim = await cosmos.decode(amino([{ type: "cosmos-sdk/MsgWithdrawDelegationReward", value: { delegator_address: ADDR.osmo, validator_address: "osmovaloper1clpqr4nrk4khgkxj78fcwwh6dl3uw4ep88n0y4" } }]), c); + expect(claim.title).toBe("Claim staking rewards from osmovaloper1clpq…n0y4"); + + const wasm = await cosmos.decode( + amino([{ type: "wasm/MsgExecuteContract", value: { sender: ADDR.osmo, contract: "osmo1contractaddr", msg: { swap: { offer: "1" } }, funds: [{ denom: "uosmo", amount: "1000" }] } }]), + c, + ); + expect(wasm.title).toBe("Use swap on contract osmo1cont…addr"); + expect(wasm.lines).toContainEqual({ label: "Message", value: JSON.stringify({ swap: { offer: "1" } }, null, 2) }); + expect(wasm.balanceChanges[0]!.delta).toBe("-1000"); + expect(wasm.warnings[0]).toMatchObject({ level: "caution", code: "unknown-call" }); + + const grant = await cosmos.decode( + amino([{ type: "cosmos-sdk/MsgGrant", value: { granter: ADDR.osmo, grantee: ADDR.osmoBob, grant: { authorization: { type: "cosmos-sdk/GenericAuthorization", value: { msg: TYPE.msgSend } } } } }]), + c, + ); + expect(grant.title).toBe("Let osmo1jrkm…4pqs act for your account"); + expect(grant.warnings[0]).toMatchObject({ level: "danger", code: "account-takeover", msg: { id: "bg.cosmos.authzTakeAll" } }); + const fee = await cosmos.decode(amino([{ type: "cosmos-sdk/MsgGrantAllowance", value: { granter: ADDR.osmo, grantee: ADDR.osmoBob, allowance: { type: "cosmos-sdk/BasicAllowance", value: {} } } }]), c); + expect(fee.warnings[0]).toMatchObject({ level: "danger", code: "account-takeover" }); + + const two = await cosmos.decode(amino([...dappAminoDoc().msgs, { type: "cosmos-sdk/MsgDelegate", value: { delegator_address: ADDR.osmo, validator_address: "osmovaloper1clpqr4nrk4khgkxj78fcwwh6dl3uw4ep88n0y4", amount: { denom: "uosmo", amount: "1" } } }]), c); + expect(two.title).toBe("Send 0.25 OSMO to osmo1jrkm…4pqs"); + expect(two.lines).toContainEqual(expect.objectContaining({ label: "Also", value: "Stake 0.000001 OSMO with osmovaloper1clpq…n0y4" })); + + const blind = await cosmos.decode(amino([{ type: "osmosis/poolmanager/swap-exact-amount-in", value: { sender: ADDR.osmo } }]), c); + expect(blind.blind).toBe(true); + expect(blind.title).toBe("Approve a transaction for app.osmosis.example"); + expect(blind.warnings[0]).toMatchObject({ level: "danger", code: "blind-signing" }); + + // Direct bytes we can't parse at all: blind, still signable only if blind signing is on (the approval decides). + const junk = dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmo, signDoc: { ...dappDirectDoc(), bodyBytes: btoa("ÿÿÿ") } }); + expect((await cosmos.decode(junk, c)).blind).toBe(true); + }); + + it("refuses the wrong network, the wrong signer and transactions it isn't a signer of", async () => { + const c = ctx(); + const doc = dappDirectDoc(); + await expect(cosmos.decode(dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmo, signDoc: { ...doc, chainId: "osmosis-1" } }), c)).rejects.toMatchObject({ + code: "cosmos/network-mismatch", + }); + await expect(cosmos.decode(dappRequest(OSMOSIS_MAINNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmo, signDoc: doc }), c)).rejects.toMatchObject({ code: "cosmos/network-mismatch" }); + await expect(initia.decode(dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmo, signDoc: doc }), c)).rejects.toMatchObject({ code: "cosmos/network-mismatch" }); + await expect(cosmos.decode(dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signAmino, { signerAddress: ADDR.osmo, signDoc: { ...dappAminoDoc(), chain_id: "dydx-testnet-4" } }), c)).rejects.toMatchObject({ + code: "cosmos/network-mismatch", + }); + await expect(cosmos.decode(dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmoBob, signDoc: doc }), c)).rejects.toMatchObject({ code: "cosmos/wrong-account" }); + await expect(cosmos.decode(dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signArbitrary, { signer: ADDR.dydx, data: "aGk=" }), c)).rejects.toMatchObject({ code: "cosmos/wrong-account" }); + const notMine = { ...dappAminoDoc(), msgs: [{ type: "cosmos-sdk/MsgSend", value: { from_address: ADDR.osmoBob, to_address: ADDR.osmo, amount: [{ denom: "uosmo", amount: "1" }] } }] }; + await expect(cosmos.decode(dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signAmino, { signerAddress: ADDR.osmo, signDoc: notMine }), c)).rejects.toMatchObject({ code: "cosmos/not-a-signer" }); + await expect(cosmos.decode(dappRequest(OSMOSIS_TESTNET, "cosmos_signEverything", {}), c)).rejects.toBeInstanceOf(ClipError); + }); + + it("sendTx broadcasts signed bytes without an approval (Keplr sendTx) and reports refusals in plain words", async () => { + const ok = mockFetch([[/\/cosmos\/tx\/v1beta1\/txs$/, { tx_response: { code: 0, txhash: "abcd" } }]]); + const c = ctxFor(OSMOSIS_TESTNET, ok.fetch); + const tx = btoa(String.fromCharCode(...TxRaw.encode({ bodyBytes: new Uint8Array([10, 0]), authInfoBytes: new Uint8Array([18, 0]), signatures: [new Uint8Array(64)] }).finish())); + expect(await cosmos.read(COSMOS_METHODS.sendTx, { tx, mode: "sync" }, c)).toEqual({ txhash: "ABCD" }); + expect(JSON.parse(ok.calls[0]!.body!)).toMatchObject({ mode: "BROADCAST_MODE_SYNC" }); + const bad = mockFetch([[/txs$/, broadcastFail(5, "insufficient funds")]]); + await expect(cosmos.read(COSMOS_METHODS.sendTx, { tx, mode: "sync" }, ctxFor(OSMOSIS_TESTNET, bad.fetch))).rejects.toMatchObject({ userMessage: "You don't have enough OSMO for this and its network fee." }); + }); +}); + +describe("network specs", () => { + it("every chain has its family's coin type, a prefix, two REST endpoints where available and a fee token", () => { + for (const c of COSMOS_CHAINS) { + expect(c.coinType).toBe({ cosmos: 118, provenance: 505, thorchain: 931, initia: 60 }[c.family]); + expect(c.rest.length).toBeGreaterThanOrEqual(c.chainId === "pio-testnet-1" || c.chainId === "initiation-2" ? 1 : 2); + expect(c.feeTokens[0]!.denom).toBe(c.native.denom); + expect(c.rest.every((u) => u.startsWith("https://") && !/key=|apikey/i.test(u))).toBe(true); + } + expect(COSMOS_CHAINS.filter((c) => c.testnet).map((c) => c.chainId)).toEqual(["osmo-test-5", "dydx-testnet-4", "zig-test-2", "pio-testnet-1", "initiation-2"]); + }); + + it("an account type the node wraps (vesting) still yields number and sequence", async () => { + const { getAccount, restFor } = await import("../src/rest.js"); + const vesting = { + account: { + "@type": "/cosmos.vesting.v1beta1.ContinuousVestingAccount", + base_vesting_account: { base_account: { address: ADDR.osmo, pub_key: null, account_number: "77", sequence: "3" }, original_vesting: [] }, + }, + }; + const m = mockFetch([ + [/account_info/, reply(501, { code: 12, message: "Not Implemented" })], + [/accounts\//, vesting], + ]); + const a = await getAccount(restFor(ctxFor(OSMOSIS_TESTNET, m.fetch)), ADDR.osmo); + expect(a).toMatchObject({ accountNumber: 77n, sequence: 3n }); + expect(accountInfo).toBeTypeOf("function"); + expect(makeAccount("cosmos").address).toBe(VAULT_ADDRESS.cosmos); + }); +}); + +describe("1Mask's chain table (packages/1mask/src/shared/cosmos.ts) matches these networks", () => { + it("same chains, families, prefixes, coin types, key algos, natives and gas prices", async () => { + const { COSMOS_CHAIN_FACTS } = await import("../../1mask/src/shared/cosmos.js"); + expect(COSMOS_CHAIN_FACTS.map((c) => c.chainId)).toEqual(COSMOS_CHAINS.map((c) => c.chainId)); + for (const f of COSMOS_CHAIN_FACTS) { + const s = COSMOS_CHAINS.find((c) => c.chainId === f.chainId)!; + expect({ family: f.family, prefix: f.prefix, coinType: f.coinType, algo: f.algo, testnet: f.testnet, name: f.chainName }).toEqual({ + family: s.family, + prefix: s.prefix, + coinType: s.coinType, + algo: s.keyKind, + testnet: s.testnet, + name: s.name, + }); + expect(f.native).toEqual({ coinDenom: s.native.symbol, coinMinimalDenom: s.native.denom, coinDecimals: s.native.decimals }); + expect(f.gas).toEqual({ low: Number(s.feeTokens[0]!.low), average: Number(s.feeTokens[0]!.average), high: Number(s.feeTokens[0]!.high) }); + expect(f.usdc).toBe(s.tokens?.find((t) => t.key === "usdc")?.denom); + } + }); +}); diff --git a/packages/chains-cosmos/test/helpers.ts b/packages/chains-cosmos/test/helpers.ts new file mode 100644 index 00000000..edbd0c70 --- /dev/null +++ b/packages/chains-cosmos/test/helpers.ts @@ -0,0 +1,105 @@ +import type { Account, ChainContext, Network, SignablePayload, Signature } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import type { CosmosFamily } from "../src/networks.js"; +import { fromHex } from "../src/util.js"; +import { SIGS } from "./signatures.js"; + +/** + * Public keys of account 0 of the public BIP-39 test vector ("abandon" ×11 + "about", no passphrase), printed by a + * scratch script outside the repo with the vault's own derivation (packages/vault/src/derive.ts deriveFamilyKey at + * derivationPath(family, 0)). Only public keys and addresses live in the repo. + */ +export const PUB: Record = { + cosmos: "024f4e2ad99c34d60b9ba6283c9431a8418af8673212961f97a77b6377fcd05b62", + provenance: "02b46c78777309c65fb5c2308574fb9a76853d7c0e91d1a1bd1f79f6ccb64ef6da", + thorchain: "02205c476a22d5fe10b74489db9479d0e36e25a32da393a771fcf12380136a451f", + initia: "0237b0bb7a8288d38ed49a524b5dc98cff3eb5ca824c9f9dc0dfdb3d9cd600f299", +}; + +/** The vault's Account.address for each family (mainnet spelling, packages/vault/src/address.ts). */ +export const VAULT_ADDRESS: Record = { + cosmos: "cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4", + provenance: "pb1fpwxdyscnu2dzjwghpstx7l9xtvsap48l5gnrm", + thorchain: "thor1gm00vwsfcp48enm4uv9e5dhm37jtd0ye27wrx0", + initia: "init1npvwllfr9dqr8erajqqr6s0vxnk2ak558xjc5c", +}; + +export const ADDR = { + osmo: "osmo19rl4cm2hmr8afy4kldpxz3fka4jguq0a5m7df8", + dydx: "dydx19rl4cm2hmr8afy4kldpxz3fka4jguq0a4erelz", + zig: "zig19rl4cm2hmr8afy4kldpxz3fka4jguq0aa2g0aa", + tp: "tp1fpwxdyscnu2dzjwghpstx7l9xtvsap48vldlp3", + pb: "pb1fpwxdyscnu2dzjwghpstx7l9xtvsap48l5gnrm", + thor: "thor1gm00vwsfcp48enm4uv9e5dhm37jtd0ye27wrx0", + init: "init1npvwllfr9dqr8erajqqr6s0vxnk2ak558xjc5c", + /** Account 1 of the same phrase (a second address to send to; nothing secret). */ + osmoBob: "osmo1jrkmdcwgq94uaamx6zax2luewlhf7u4k5r4pqs", + thorBob: "thor1lq2ufla82u5s85gyhe422w6mt02hrxf53q9g5j", + initBob: "init1d7ky6xxfzg6rh7r05uzfxexafepy4wwq924swa", + tpBob: "tp1wqczt252gxx4lnwvfq6036nqanqsf53rnsnc8n", +}; + +export function makeAccount(family: CosmosFamily): Account { + const coin = { cosmos: 118, provenance: 505, thorchain: 931, initia: 60 }[family]; + return { id: `${family}:0`, family, index: 0, curve: "secp256k1", derivationPath: `m/44'/${coin}'/0'/0/0`, publicKey: PUB[family], address: VAULT_ADDRESS[family] }; +} + +export function ctxFor(network: Network, fetchImpl: typeof fetch): ChainContext { + return { network, account: makeAccount(network.family as CosmosFamily), fetch: fetchImpl }; +} + +/** Vault stand-in: answers with a precomputed signature that verifies for the payload (verification only). */ +export function fixtureSigner(family: CosmosFamily) { + const pub = fromHex(PUB[family]); + return { + sign(p: SignablePayload): Signature { + const hit = Object.values(SIGS).map(fromHex).find((s) => secp256k1.verify(s, p.bytes, pub, { prehash: false })); + if (!hit) throw new Error(`no fixture signature for digest ${Buffer.from(p.bytes).toString("hex")}`); + return { scheme: "ecdsa-secp256k1", bytes: hit, recovery: 0, publicKey: PUB[family] }; + }, + }; +} + +type Body = unknown | ((url: string, init?: RequestInit) => unknown); +export interface Reply { + status: number; + body: unknown; +} +export const reply = (status: number, body: unknown): Reply => ({ status, body }); +const isReply = (v: unknown): v is Reply => !!v && typeof v === "object" && "status" in v && "body" in v && Object.keys(v).length === 2; + +/** URL-routed mock fetch (first match wins). Handlers may return a Reply for a non-200 status. */ +export function mockFetch(routes: [RegExp, Body][]) { + const calls: { url: string; method: string; body?: string }[] = []; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + calls.push({ url, method: init?.method ?? "GET", ...(typeof init?.body === "string" ? { body: init.body } : {}) }); + for (const [re, body] of routes) { + if (!re.test(url)) continue; + let v = typeof body === "function" ? (body as (u: string, i?: RequestInit) => unknown)(url, init) : body; + if (!isReply(v)) v = reply(200, v); + const r = v as Reply; + return new Response(typeof r.body === "string" ? r.body : JSON.stringify(r.body), { status: r.status }); + } + return new Response(JSON.stringify({ code: 12, message: `no mock for ${url}` }), { status: 501 }); + }) as typeof fetch; + return { fetch: f, calls }; +} + +/* ------------------------------------------------------------------ REST fixtures (shapes as returned by the live nodes, Oct 2026) */ + +export const accountInfo = (address: string, accountNumber: string, sequence: string, typeUrl = "/cosmos.crypto.secp256k1.PubKey") => ({ + info: { address, pub_key: { "@type": typeUrl, key: "AiXNMqSHp1E2vU0sHzROr3wN+vg3P2PcZpAG3nIlT1Ll" }, account_number: accountNumber, sequence }, +}); + +export const NOT_FOUND = reply(404, { code: 5, message: "rpc error: code = NotFound desc = account osmo1… not found: key not found", details: [] }); + +export const balances = (list: { denom: string; amount: string }[]) => ({ balances: list, pagination: { next_key: null, total: String(list.length) } }); + +export const simulated = (gasUsed: string) => ({ gas_info: { gas_wanted: "0", gas_used: gasUsed }, result: { data: "", log: "", events: [], msg_responses: [] } }); + +export const broadcastOk = (txhash: string) => ({ tx_response: { height: "0", txhash, codespace: "", code: 0, data: "", raw_log: "", logs: [], info: "", gas_wanted: "0", gas_used: "0", tx: null, timestamp: "", events: [] } }); + +export const broadcastFail = (code: number, raw_log: string, codespace = "sdk") => ({ tx_response: { height: "0", txhash: "AB", codespace, code, raw_log } }); + +export const txFound = (txhash: string, height = "123456") => ({ tx_response: { height, txhash, code: 0, raw_log: "" } }); diff --git a/packages/chains-cosmos/test/proto.test.ts b/packages/chains-cosmos/test/proto.test.ts new file mode 100644 index 00000000..a4f9e7c2 --- /dev/null +++ b/packages/chains-cosmos/test/proto.test.ts @@ -0,0 +1,177 @@ +import { makeSignDoc, serializeSignDoc as cosmjsSerialize } from "@cosmjs/amino"; +import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; +import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; +import { PubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys"; +import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; +import { MsgBeginRedelegate, MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; +import { AuthInfo, SignDoc, TxBody, TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing"; +import { MsgGrant } from "cosmjs-types/cosmos/authz/v1beta1/tx"; +import { GenericAuthorization } from "cosmjs-types/cosmos/authz/v1beta1/authz"; +import { MsgGrantAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx"; +import { BasicAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant"; +import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx"; +import { describe, expect, it } from "vitest"; +import { makeAdr36SignDoc, serializeSignDoc } from "../src/amino.js"; +import { fromAmino, fromAny } from "../src/msgs.js"; +import { + ProtoError, + SIGN_MODE_DIRECT, + TYPE, + decodeAuthInfo, + decodeThorMsgSend, + decodeTxBody, + decodeTxRaw, + encodeAuthInfo, + encodeMsgSend, + encodePubKey, + encodeSignDoc, + encodeThorMsgSend, + encodeTxBody, + encodeTxRaw, + readFields, +} from "../src/proto.js"; +import { fromHex, hex, utf8 } from "../src/util.js"; +import { PUB } from "./helpers.js"; + +/** Every hand-written encoding is checked byte for byte against cosmjs-types (ts-proto, the cosmjs encoder). */ + +const ME = "osmo19rl4cm2hmr8afy4kldpxz3fka4jguq0a5m7df8"; +const BOB = "osmo1jrkmdcwgq94uaamx6zax2luewlhf7u4k5r4pqs"; +const pub = fromHex(PUB.cosmos); + +describe("protobuf against cosmjs-types", () => { + it("TxBody, AuthInfo, SignDoc and TxRaw encode identically", () => { + const send = { fromAddress: ME, toAddress: BOB, amount: [{ denom: "uosmo", amount: "1500000" }] }; + const mine = encodeTxBody({ messages: [{ typeUrl: TYPE.msgSend, value: encodeMsgSend(send) }], memo: "hi <&>", timeoutHeight: 99n }); + const theirs = TxBody.encode( + TxBody.fromPartial({ messages: [{ typeUrl: TYPE.msgSend, value: MsgSend.encode(MsgSend.fromPartial(send)).finish() }], memo: "hi <&>", timeoutHeight: 99n }), + ).finish(); + expect(hex(mine)).toBe(hex(theirs)); + + const pk = { typeUrl: "/cosmos.crypto.secp256k1.PubKey", value: PubKey.encode({ key: pub }).finish() }; + expect(hex(encodePubKey(pub))).toBe(hex(pk.value)); + const auth = encodeAuthInfo({ signerInfos: [{ publicKey: pk, mode: SIGN_MODE_DIRECT, sequence: 7n }], fee: { amount: [{ denom: "uosmo", amount: "3750" }], gasLimit: 150000n } }); + const authTheirs = AuthInfo.encode( + AuthInfo.fromPartial({ + signerInfos: [{ publicKey: pk, modeInfo: { single: { mode: SignMode.SIGN_MODE_DIRECT } }, sequence: 7n }], + fee: { amount: [{ denom: "uosmo", amount: "3750" }], gasLimit: 150000n }, + }), + ).finish(); + expect(hex(auth)).toBe(hex(authTheirs)); + + // Sequence 0 (a first transaction) and an empty fee still encode the same. + const auth0 = encodeAuthInfo({ signerInfos: [{ publicKey: pk, mode: SIGN_MODE_DIRECT, sequence: 0n }], fee: { amount: [], gasLimit: 0n } }); + const auth0Theirs = AuthInfo.encode(AuthInfo.fromPartial({ signerInfos: [{ publicKey: pk, modeInfo: { single: { mode: SignMode.SIGN_MODE_DIRECT } }, sequence: 0n }], fee: {} })).finish(); + expect(hex(auth0)).toBe(hex(auth0Theirs)); + + const doc = encodeSignDoc({ bodyBytes: mine, authInfoBytes: auth, chainId: "osmo-test-5", accountNumber: 12345n }); + expect(hex(doc)).toBe(hex(SignDoc.encode({ bodyBytes: mine, authInfoBytes: auth, chainId: "osmo-test-5", accountNumber: 12345n }).finish())); + const doc0 = encodeSignDoc({ bodyBytes: mine, authInfoBytes: auth, chainId: "osmo-test-5", accountNumber: 0n }); + expect(hex(doc0)).toBe(hex(SignDoc.encode({ bodyBytes: mine, authInfoBytes: auth, chainId: "osmo-test-5", accountNumber: 0n }).finish())); + + const sig = new Uint8Array(64).fill(7); + const raw = encodeTxRaw({ bodyBytes: mine, authInfoBytes: auth, signatures: [sig] }); + expect(hex(raw)).toBe(hex(TxRaw.encode({ bodyBytes: mine, authInfoBytes: auth, signatures: [sig] }).finish())); + expect(decodeTxRaw(raw).signatures[0]).toEqual(sig); + + const body = decodeTxBody(theirs); + expect(body.memo).toBe("hi <&>"); + expect(body.timeoutHeight).toBe(99n); + expect(fromAny(body.messages[0]!, "osmo")).toEqual({ kind: "send", from: ME, to: BOB, amount: [{ denom: "uosmo", amount: "1500000" }] }); + const a = decodeAuthInfo(authTheirs); + expect(a.signerInfos[0]).toMatchObject({ mode: SIGN_MODE_DIRECT, sequence: 7n, multi: false }); + expect(a.fee).toEqual({ amount: [{ denom: "uosmo", amount: "3750" }], gasLimit: 150000n, payer: "", granter: "" }); + }); + + it("decodes the messages Clip explains", () => { + const any = (typeUrl: string, value: Uint8Array) => ({ typeUrl, value }); + const transfer = MsgTransfer.fromPartial({ + sourcePort: "transfer", + sourceChannel: "channel-0", + token: Coin.fromPartial({ denom: "uosmo", amount: "5" }), + sender: ME, + receiver: "cosmos1jrkmdcwgq94uaamx6zax2luewlhf7u4kucx3kz", + timeoutHeight: { revisionNumber: 1n, revisionHeight: 2n }, + timeoutTimestamp: 1700000000000000000n, + memo: "m", + }); + expect(fromAny(any(TYPE.msgTransfer, MsgTransfer.encode(transfer).finish()), "osmo")).toEqual({ + kind: "ibc-transfer", + sender: ME, + receiver: "cosmos1jrkmdcwgq94uaamx6zax2luewlhf7u4kucx3kz", + token: { denom: "uosmo", amount: "5" }, + port: "transfer", + channel: "channel-0", + memo: "m", + timeoutTimestamp: 1700000000000000000n, + }); + const del = MsgDelegate.fromPartial({ delegatorAddress: ME, validatorAddress: "osmovaloper1x", amount: { denom: "uosmo", amount: "9" } }); + expect(fromAny(any(TYPE.msgDelegate, MsgDelegate.encode(del).finish()), "osmo")).toEqual({ kind: "delegate", delegator: ME, validator: "osmovaloper1x", amount: [{ denom: "uosmo", amount: "9" }] }); + expect(fromAny(any(TYPE.msgUndelegate, MsgDelegate.encode(del).finish()), "osmo")).toMatchObject({ kind: "undelegate" }); + const red = MsgBeginRedelegate.fromPartial({ delegatorAddress: ME, validatorSrcAddress: "a", validatorDstAddress: "b", amount: { denom: "uosmo", amount: "3" } }); + expect(fromAny(any(TYPE.msgBeginRedelegate, MsgBeginRedelegate.encode(red).finish()), "osmo")).toEqual({ kind: "redelegate", delegator: ME, from: "a", to: "b", amount: [{ denom: "uosmo", amount: "3" }] }); + const wd = MsgWithdrawDelegatorReward.fromPartial({ delegatorAddress: ME, validatorAddress: "v" }); + expect(fromAny(any(TYPE.msgWithdrawReward, MsgWithdrawDelegatorReward.encode(wd).finish()), "osmo")).toEqual({ kind: "withdraw-rewards", delegator: ME, validator: "v" }); + const ex = MsgExecuteContract.fromPartial({ sender: ME, contract: "osmo1contract", msg: utf8('{"swap":{"min":"1"}}'), funds: [{ denom: "uosmo", amount: "4" }] }); + expect(fromAny(any(TYPE.msgExecuteContract, MsgExecuteContract.encode(ex).finish()), "osmo")).toMatchObject({ kind: "execute", contract: "osmo1contract", msg: { swap: { min: "1" } }, funds: [{ denom: "uosmo", amount: "4" }] }); + const grant = MsgGrant.fromPartial({ + granter: ME, + grantee: BOB, + grant: { authorization: { typeUrl: TYPE.genericAuthorization, value: GenericAuthorization.encode({ msg: TYPE.msgSend }).finish() }, expiration: { seconds: 1800000000n, nanos: 0 } }, + }); + expect(fromAny(any(TYPE.msgGrant, MsgGrant.encode(grant).finish()), "osmo")).toEqual({ kind: "authz-grant", granter: ME, grantee: BOB, authorization: TYPE.msgSend, expiration: 1800000000n }); + const fg = MsgGrantAllowance.fromPartial({ granter: ME, grantee: BOB, allowance: { typeUrl: TYPE.basicAllowance, value: BasicAllowance.encode({ spendLimit: [{ denom: "uosmo", amount: "100" }] }).finish() } }); + expect(fromAny(any(TYPE.msgGrantAllowance, MsgGrantAllowance.encode(fg).finish()), "osmo")).toEqual({ kind: "feegrant", granter: ME, grantee: BOB, allowance: TYPE.basicAllowance, spendLimit: [{ denom: "uosmo", amount: "100" }] }); + expect(fromAny(any("/osmosis.gamm.v1beta1.MsgSwapExactAmountIn", new Uint8Array([10, 1, 65])), "osmo")).toEqual({ kind: "unknown", type: "/osmosis.gamm.v1beta1.MsgSwapExactAmountIn" }); + // A malformed known message is unknown (blind), never half-read. + expect(fromAny(any(TYPE.msgSend, new Uint8Array([10, 50, 1])), "osmo")).toEqual({ kind: "unknown", type: TYPE.msgSend }); + }); + + it("THORChain types.MsgSend carries raw address bytes (thornode msg_send.proto)", () => { + const from = new Uint8Array(20).fill(1); + const to = new Uint8Array(20).fill(2); + const b = encodeThorMsgSend({ fromAddress: from, toAddress: to, amount: [{ denom: "rune", amount: "100" }] }); + // Field 1 / 2 are bytes (0x0a 0x14 …, 0x12 0x14 …), field 3 a cosmos Coin, as in thornode's generated Go struct. + expect(hex(b.subarray(0, 2))).toBe("0a14"); + expect(hex(b.subarray(22, 24))).toBe("1214"); + expect(decodeThorMsgSend(b)).toEqual({ fromAddress: from, toAddress: to, amount: [{ denom: "rune", amount: "100" }] }); + expect(fromAny({ typeUrl: TYPE.thorMsgSend, value: b }, "thor")).toMatchObject({ kind: "send", amount: [{ denom: "rune", amount: "100" }] }); + }); + + it("rejects malformed protobuf", () => { + expect(() => readFields(new Uint8Array([0x0a, 0x05, 1]))).toThrow(ProtoError); + expect(() => readFields(new Uint8Array([0x0b]))).toThrow(ProtoError); // group (wire 3) + expect(() => decodeTxBody(new Uint8Array([0x22, 0x00, 0x9a, 0x01, 0x00]))).toThrow(ProtoError); // unknown field 19 + }); +}); + +describe("amino JSON against @cosmjs/amino", () => { + it("serializes sign docs exactly as cosmjs (sorted keys, HTML escaping)", () => { + const msgs = [{ type: "cosmos-sdk/MsgSend", value: { from_address: ME, to_address: BOB, amount: [{ denom: "uosmo", amount: "1" }] } }]; + const fee = { amount: [{ denom: "uosmo", amount: "2500" }], gas: "100000" }; + const doc = makeSignDoc(msgs, fee, "osmo-test-5", "a & c", 12, 3); + expect(new TextDecoder().decode(serializeSignDoc(doc))).toBe(new TextDecoder().decode(cosmjsSerialize(doc))); + expect(new TextDecoder().decode(serializeSignDoc(doc))).toContain("a \\u003cb\\u003e \\u0026 c"); + }); + + it("builds ADR-36 docs as Keplr's makeADR36AminoSignDoc", () => { + const doc = makeAdr36SignDoc(ME, btoa("hello")); + expect(new TextDecoder().decode(serializeSignDoc(doc))).toBe( + `{"account_number":"0","chain_id":"","fee":{"amount":[],"gas":"0"},"memo":"","msgs":[{"type":"sign/MsgSignData","value":{"data":"aGVsbG8=","signer":"${ME}"}}],"sequence":"0"}`, + ); + }); + + it("reads amino messages", () => { + expect(fromAmino({ type: "cosmos-sdk/MsgSend", value: { from_address: ME, to_address: BOB, amount: [{ denom: "uosmo", amount: "1" }] } })).toEqual({ + kind: "send", + from: ME, + to: BOB, + amount: [{ denom: "uosmo", amount: "1" }], + }); + expect(fromAmino({ type: "wasm/MsgExecuteContract", value: { sender: ME, contract: "c", msg: { a: 1 }, funds: [] } })).toMatchObject({ kind: "execute", msg: { a: 1 } }); + expect(fromAmino({ type: "cosmos-sdk/MsgSend", value: { from_address: ME, to_address: BOB, amount: [{ denom: "uosmo", amount: "-1" }] } })).toEqual({ kind: "unknown", type: "cosmos-sdk/MsgSend" }); + expect(fromAmino({ type: "osmosis/gamm/swap-exact-amount-in", value: {} })).toEqual({ kind: "unknown", type: "osmosis/gamm/swap-exact-amount-in" }); + }); +}); diff --git a/packages/chains-cosmos/test/scenarios.ts b/packages/chains-cosmos/test/scenarios.ts new file mode 100644 index 00000000..1bb86718 --- /dev/null +++ b/packages/chains-cosmos/test/scenarios.ts @@ -0,0 +1,104 @@ +import type { ChainContext, DappRequest, Network } from "@clip-wallet/core"; +import { createCosmosModule, COSMOS_METHODS } from "../src/module.js"; +import { INITIA_TESTNET, OSMOSIS_TESTNET, PROVENANCE_TESTNET, THORCHAIN_MAINNET, nativeAsset, specOf } from "../src/networks.js"; +import { SIGN_MODE_DIRECT, TYPE, encodeAuthInfo, encodeMsgSend, encodePubKey, encodeTxBody } from "../src/proto.js"; +import { b64encode, fromHex } from "../src/util.js"; +import { ADDR, PUB, accountInfo, balances, broadcastOk, ctxFor, mockFetch, simulated, txFound } from "./helpers.js"; + +/** + * Deterministic signing scenarios shared by the tests and the offline script that produced test/signatures.ts + * (it ran these builders, asked the vault to sign each prepare() digest with the "abandon … about" account, and + * printed only the signatures). + */ + +export const TXHASH = "9F2C1A7E4B3D5C6E7F8091A2B3C4D5E6F708192A3B4C5D6E7F8091A2B3C4D5E6"; + +export const opts = { simulate: false, pollIntervalMs: 0, pollAttempts: 2 } as const; + +/** A node that knows our account (number 4242, sequence 7), holds balances, simulates 80 000 gas and accepts a broadcast. */ +export function chainRoutes(address: string, bal: { denom: string; amount: string }[], extra: [RegExp, unknown][] = [], typeUrl?: string): [RegExp, unknown][] { + return [ + ...extra, + [new RegExp(`/cosmos/auth/v1beta1/account_info/${address}$`), accountInfo(address, "4242", "7", typeUrl)], + [new RegExp(`/cosmos/bank/v1beta1/balances/${address}`), balances(bal)], + [/\/cosmos\/tx\/v1beta1\/simulate$/, simulated("80000")], + [/\/osmosis\/txfees\/v1beta1\/cur_eip_base_fee$/, { base_fee: "0.025000000000000000" }], + [/\/initia\/tx\/v1\/gas_prices\/uinit$/, { gas_price: { denom: "uinit", amount: "0.015000000000000000" } }], + [/\/thorchain\/network$/, { native_tx_fee_rune: "2000000", native_outbound_fee_rune: "2000000" }], + [/\/provenance\/tx\/v1\/calculate_flat_fee$/, { total_fees: [{ denom: "nhash", amount: "7799095305" }], estimated_gas: "80304" }], + [/\/cosmos\/tx\/v1beta1\/txs$/, broadcastOk(TXHASH)], + [new RegExp(`/cosmos/tx/v1beta1/txs/${TXHASH}$`), txFound(TXHASH)], + ]; +} + +export interface Scenario { + family: "cosmos" | "provenance" | "thorchain" | "initia"; + network: Network; + ctx: ChainContext; + request: DappRequest; + calls: { url: string; method: string; body?: string }[]; +} + +async function transfer(network: Network, from: string, to: string, amount: string, bal: { denom: string; amount: string }[], typeUrl?: string): Promise { + const family = network.family as Scenario["family"]; + const m = mockFetch(chainRoutes(from, bal, [], typeUrl)); + const ctx = ctxFor(network, m.fetch); + const mod = createCosmosModule({ family, ...opts }); + const request = await mod.buildTransfer({ asset: nativeAsset(specOf(network.id)!), to, amount }, ctx); + return { family, network, ctx, request, calls: m.calls }; +} + +/** 1.5 OSMO to account 1 on osmo-test-5 (built by the wallet). */ +export const osmoTransfer = () => transfer(OSMOSIS_TESTNET, ADDR.osmo, ADDR.osmoBob, "1500000", [{ denom: "uosmo", amount: "10000000" }]); + +/** 0.5 INIT to account 1 on initiation-2 (ethsecp256k1, keccak digest). */ +export const initiaTransfer = () => + transfer(INITIA_TESTNET, ADDR.init, ADDR.initBob, "500000", [{ denom: "uinit", amount: "3000000" }], "/initia.crypto.v1beta1.ethsecp256k1.PubKey"); + +/** 1 RUNE to account 1 on thorchain-1 (types.MsgSend, fixed native fee). */ +export const thorTransfer = () => transfer(THORCHAIN_MAINNET, ADDR.thor, ADDR.thorBob, "100000000", [{ denom: "rune", amount: "500000000" }]); + +/** 2 HASH to account 1 on pio-testnet-1. */ +export const provenanceTransfer = () => transfer(PROVENANCE_TESTNET, ADDR.tp, ADDR.tpBob, "2000000000", [{ denom: "nhash", amount: "900000000000" }]); + +/** What a dapp's cosmjs SigningStargateClient hands the wallet for a 1 uosmo MsgSend to itself. */ +export function dappDirectDoc(): { bodyBytes: string; authInfoBytes: string; chainId: string; accountNumber: string } { + const body = encodeTxBody({ messages: [{ typeUrl: TYPE.msgSend, value: encodeMsgSend({ fromAddress: ADDR.osmo, toAddress: ADDR.osmo, amount: [{ denom: "uosmo", amount: "1" }] }) }], memo: "matrix" }); + const auth = encodeAuthInfo({ + signerInfos: [{ publicKey: { typeUrl: "/cosmos.crypto.secp256k1.PubKey", value: encodePubKey(fromHex(PUB.cosmos)) }, mode: SIGN_MODE_DIRECT, sequence: 7n }], + fee: { amount: [{ denom: "uosmo", amount: "5000" }], gasLimit: 200000n }, + }); + return { bodyBytes: b64encode(body), authInfoBytes: b64encode(auth), chainId: "osmo-test-5", accountNumber: "4242" }; +} + +export const dappAminoDoc = () => ({ + chain_id: "osmo-test-5", + account_number: "4242", + sequence: "7", + fee: { amount: [{ denom: "uosmo", amount: "5000" }], gas: "200000" }, + msgs: [{ type: "cosmos-sdk/MsgSend", value: { from_address: ADDR.osmo, to_address: ADDR.osmoBob, amount: [{ denom: "uosmo", amount: "250000" }] } }], + memo: "", +}); + +export const MESSAGE = "Clip Wallet dapp matrix: sign-in check (testnet)"; + +export function dappRequest(network: Network, method: string, params: unknown, origin = "https://app.osmosis.example"): DappRequest { + return { id: `req-${method}`, origin, via: "injected", family: network.family, networkId: network.id, method, params }; +} + +/** Every signing scenario whose digest has a fixture signature. */ +export async function allScenarios(): Promise { + const fetchNone = mockFetch(chainRoutes(ADDR.osmo, [{ denom: "uosmo", amount: "10000000" }])); + const osmoCtx = ctxFor(OSMOSIS_TESTNET, fetchNone.fetch); + const initiaCtx = ctxFor(INITIA_TESTNET, fetchNone.fetch); + return [ + await osmoTransfer(), + await initiaTransfer(), + await thorTransfer(), + await provenanceTransfer(), + { family: "cosmos", network: OSMOSIS_TESTNET, ctx: osmoCtx, calls: [], request: dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signDirect, { signerAddress: ADDR.osmo, signDoc: dappDirectDoc() }) }, + { family: "cosmos", network: OSMOSIS_TESTNET, ctx: osmoCtx, calls: [], request: dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signAmino, { signerAddress: ADDR.osmo, signDoc: dappAminoDoc() }) }, + { family: "cosmos", network: OSMOSIS_TESTNET, ctx: osmoCtx, calls: [], request: dappRequest(OSMOSIS_TESTNET, COSMOS_METHODS.signArbitrary, { signer: ADDR.osmo, data: btoa(MESSAGE) }) }, + { family: "initia", network: INITIA_TESTNET, ctx: initiaCtx, calls: [], request: dappRequest(INITIA_TESTNET, COSMOS_METHODS.signArbitrary, { signer: ADDR.init, data: btoa(MESSAGE) }) }, + ]; +} diff --git a/packages/chains-cosmos/test/signatures.ts b/packages/chains-cosmos/test/signatures.ts new file mode 100644 index 00000000..970836a3 --- /dev/null +++ b/packages/chains-cosmos/test/signatures.ts @@ -0,0 +1,15 @@ +/** + * Signatures computed once OFFLINE (a scratch script outside the repo) by the vault's own signing code + * (packages/vault/src/sign.ts signEcdsa: RFC 6979, low-S) with account 0 of the public BIP-39 test vector + * ("abandon" ×11 + "about"), over the prepare() digests of test/scenarios.ts allScenarios(). Only signatures are here. + */ +export const SIGS: Record = { + "cosmos:cosmos_signAndBroadcast:0": "81d0b3fc78f06485f2cf0acb380cb573d09cf84a2613edfb3925c6826fc7de0b08ac3582db3ea00cc742fefa5250423453319827b634ae09a3c8b5941945e3a6", + "initia:cosmos_signAndBroadcast:1": "879a74bf03cc59de5c6f07987f820f58c4dc421e0b26c5ae04ed4c7188f4f3f15bd1349f0eee317ce84cf7c3406982c88f423afbeaaa8bf5f92622eed2cef655", + "thorchain:cosmos_signAndBroadcast:2": "9454eca73f1d465424ee29e134735e38dcbbc7886893052f08633041442eb4122d63cac24b924e1f21bedad436204448e4c5588f0bae4d91c539dba194f6b28b", + "provenance:cosmos_signAndBroadcast:3": "b729d7b22bc18247edf53dac6d74739d3c7ea6dc5ba711445cfa0b8746d9724e317ae4851c679fc27b744fb771e35f082082741c75e9148e3f5d31719d8f18fe", + "cosmos:cosmos_signDirect:4": "5274bd61e149325b9dd2e3c31e7515edcce8f3ad69d9527e78fd5a9875e98fa44986446fd9ba87ac6cc57173cb40e64af0c7d03ec411329f38f233e2985e0ec3", + "cosmos:cosmos_signAmino:5": "879a5410bfad7f15aef725ce276a79ebfc0a351f041fe64592c9b817ebdba21d3e90721948977ea98e9dbbeca2ce5d2947ed8665967914bd9d9c902bc643f8b0", + "cosmos:cosmos_signArbitrary:6": "a4786f269c72793e501a6c38dba60e8d2fcf0329dcdeab8f41940f83357b51de3d1dbea596e2c4151eb2b6ab71cddb5c7bd5f29f3eb7e322ea19c0db40a76a0c", + "initia:cosmos_signArbitrary:7": "9ff4a773c25f9d9be29fc33f6a249a41c4afd79655ea0b4914b68a0d73bb040e00ced84e5f56ad7ffab828871e2bc78c62757b4cd7dd06bd6cf82bd7ad47bc40" +}; diff --git a/packages/chains-cosmos/tsconfig.json b/packages/chains-cosmos/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-cosmos/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-evm/src/decode.ts b/packages/chains-evm/src/decode.ts index 784405a6..3b1a9ec2 100644 --- a/packages/chains-evm/src/decode.ts +++ b/packages/chains-evm/src/decode.ts @@ -14,6 +14,7 @@ import { toFunctionSelector, } from "viem"; import { chainIdOf, quoteFees, tokenMeta, type FeeQuote } from "./chain.js"; +import { specFor } from "./networks.js"; import { isOpStack, l1DataFee } from "./l1fee.js"; import { formatAmount, hostOf, isUnlimited, safeChecksum, shortAddress } from "./format.js"; import { type TxParams, type TypedData, parsePersonalSign, parseTx, parseTypedData } from "./params.js"; @@ -187,6 +188,14 @@ async function decodeTransaction(req: DappRequest, ctx: ChainContext, onFee?: (s } catch { /* fee unknown: the UI shows "fee unavailable" */ } + // A per-transaction fee outside gas (STRATO: 0.01 USDST), charged even if the transaction fails. + const flat = specFor(ctx.network.id)?.flatFee; + if (flat) { + d.fee = { asset: native, amount: (BigInt(d.fee?.amount ?? "0") + flat).toString() }; + const amount = `${formatAmount(flat, native.decimals)} ${native.symbol}`; + const value = msg("bg.evm.flatFee", { amount }); + d.lines.push({ label: "Network charge", value: value.fallback, labelMsg: msg("bg.label.networkCharge"), valueMsg: value }); + } return d; } diff --git a/packages/chains-evm/src/networks.ts b/packages/chains-evm/src/networks.ts index ef8a555f..1b15c419 100644 --- a/packages/chains-evm/src/networks.ts +++ b/packages/chains-evm/src/networks.ts @@ -5,9 +5,14 @@ * runs an EVM a user wallet can sign for with an eip155 address, plus the Sepolia testnets. * Several CLPR networks are multi-VM: their EVM layer is listed here (Cronos, Kava, Sei, Mezo, Stable, * MANTRA, Injective, Telos, Hydration, Bittensor, Bifrost). Hedera is its own module. - * Not included: STRATO (SolidVM, not the EVM), Vaulta EVM (17777; its public RPC, explorer and bridge were shut - * down on 2025-10-08 and api.evm.eosnetwork.com no longer resolves), TRON (own address format), Starknet, Fuel, Mixin and the - * non-EVM ledgers. + * STRATO runs SolidVM, not the EVM, but its nodes take signed legacy Ethereum transactions (EIP-155) through + * `eth_sendRawTransaction`: a value transfer with empty data moves the native USDST, a call's selector is matched to the + * SolidVM function (strato-net/strato-platform techdocs/platform/transactions-and-fees.md, reference/json-rpc.md; + * `EthereumTX` in strato/core/blockapps-data/src/Blockchain/Data/Transaction.hs). Gas is unpriced (eth_gasPrice 0x0); + * the network charges a flat 0.01 USDST (or a voucher) per transaction: `flatFee`. + * Not included: Vaulta EVM (17777; its public RPC, explorer and bridge were shut down on 2025-10-08 and + * api.evm.eosnetwork.com no longer resolves; Vaulta itself is in chains-antelope), TRON (own address format), Starknet, + * Fuel, Mixin and the non-EVM ledgers. * * Chain ids: cross-checked against viem's chain definitions (viem 2.57) and chainlist (chainid.network), * and for the four chains neither has under these ids (Anubis 6714, BOT Chain 677, GRX 1110, Hydration 222222) against the CLPR verifier docs (docs/chains/*.md, live `eth_chainId`) and a live `eth_chainId` @@ -31,6 +36,11 @@ export interface EvmNetworkSpec { legacyGas?: boolean; /** Which CLPR network this EVM belongs to when it is one VM of a multi-VM chain. */ evmLayerOf?: string; + /** + * A fee the network charges per transaction outside gas × price (base units of the native asset), shown on the + * approval screen with the gas fee. STRATO: 0.01 USDST (or one fee voucher). + */ + flatFee?: bigint; } const ETH = { symbol: "ETH", name: "Ether", decimals: 18, key: "eth" }; @@ -42,8 +52,10 @@ export const EVM_NETWORK_SPECS: EvmNetworkSpec[] = [ { slug: "anubis", name: "Anubis", chainId: 6714, native: coin("gasDAI", "Gas DAI", "gasdai"), rpcUrls: ["https://rpc.anubispace.org"], explorerUrl: "https://anubisscan.io", testnet: false }, { slug: "arbitrum-nova", name: "Arbitrum Nova", chainId: 42170, native: ETH, rpcUrls: ["https://nova.arbitrum.io/rpc"], explorerUrl: "https://nova.arbiscan.io", blockscout: "https://arbitrum-nova.blockscout.com", testnet: false }, { slug: "arbitrum-one", name: "Arbitrum One", chainId: 42161, native: ETH, rpcUrls: ["https://arb1.arbitrum.io/rpc", "https://arbitrum-one-rpc.publicnode.com"], explorerUrl: "https://arbiscan.io", blockscout: "https://arbitrum.blockscout.com", testnet: false }, - // CLPR's Arc docs pin the testnet id; Arc mainnet is not live in the sources. Native gas is USDC (18 decimals on the EVM side). - { slug: "arc-testnet", name: "Arc Testnet", chainId: 5042002, native: { symbol: "USDC", name: "USD Coin", decimals: 18, key: "usdc" }, rpcUrls: ["https://rpc.testnet.arc.network"], explorerUrl: "https://testnet.arcscan.app", testnet: true }, + // Arc mainnet went live on 2026-09-16 (docs.arc.io/arc/references/connect-to-arc: chain 5042, rpc.mainnet.arc.io, + // explorer.arc.io; eth_chainId 0x13b2 checked 2026-10-06). Native gas is USDC (18 decimals on the EVM side), Circle's own. + { slug: "arc", name: "Arc", chainId: 5042, native: { symbol: "USDC", name: "USD Coin", decimals: 18, key: "usdc" }, rpcUrls: ["https://rpc.mainnet.arc.io"], explorerUrl: "https://explorer.arc.io", testnet: false }, + { slug: "arc-testnet", name: "Arc Testnet", chainId: 5042002, native: { symbol: "USDC", name: "USD Coin", decimals: 18, key: "usdc" }, rpcUrls: ["https://rpc.testnet.arc.io", "https://rpc.testnet.arc.network"], explorerUrl: "https://explorer.testnet.arc.io", testnet: true }, { slug: "aurora", name: "Aurora", chainId: 1313161554, native: ETH, rpcUrls: ["https://mainnet.aurora.dev"], explorerUrl: "https://explorer.mainnet.aurora.dev", blockscout: "https://explorer.mainnet.aurora.dev", testnet: false }, { slug: "avalanche-c-chain", name: "Avalanche C-Chain", chainId: 43114, native: coin("AVAX", "Avalanche"), rpcUrls: ["https://api.avax.network/ext/bc/C/rpc", "https://avalanche-c-chain-rpc.publicnode.com"], explorerUrl: "https://snowtrace.io", testnet: false }, { slug: "bnb-smart-chain", name: "BNB Smart Chain", chainId: 56, native: coin("BNB", "BNB"), rpcUrls: ["https://bsc-dataseed.bnbchain.org", "https://bsc-rpc.publicnode.com"], explorerUrl: "https://bscscan.com", testnet: false }, @@ -97,9 +109,13 @@ export const EVM_NETWORK_SPECS: EvmNetworkSpec[] = [ { slug: "linea", name: "Linea", chainId: 59144, native: ETH, rpcUrls: ["https://rpc.linea.build"], explorerUrl: "https://lineascan.build", testnet: false }, { slug: "scroll", name: "Scroll", chainId: 534352, native: ETH, rpcUrls: ["https://rpc.scroll.io", "https://scroll-rpc.publicnode.com"], explorerUrl: "https://scrollscan.com", blockscout: "https://scroll.blockscout.com", testnet: false }, { slug: "morph", name: "Morph", chainId: 2818, native: ETH, rpcUrls: ["https://rpc.morphl2.io"], explorerUrl: "https://explorer.morphl2.io", testnet: false }, + // STRATO (mainnet "upquark"): eth_chainId 0x7030addddcf2, live 2026-10-06. Native USDST (18 decimals, token 0x937e…1010). + { slug: "strato", name: "STRATO", chainId: 123354377739506, native: { symbol: "USDST", name: "USDST", decimals: 18, key: "usdst" }, rpcUrls: ["https://noderpc.strato.nexus/rpc", "https://app.strato.nexus/rpc"], explorerUrl: "https://stratoscan.strato.nexus", testnet: false, legacyGas: true, flatFee: 10n ** 16n }, { slug: "etherlink", name: "Etherlink", chainId: 42793, native: coin("XTZ", "Tez"), rpcUrls: ["https://node.mainnet.etherlink.com"], explorerUrl: "https://explorer.etherlink.com", blockscout: "https://explorer.etherlink.com", testnet: false, evmLayerOf: "etherlink" }, /* ---------------------------------------------------------- testnets (default build) */ + // STRATO testnet "helium": eth_chainId 0xb165855668ca, live 2026-10-06. + { slug: "strato-helium", name: "STRATO Helium", chainId: 195049586845898, native: { symbol: "USDST", name: "Test USDST", decimals: 18, key: "usdst-testnet" }, rpcUrls: ["https://app.testnet.strato.nexus/rpc"], explorerUrl: "https://stratoscan.testnet.strato.nexus", testnet: true, legacyGas: true, flatFee: 10n ** 16n }, { slug: "sepolia", name: "Sepolia", chainId: 11155111, native: TEST_ETH, rpcUrls: ["https://ethereum-sepolia-rpc.publicnode.com", "https://11155111.rpc.thirdweb.com"], explorerUrl: "https://sepolia.etherscan.io", blockscout: "https://eth-sepolia.blockscout.com", testnet: true }, { slug: "base-sepolia", name: "Base Sepolia", chainId: 84532, native: TEST_ETH, rpcUrls: ["https://sepolia.base.org"], explorerUrl: "https://sepolia.basescan.org", blockscout: "https://base-sepolia.blockscout.com", testnet: true }, { slug: "arbitrum-sepolia", name: "Arbitrum Sepolia", chainId: 421614, native: TEST_ETH, rpcUrls: ["https://sepolia-rollup.arbitrum.io/rpc"], explorerUrl: "https://sepolia.arbiscan.io", blockscout: "https://arbitrum-sepolia.blockscout.com", testnet: true }, diff --git a/packages/chains-evm/test/decode.test.ts b/packages/chains-evm/test/decode.test.ts index 111485ca..e8a26157 100644 --- a/packages/chains-evm/test/decode.test.ts +++ b/packages/chains-evm/test/decode.test.ts @@ -329,3 +329,14 @@ describe("plain language", () => { for (const r of reqs) expect((await decode(r)).title).not.toMatch(NO_RAW_METHOD); }); }); + +describe("STRATO: flat per-transaction charge", () => { + it("adds 0.01 USDST to the (zero) gas fee and says so", async () => { + const STRATO = "eip155:123354377739506"; + const req: DappRequest = { ...tx({ to: BOB, value: numberToHex(10n ** 18n) }), networkId: STRATO }; + const d = await mod.decode(req, ctxFor(STRATO, mockFetch({ rpc: { ...SEPOLIA_STATE.rpc, eth_gasPrice: "0x0", eth_getBlockByNumber: { number: "0x1", timestamp: "0x1" }, eth_estimateGas: "0x5208", eth_chainId: "0x7030addddcf2" } }))); + expect(d.title).toBe("Send 1 USDST to 0x1234…5678"); + expect(d.fee).toMatchObject({ amount: "10000000000000000" }); + expect(d.lines.find((l) => l.label === "Network charge")?.value).toBe("0.01 USDST per transaction, even if it fails"); + }); +}); diff --git a/packages/chains-evm/test/registry.test.ts b/packages/chains-evm/test/registry.test.ts index 2f06318b..dbcc7877 100644 --- a/packages/chains-evm/test/registry.test.ts +++ b/packages/chains-evm/test/registry.test.ts @@ -15,21 +15,31 @@ describe("network registry", () => { } }); - it("covers 59 CLPR EVM networks plus the Sepolia testnets", () => { - expect(EVM_NETWORKS).toHaveLength(63); - expect(EVM_TESTNETS.map((n) => n.chainId).sort((a, b) => a! - b!)).toEqual([84532, 421614, 5042002, 11155111, 11155420]); + it("covers 61 CLPR EVM networks plus the Sepolia, Arc and STRATO testnets", () => { + expect(EVM_NETWORKS).toHaveLength(66); + expect(EVM_NETWORKS.filter((n) => !n.testnet)).toHaveLength(60); + expect(EVM_TESTNETS.map((n) => n.chainId).sort((a, b) => a! - b!)).toEqual([84532, 421614, 5042002, 11155111, 11155420, 195049586845898]); }); it("pins well-known chain ids", () => { const by = Object.fromEntries(EVM_NETWORK_SPECS.map((s) => [s.slug, s.chainId])); - expect(by).toMatchObject({ ethereum: 1, base: 8453, "arbitrum-one": 42161, "op-mainnet": 10, "bnb-smart-chain": 56, "polygon-pos": 137, "avalanche-c-chain": 43114, linea: 59144, scroll: 534352, "zksync-era": 324, monad: 143 }); + expect(by).toMatchObject({ ethereum: 1, base: 8453, "arbitrum-one": 42161, "op-mainnet": 10, "bnb-smart-chain": 56, "polygon-pos": 137, "avalanche-c-chain": 43114, linea: 59144, scroll: 534352, "zksync-era": 324, monad: 143, arc: 5042, "arc-testnet": 5042002, strato: 123354377739506 }); }); it("does not include Hedera (its own module) or non-EVM ledgers", () => { const ids = EVM_NETWORKS.map((n) => n.chainId); expect(ids).not.toContain(295); expect(ids).not.toContain(296); - expect(ids).not.toContain(123354377739506); // STRATO, SolidVM + }); + + it("STRATO: legacy transactions, unpriced gas plus a flat 0.01 USDST per transaction", async () => { + const { specFor } = await import("../src/networks.js"); + for (const id of ["eip155:123354377739506", "eip155:195049586845898"]) { + const s = specFor(id)!; + expect(s.legacyGas).toBe(true); + expect(s.flatFee).toBe(10n ** 16n); + expect(s.native).toMatchObject({ symbol: "USDST", decimals: 18 }); + } }); it("keeps Hedera's EVM apart: request-only networks 296/295, HBAR in weibars, specFor knows them", async () => { diff --git a/packages/chains-fuel/LICENSE b/packages/chains-fuel/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-fuel/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-fuel/NOTICE b/packages/chains-fuel/NOTICE new file mode 100644 index 00000000..dafb938a --- /dev/null +++ b/packages/chains-fuel/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-fuel +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-fuel/README.md b/packages/chains-fuel/README.md new file mode 100644 index 00000000..29ce6045 --- /dev/null +++ b/packages/chains-fuel/README.md @@ -0,0 +1,125 @@ +# @clip-wallet/chains-fuel + +Fuel `ChainModule` for Clip Wallet. It builds, decodes and dry-runs Fuel transactions, and never touches keys. +`prepare()` returns the 32-byte digest a Fuel signature covers (the transaction id, or fuels-ts' message hash). +`finalize()` checks the vault's signature with `secp256k1.verify`, makes sure its recovery id recovers the account's +key (Fuel verifies by recovery), writes it as fuels-ts does (r ‖ s with the recovery id in the top bit of s), puts it +in the account's witness slots and submits. + +No SDK at runtime: transactions are serialized by hand from the specs (`src/tx.ts`), fuel-core's GraphQL API is +called with plain `ctx.fetch` (`src/gql.ts`), and only `@noble/curves` / `@noble/hashes` are dependencies. `fuels` +(fuels-ts 0.103) is a devDependency: every serialization, transaction id, fee formula, address and message hash is +compared against it in the tests. + +## Networks + +| NetworkId | chain id | name | GraphQL (`rpcUrls[0]`) | explorer | +|---|---|---|---|---| +| `fuel:0` | 0 | Fuel Sepolia Testnet | https://testnet.fuel.network/v1/graphql | https://app-testnet.fuel.network | +| `fuel:9889` | 9889 | Ignition (mainnet) | https://mainnet.fuel.network/v1/graphql | https://app.fuel.network | + +- There's no ChainAgnostic CAIP-2 namespace for Fuel. The Fuel connector standard names networks by their numeric + chain id (fuels-ts `CHAIN_IDS.fuel`: mainnet 9889, testnet 0, devnet 1119889111; @fuels/connectors uses the same + table), so the ids are `fuel:`. +- Chain ids, names and base asset verified with `{ chain { name consensusParameters { chainId baseAssetId } } }` on + both endpoints (fuel-core 0.48.3, Oct 2026). +- `fuelNetOf()` accepts the id, the chain id (number or string) or a known GraphQL URL. + +Assets (Fuel's verified-assets list, https://verified-assets.fuel.network/assets.json): + +- The base asset is ETH (asset id `0xf8f8…ad07` on both networks, 9 decimals): key `eth` on mainnet, `eth-testnet` on + the testnet (the same keys chains-evm and chains-starknet use). +- USDC (`usdc.e`), USDT (`usdt.e`) and FUEL (`fuel`) are minted by Fuel's canonical bridge (contract `0x4ea6…d0e8` on + mainnet, `0xd021…5471` on testnet), so they're bridged copies (`bridged: true`, own keys). Testnet has USDC and FUEL. +- `getBalances` returns ETH (0 when empty) and the curated tokens only; anything else anyone can mint stays out. + `getNfts` returns `[]`. + +## Accounts and addresses + +- `derivationPath(i)` = `m/44'/1179993420'/i'/0/0` (Fuel Wallet, fuels-ts `WalletManager`), secp256k1. +- The address is SHA-256 of the 64-byte uncompressed key (no 0x04), shown with the fuels-ts checksum + (`Address.toChecksum`). The "abandon … about" accounts 0 and 1 are `0x806E…5b89` and `0x033d…5E17`, the same as + fuels-ts `Wallet.fromMnemonic`. +- `isAddress` takes 0x + 64 hex: all lower case or all upper case, or exactly the checksum (a mistyped case is + refused). The old bech32 `fuel1…` form was removed from fuels-ts and isn't accepted. +- One address for every Fuel network; no `receiveAddress` needed. + +## Requests (`FUEL_METHODS`) + +They mirror the signing calls of the Fuel connector standard, so 1Mask's Fuel connector passes them through. + +| method | params | result | +|---|---|---| +| `fuel_sendTransaction` | `{ address, transaction, provider?: { url } }` | the transaction id (`0x…`) after `submitAndAwaitStatus` | +| `fuel_signTransaction` | same | the signed request as fuels-ts TransactionRequest JSON (what Fuel Wallet returns; `transactionRequestify` reads it) | +| `fuel_signMessage` | `{ address, message }` | `0x` + 64-byte signature (r ‖ s, recovery id in s's top bit) | + +- `transaction` is `JSON.stringify(transactionRequest)` of a fuels-ts ScriptTransactionRequest (object or text), as + the Fuel Wallet connector sends it: BN fields as 0x-hex, bytes as 0x-hex. The request's `flag` (the dapp's own + dry-run summary) is ignored. +- `message` is `{ text }` (a plain string), `{ personalSign }` (text) or `{ personalSignHex }` (bytes). fuels-ts + `hashMessage`: a plain string is SHA-256 of its UTF-8 with no prefix; `personalSign` is SHA-256 of + `"\x19Fuel Signed Message:\n" + length + bytes`. +- `address`, when given, must be this account. A `provider.url` that is another Fuel network's endpoint is refused + (`fuel/network-mismatch`), as is a request whose `networkId` isn't the connected network. +- `buildTransfer` returns `fuel_sendTransaction` with `origin: WALLET_ORIGIN`. + +## Building a transfer + +`buildTransfer({ asset, to, amount })` (ETH or a token by asset id): + +1. `chain { consensusParameters }` (checked against the network's chain id) and `estimateGasPrice(blockHorizon: "10")`. +2. `coinsToSpend` for the amount (+ the fee budget in ETH). Coins and message coins become inputs with witness 0. +3. Outputs: a Coin output to the recipient, a Change output to you for the asset, and one for ETH when sending a token. +4. Script: fuels-ts' `returnZeroScript` (`0x24000000`, RET $zero). Its gas is measured with a dry run at gas price 0 + (64 gas today); that becomes the script gas limit. +5. `max_fee` from the spec formula (`src/fee.ts`, equal to fuels-ts `getMinGas`/`getMaxGas`) with a 64-byte witness + placeholder: `ceil(max_gas · gas_price / gas_price_factor) + tip + 1`. If the coins don't cover amount + fee, it + asks again with a bigger budget (four rounds at most). + +The transaction id is SHA-256(chain id u64 ‖ the transaction with receipts root, tx pointers, predicate gas used, +contract input/output roots, change and variable output amounts zeroed and no witnesses). A transfer built this way +on the testnet was signed with the vault's `signEcdsa` and passed `dryRun(utxoValidation: true)`, and one went +through fuels-ts `Fuel` → 1Mask's connector → this module → `submitAndAwaitStatus` (testnet tx +`0x5717…9db6`). + +## decode() + +- Your coins are coin inputs you own (no predicate) and message coins addressed to you; you sign the witness slots + they name. No such input → `fuel/not-a-signer`. +- Script `0x24000000` with no script data, no contract inputs and no data messages (a plain transfer): fully read + from inputs and outputs, no network call. "Send 1.5 USDC to 0x033d…5e17", one "To" line per Coin output, "Network + fee at most" (the `max_fee` policy), balance changes = what leaves you minus Coin outputs to you and your Change. +- Any other script, or contract inputs: the outcome isn't knowable from outputs alone, so it's dry-run + (`dryRun(utxoValidation: false, gasPrice: 0)`). CALL / TRANSFER / TRANSFER_OUT receipts from the script itself + are what it spends; TRANSFER_OUT to you is what contracts send you. Title "Approve a smart contract action for + ", a "Contract" line per contract, `simulated: true`, caution `unknown-call`. +- Dry run fails, or `simulate: false` → `blind: true` with `simulation-failed` (the node's raw reason isn't shown). +- Predicate inputs, or a transaction that isn't a Script (Create, Upgrade, Upload, Blob) → `blind: true`. +- A Change output for your asset that goes to someone else → danger ("Everything left of your ETH after this goes + to …"). Your asset with no Change output → danger with what would be lost. +- `max_fee` above 0.001 ETH → caution `high-fee` (danger above 0.01 ETH). `expiration` → "Valid until block N". +- `fuel_signTransaction` adds "Sent by (it gets the signed transaction)". +- Messages: "Sign a message for " with the text. A plain string that isn't readable text is blind: it is hashed + with no prefix, and readable text can't be the preimage of a transaction id (that starts with the 8-byte chain id, + whose first byte is 0x00). `personalSign` bytes are shown as hex with a warning (the prefix keeps them from being + a transaction). + +## Errors + +Plain words with `fuel/…` codes, never the node's text: `fuel/insufficient-funds`, `fuel/bad-address`, +`fuel/self-transfer`, `fuel/bad-amount`, `fuel/network-mismatch`, `fuel/not-your-account`, `fuel/not-a-signer`, +`fuel/malformed`, `fuel/unsupported-method`, `fuel/bad-signature`, `fuel/coins-spent` (a UTXO was already spent), +`fuel/fee-too-low` (`InsufficientMaxFee`), `fuel/failed` (reverted on chain: only the fee was spent), +`fuel/rejected`, `fuel/offline`, `fuel/too-many-inputs`. + +## Dapps + +1Mask's Fuel connector (`packages/1mask/src/inpage/fuel.ts`) implements the Fuel connector standard (fuels-ts +`FuelConnector`) without bundling fuels-ts, announces itself with the `FuelConnector` window event under the wallet's +own name and icon, and is also at `window.clipwallet.fuel`. `test/connector.test.ts` runs it under the real fuels-ts +`Fuel` manager with this module behind it. + +## Testnet faucet + +https://faucet-testnet.fuel.network/ (behind a Cloudflare interactive challenge, so it needs a person in a browser). diff --git a/packages/chains-fuel/package.json b/packages/chains-fuel/package.json new file mode 100644 index 00000000..b69ddb19 --- /dev/null +++ b/packages/chains-fuel/package.json @@ -0,0 +1,77 @@ +{ + "name": "@clip-wallet/chains-fuel", + "version": "0.1.0", + "description": "Fuel ChainModule for Clip Wallet: builds and decodes transactions in plain words, dry-runs contract calls; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "fuel", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-fuel#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-fuel" + }, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0" + }, + "devDependencies": { + "fuels": "0.103.0" + } +} diff --git a/packages/chains-fuel/src/address.ts b/packages/chains-fuel/src/address.ts new file mode 100644 index 00000000..267dd984 --- /dev/null +++ b/packages/chains-fuel/src/address.ts @@ -0,0 +1,56 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { fromHex, hex, utf8 } from "./util.js"; + +/** + * Fuel addresses (fuels-ts @fuel-ts/address + Signer): the address of a secp256k1 key is SHA-256 of its 64-byte + * uncompressed form without the 0x04 prefix, written as 0x-hex with fuels-ts' checksum (`Address.toChecksum`): a hex + * letter is upper case where the matching nibble of SHA-256 of the lower-case hex TEXT (no "0x") is 8 or more. Same as + * the vault's `fuelAddress` (packages/vault/src/encodings87.ts), recomputed here because chain modules never import + * the vault. + */ +export function addressBytesFromPublicKey(publicKey: Uint8Array): Uint8Array { + const uncompressed = publicKey.length === 65 ? publicKey : secp256k1.Point.fromBytes(publicKey).toBytes(false); + if (uncompressed.length !== 65 || uncompressed[0] !== 4) throw new Error("secp256k1 public key expected"); + return sha256(uncompressed.subarray(1)); +} + +export function toChecksum(address: string): string { + const lower = address.toLowerCase().replace(/^0x/, ""); + if (!/^[0-9a-f]{64}$/.test(lower)) throw new Error("fuel addresses are 32 bytes"); + const h = sha256(utf8(lower)); + let out = "0x"; + for (let i = 0; i < 32; i++) { + const byte = h[i]!; + out += byte >> 4 >= 8 ? lower[i * 2]!.toUpperCase() : lower[i * 2]!; + out += (byte & 0x0f) >= 8 ? lower[i * 2 + 1]!.toUpperCase() : lower[i * 2 + 1]!; + } + return out; +} + +export function fuelAddressFromPublicKey(publicKey: Uint8Array): string { + return toChecksum(hex(addressBytesFromPublicKey(publicKey))); +} + +/** + * A Fuel address: 0x + 64 hex digits. All-lower-case or all-upper-case is accepted (no checksum to check, as + * EIP-55); mixed case must be the fuels-ts checksum exactly, so a typo in a copied address is caught. The old bech32 + * `fuel1…` form was removed from fuels-ts (v0.94) and isn't accepted. + */ +export function isFuelAddress(value: string): boolean { + const v = value.trim(); + if (!/^0x[0-9a-fA-F]{64}$/.test(v)) return false; + const body = v.slice(2); + if (body === body.toLowerCase() || body === body.toUpperCase()) return true; + return toChecksum(v) === v; +} + +/** Lower-case b256 for comparisons and the wire; throws for anything that isn't a Fuel address. */ +export function b256(value: string): string { + if (!isFuelAddress(value)) throw new Error("not a fuel address"); + return value.trim().toLowerCase(); +} + +export function b256Bytes(value: string): Uint8Array { + return fromHex(b256(value)); +} diff --git a/packages/chains-fuel/src/describe.ts b/packages/chains-fuel/src/describe.ts new file mode 100644 index 00000000..c6b7e57f --- /dev/null +++ b/packages/chains-fuel/src/describe.ts @@ -0,0 +1,202 @@ +import { type AssetRef, type BalanceChange, type Msg, type NetworkId, type Warning, msg, say } from "@clip-wallet/core"; +import type { Receipt } from "./gql.js"; +import { assetFor } from "./networks.js"; +import { type ScriptTx, RETURN_ZERO_SCRIPT, ZERO32 } from "./tx.js"; +import { formatUnits, hex0x, short } from "./util.js"; + +/** + * What a Script transaction does to this account, from what can be proven: + * - Inputs and outputs prove the plain coin flows: what you put in (your coin inputs), Coin outputs (fixed amounts + * to fixed owners) and Change outputs (whatever is left of an asset after the script runs and the fee is paid). + * - A script other than fuels-ts' "return 0" (or any contract input) can move coins on its own: forward them into + * contract calls (CALL receipts), transfer them (TRANSFER / TRANSFER_OUT) or receive some back from contracts + * (TRANSFER_OUT to you, into Variable outputs). Those flows only show up when the transaction is run, so the + * module dry-runs it and reads the receipts (`simulated: true`). + * - Predicate inputs (programs that own coins) and non-Script transactions aren't explained: blind. + */ + +export interface Analysis { + /** Asset id (lower case) → base units. */ + mineIn: Map; + /** Witness slots this account signs. */ + myWitnesses: Set; + sends: { to: string; assetId: string; amount: bigint }[]; + toMe: Map; + /** Change output recipient per asset. */ + change: Map; + /** Sum of all inputs (anyone's) and Coin outputs per asset, for the leftover. */ + allIn: Map; + coinOut: Map; + contracts: string[]; + predicates: number; + /** Message inputs that carry data (bridge messages for a script to consume). */ + dataMessages: number; + othersSign: boolean; + plainScript: boolean; +} + +const add = (m: Map, k: string, v: bigint) => m.set(k, (m.get(k) ?? 0n) + v); + +export function analyze(tx: ScriptTx, me: string): Analysis { + const a: Analysis = { + mineIn: new Map(), + myWitnesses: new Set(), + sends: [], + toMe: new Map(), + change: new Map(), + allIn: new Map(), + coinOut: new Map(), + contracts: [], + predicates: 0, + dataMessages: 0, + othersSign: false, + plainScript: false, + }; + for (const i of tx.inputs) { + if (i.type === 1) { + a.contracts.push(i.contractId); + continue; + } + if (i.predicate.length) a.predicates++; + if (i.type === 0) { + add(a.allIn, i.assetId, i.amount); + if (!i.predicate.length && i.owner === me) { + add(a.mineIn, i.assetId, i.amount); + a.myWitnesses.add(i.witnessIndex); + } else if (!i.predicate.length) a.othersSign = true; + } else { + if (i.data.length) a.dataMessages++; + // A message coin (no data) spends like a base-asset coin of its recipient. + if (!i.data.length) add(a.allIn, "base", i.amount); + if (!i.predicate.length && i.recipient === me) { + if (!i.data.length) add(a.mineIn, "base", i.amount); + a.myWitnesses.add(i.witnessIndex); + } else if (!i.predicate.length) a.othersSign = true; + } + } + for (const o of tx.outputs) { + if (o.type === 0) { + add(a.coinOut, o.assetId, o.amount); + if (o.to === me) add(a.toMe, o.assetId, o.amount); + else a.sends.push({ to: o.to, assetId: o.assetId, amount: o.amount }); + } else if (o.type === 2) a.change.set(o.assetId, o.to); + } + a.plainScript = hex0x(tx.script) === RETURN_ZERO_SCRIPT && tx.scriptData.length === 0 && a.contracts.length === 0 && a.dataMessages === 0; + return a; +} + +/** Moves from the script's own balances (receipt `id` empty or zero) and transfers to `me`, read from receipts. */ +export interface Simulated { + /** Coins the script forwarded to contracts or sent out itself, per asset. */ + spent: Map; + /** Coins contracts sent to this account (TRANSFER_OUT to you), per asset. */ + received: Map; + calls: string[]; +} + +const fromScript = (r: Receipt) => !r.id || r.id === ZERO32; + +export function readReceipts(receipts: Receipt[], me: string): Simulated { + const s: Simulated = { spent: new Map(), received: new Map(), calls: [] }; + for (const r of receipts) { + const amount = BigInt(r.amount ?? "0"); + const asset = (r.assetId ?? "").toLowerCase(); + switch (r.receiptType) { + case "CALL": + if (r.to && !s.calls.includes(r.to.toLowerCase())) s.calls.push(r.to.toLowerCase()); + if (fromScript(r) && amount > 0n) add(s.spent, asset, amount); + break; + case "TRANSFER": + if (fromScript(r) && amount > 0n) add(s.spent, asset, amount); + break; + case "TRANSFER_OUT": + if (fromScript(r) && amount > 0n) add(s.spent, asset, amount); + if ((r.toAddress ?? "").toLowerCase() === me && amount > 0n) add(s.received, asset, amount); + break; + } + } + return s; +} + +export interface Described { + title: string; + titleMsg?: Msg; + lines: { label: string; value: string }[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + simulated: boolean; + blind: boolean; +} + +export const amountText = (asset: AssetRef, v: bigint) => `${formatUnits(v, asset.decimals)} ${asset.symbol}`; + +/** + * Balance changes for this account, the network fee excluded (it is shown on its own): what you put in comes back as + * Coin outputs to you, coins contracts sent you, and the leftover of each asset when its Change output is yours. + * Leftover = everything put in − Coin outputs − what the script spent (simulation); the fee is taken from the base + * asset's leftover by the network. + */ +export function describeFlows( + tx: ScriptTx, + a: Analysis, + sim: Simulated | null, + ctx: { networkId: NetworkId; me: string; baseAssetId: string; maxFee: bigint }, +): { balanceChanges: BalanceChange[]; warnings: Warning[]; lines: { label: string; value: string }[] } { + const norm = (k: string) => (k === "base" ? ctx.baseAssetId : k); + const mineIn = new Map(); + for (const [k, v] of a.mineIn) add(mineIn, norm(k), v); + const allIn = new Map(); + for (const [k, v] of a.allIn) add(allIn, norm(k), v); + const assets = new Set([...mineIn.keys(), ...a.toMe.keys(), ...(sim ? sim.received.keys() : [])]); + const warnings: Warning[] = []; + const lines: { label: string; value: string }[] = []; + const changes: BalanceChange[] = []; + for (const id of assets) { + const asset = assetFor(ctx.networkId, id); + const leftover = (allIn.get(id) ?? 0n) - (a.coinOut.get(id) ?? 0n) - (sim?.spent.get(id) ?? 0n); + let delta = -(mineIn.get(id) ?? 0n) + (a.toMe.get(id) ?? 0n) + (sim?.received.get(id) ?? 0n); + const changeTo = a.change.get(id); + if (leftover > 0n) { + if (changeTo === ctx.me) delta += leftover; + else if (mineIn.get(id)) { + if (changeTo) { + const m = msg("bg.fuel.changeToOther", { symbol: asset.symbol, to: short(changeTo) }); + warnings.push({ level: "danger", code: "malicious-transaction", message: m.fallback, msg: m }); + } else { + // No Change output: what's left after the fee isn't sent anywhere and is lost. + const lost = id === ctx.baseAssetId ? leftover - ctx.maxFee : leftover; + if (lost > 0n) { + const m = msg("bg.fuel.leftoverLost", { amount: amountText(asset, lost) }); + warnings.push({ level: "danger", code: "malicious-transaction", message: m.fallback, msg: m }); + } + } + } + } + if (delta !== 0n) changes.push({ asset, delta: delta.toString() }); + } + return { balanceChanges: changes, warnings, lines }; +} + +/** "Send 0.5 ETH to 0x033d…5e17" when exactly one asset leaves to exactly one other owner, else a general title. */ +export function transferTitle(a: Analysis, networkId: NetworkId): { title: string; titleMsg?: Msg } | null { + if (!a.sends.length) return null; + const to = new Set(a.sends.map((s) => s.to)); + const assets = new Set(a.sends.map((s) => s.assetId)); + if (to.size === 1 && assets.size === 1) { + const asset = assetFor(networkId, a.sends[0]!.assetId); + const total = a.sends.reduce((t, s) => t + s.amount, 0n); + const m = msg("bg.req.sendTo", { amount: amountText(asset, total), to: short(a.sends[0]!.to) }); + return { title: m.fallback, titleMsg: m }; + } + const m = msg("bg.req.sendToCount", { count: to.size }); + return { title: m.fallback, titleMsg: m }; +} + +export function sendLines(a: Analysis, networkId: NetworkId): { label: string; value: string }[] { + return a.sends.map((s) => ({ label: "To", value: `${s.to} (${amountText(assetFor(networkId, s.assetId), s.amount)})` })); +} + +/** The dry run failed or couldn't run: the network's reason is raw node text, so it isn't shown. */ +export function simulationFailed(): Warning { + return { level: "danger", code: "simulation-failed", message: say("bg.warn.cantTestRun") }; +} diff --git a/packages/chains-fuel/src/fee.ts b/packages/chains-fuel/src/fee.ts new file mode 100644 index 00000000..083c9c06 --- /dev/null +++ b/packages/chains-fuel/src/fee.ts @@ -0,0 +1,57 @@ +import type { ChainInfo, DependentCost } from "./gql.js"; +import { encodeScriptTx, type ScriptTx } from "./tx.js"; + +/** + * Fee maths for Script transactions (specs: tx-validity "Max fee"; fuel-vm `TransactionFee`; fuels-ts + * providers/utils/gas.ts `getMinGas` / `getMaxGas` / `calculateGasFee`, which the tests cross-check): + * + * min_gas = vm_init(size) + size·gas_per_byte + Σ inputs (ecr1 once per distinct witness; predicates: + * vm_init(size) + contract_root(len(predicate)) + predicate_gas_used) + s256(size) [tx id] + * max_gas = min_gas + (witness_limit − witness bytes)·gas_per_byte (when a witness limit is set) + script_gas_limit + * fee(gas) = gas·gas_price / gas_price_factor (rounded up by fuel-vm) + tip + * + * `size` is the whole serialized transaction, witnesses included. + */ + +export function dependentCost(units: bigint, c: DependentCost): bigint { + const base = BigInt(c.base); + if (c.unitsPerGas !== undefined) return base + units / BigInt(c.unitsPerGas); + return base + units * BigInt(c.gasPerUnit ?? 0); +} + +export function minGas(tx: ScriptTx, chain: ChainInfo): bigint { + const size = BigInt(encodeScriptTx(tx).length); + const g = chain.gasCosts; + const vmInit = dependentCost(size, g.vmInitialization); + let inputs = 0n; + const seen = new Set(); + for (const i of tx.inputs) { + if (i.type === 1) continue; + if (i.predicate.length) { + inputs += vmInit + dependentCost(BigInt(i.predicate.length), g.contractRoot) + i.predicateGasUsed; + } else if (!seen.has(i.witnessIndex)) { + seen.add(i.witnessIndex); + inputs += g.ecr1; + } + } + return vmInit + size * chain.gasPerByte + inputs + dependentCost(size, g.s256); +} + +export function maxGas(tx: ScriptTx, chain: ChainInfo, min = minGas(tx, chain)): bigint { + const witnessBytes = BigInt(tx.witnesses.reduce((a, w) => a + w.length, 0)); + const wl = tx.policies.witnessLimit; + const extra = wl !== undefined && wl > 0n && wl >= witnessBytes ? (wl - witnessBytes) * chain.gasPerByte : 0n; + const max = min + extra + tx.scriptGasLimit; + return max > chain.maxGasPerTx ? chain.maxGasPerTx : max; +} + +/** fuel-vm gas_to_fee: ceil(gas · price / factor). */ +export function gasToFee(gas: bigint, gasPrice: bigint, factor: bigint): bigint { + const n = gas * gasPrice; + return (n + factor - 1n) / factor; +} + +/** The smallest max_fee the node accepts for `tx` at `gasPrice` (fuels-ts adds 1 to its floor-division estimate). */ +export function requiredMaxFee(tx: ScriptTx, chain: ChainInfo, gasPrice: bigint): bigint { + return gasToFee(maxGas(tx, chain), gasPrice, chain.gasPriceFactor) + (tx.policies.tip ?? 0n); +} diff --git a/packages/chains-fuel/src/gql.ts b/packages/chains-fuel/src/gql.ts new file mode 100644 index 00000000..0eb4d9af --- /dev/null +++ b/packages/chains-fuel/src/gql.ts @@ -0,0 +1,230 @@ +import { ClipError, type ChainContext } from "@clip-wallet/core"; +import { specFor } from "./networks.js"; + +/** + * A small fuel-core GraphQL client over `ctx.fetch` (no fuels-ts at runtime). Queries are the ones fuels-ts sends + * (@fuel-ts/account providers/operations.graphql), trimmed to the fields used here. Subscriptions are served as + * server-sent events on `-sub` (fuels-ts FuelGraphqlSubscriber). + */ + +export class GqlError extends Error { + constructor( + message: string, + public readonly kind: "graphql" | "http" | "offline", + ) { + super(message); + } +} + +export interface DependentCost { + base: string; + unitsPerGas?: string; + gasPerUnit?: string; +} + +export interface ChainInfo { + chainId: number; + baseAssetId: string; + maxInputs: number; + maxOutputs: number; + maxGasPerTx: bigint; + maxSize: bigint; + gasPriceFactor: bigint; + gasPerByte: bigint; + gasCosts: { ecr1: bigint; vmInitialization: DependentCost; s256: DependentCost; contractRoot: DependentCost }; +} + +export interface Receipt { + receiptType: string; + id?: string | null; + to?: string | null; + toAddress?: string | null; + amount?: string | null; + assetId?: string | null; + contractId?: string | null; + subId?: string | null; + val?: string | null; + gasUsed?: string | null; + result?: string | null; + reason?: string | null; + sender?: string | null; + recipient?: string | null; +} + +export interface DryRunResult { + id: string; + status: { type: "DryRunSuccessStatus" | "DryRunFailureStatus"; totalGas: string; totalFee: string; reason?: string }; + receipts: Receipt[]; +} + +export type Spendable = + | { type: "Coin"; utxoId: string; amount: string; assetId: string; owner: string } + | { type: "MessageCoin"; nonce: string; amount: string; assetId: string; sender: string; recipient: string }; + +export type FinalStatus = + | { type: "SuccessStatus"; id?: string } + | { type: "FailureStatus"; reason: string } + | { type: "SqueezedOutStatus"; reason: string } + /** The stream ended after the node accepted the transaction but before a final status. */ + | { type: "SubmittedStatus" }; + +const DEP = "... on LightOperation { base unitsPerGas } ... on HeavyOperation { base gasPerUnit }"; +const CHAIN_QUERY = `query getChain { chain { consensusParameters { chainId baseAssetId txParams { maxInputs maxOutputs maxGasPerTx maxSize } feeParams { gasPriceFactor gasPerByte } gasCosts { ecr1 vmInitialization { ${DEP} } s256 { ${DEP} } contractRoot { ${DEP} } } } } }`; +const RECEIPT = "receiptType id to toAddress amount assetId contractId subId val gasUsed result reason sender recipient"; + +export function graphqlUrl(ctx: ChainContext): string { + const url = ctx.network.rpcUrls[0] ?? specFor(ctx.network.id)?.graphql; + if (!url) throw new ClipError("No connection is set up for this network.", "fuel/no-rpc"); + return url; +} + +export function gqlFor(ctx: ChainContext, opts: { timeoutMs?: number } = {}) { + const url = graphqlUrl(ctx); + const f = ctx.fetch; + + async function query(q: string, variables?: Record): Promise { + let res: Response; + try { + res = await f(url, { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify(variables ? { query: q, variables } : { query: q }), + }); + } catch (e) { + throw new GqlError(String((e as Error)?.message ?? e), "offline"); + } + let body: { data?: T; errors?: { message?: string }[] }; + try { + body = (await res.json()) as typeof body; + } catch { + throw new GqlError(`HTTP ${res.status}`, "http"); + } + if (body.errors?.length) throw new GqlError(body.errors.map((e) => e.message ?? "").join("; "), "graphql"); + if (!res.ok || !body.data) throw new GqlError(`HTTP ${res.status}`, "http"); + return body.data; + } + + let chain: Promise | undefined; + + return { + query, + + chainInfo(): Promise { + chain ??= query<{ chain: { consensusParameters: Record } }>(CHAIN_QUERY).then(({ chain: c }) => { + const p = c.consensusParameters; + return { + chainId: Number(p.chainId), + baseAssetId: String(p.baseAssetId).toLowerCase(), + maxInputs: Number(p.txParams.maxInputs), + maxOutputs: Number(p.txParams.maxOutputs), + maxGasPerTx: BigInt(p.txParams.maxGasPerTx), + maxSize: BigInt(p.txParams.maxSize), + gasPriceFactor: BigInt(p.feeParams.gasPriceFactor), + gasPerByte: BigInt(p.feeParams.gasPerByte), + gasCosts: { + ecr1: BigInt(p.gasCosts.ecr1), + vmInitialization: p.gasCosts.vmInitialization, + s256: p.gasCosts.s256, + contractRoot: p.gasCosts.contractRoot, + }, + }; + }); + chain.catch(() => (chain = undefined)); + return chain; + }, + + /** fuels-ts `estimateGasPrice(10)`: the gas price expected 10 blocks ahead (U32 is passed as a string). */ + async estimateGasPrice(blockHorizon = 10): Promise { + const d = await query<{ estimateGasPrice: { gasPrice: string } }>("query estimateGasPrice($h: U32!) { estimateGasPrice(blockHorizon: $h) { gasPrice } }", { h: String(blockHorizon) }); + return BigInt(d.estimateGasPrice.gasPrice); + }, + + async balances(owner: string): Promise<{ assetId: string; amount: string }[]> { + const d = await query<{ balances: { nodes: { assetId: string; amount: string }[] } }>( + "query getBalances($owner: Address!) { balances(filter: { owner: $owner }, first: 100) { nodes { assetId amount } } }", + { owner }, + ); + return d.balances.nodes; + }, + + async coinsToSpend(owner: string, want: { assetId: string; amount: bigint }[], excludedUtxos: string[] = []): Promise { + const d = await query<{ coinsToSpend: (Record & { __typename: string })[][] }>( + "query getCoinsToSpend($owner: Address!, $q: [SpendQueryElementInput!]!, $ex: ExcludeInput) { coinsToSpend(owner: $owner, queryPerAsset: $q, excludedIds: $ex) { __typename ... on Coin { utxoId amount assetId owner } ... on MessageCoin { nonce amount assetId sender recipient } } }", + { owner, q: want.map((w) => ({ assetId: w.assetId, amount: w.amount.toString() })), ex: { utxos: excludedUtxos, messages: [] } }, + ); + return d.coinsToSpend.map((list) => list.map((c) => ({ ...c, type: c.__typename }) as unknown as Spendable)); + }, + + /** `gasPrice` overrides the price the node checks max_fee against ("0": measure gas without a fee). */ + async dryRun(txHex: string, utxoValidation = false, gasPrice?: bigint): Promise { + const d = await query<{ dryRun: { id: string; status: Record; receipts: Receipt[] }[] }>( + `query dryRun($txs: [HexString!]!, $v: Boolean, $p: U64) { dryRun(txs: $txs, utxoValidation: $v, gasPrice: $p) { id status { type: __typename ... on DryRunSuccessStatus { totalGas totalFee } ... on DryRunFailureStatus { totalGas totalFee reason } } receipts { ${RECEIPT} } } }`, + { txs: [txHex], v: utxoValidation, ...(gasPrice !== undefined ? { p: gasPrice.toString() } : {}) }, + ); + const r = d.dryRun[0]; + if (!r) throw new GqlError("empty dry run", "graphql"); + return r as unknown as DryRunResult; + }, + + /** + * `submitAndAwaitStatus` (server-sent events on `-sub`): resolves with the final status (success, failure or + * squeezed out), or SubmittedStatus when the stream ends early (the caller then polls `transactionStatus`). + */ + async submitAndAwait(txHex: string): Promise { + const STATUS = "type: __typename ... on SuccessStatus { transactionId } ... on FailureStatus { reason } ... on SqueezedOutStatus { reason }"; + let res: Response; + try { + res = await f(`${url}-sub`, { + method: "POST", + headers: { "content-type": "application/json", accept: "text/event-stream" }, + body: JSON.stringify({ + query: `subscription submitAndAwaitStatus($tx: HexString!) { submitAndAwaitStatus(tx: $tx) { ${STATUS} } }`, + variables: { tx: txHex }, + operationName: "submitAndAwaitStatus", + }), + }); + } catch (e) { + throw new GqlError(String((e as Error)?.message ?? e), "offline"); + } + if (!res.ok || !res.body) throw new GqlError(`HTTP ${res.status}`, "http"); + const reader = res.body.getReader(); + const dec = new TextDecoder(); + let text = ""; + let submitted = false; + for (;;) { + const m = /data:(.*)\n\n/.exec(text); + if (m) { + text = text.slice(m.index + m[0].length); + let ev: { data?: { submitAndAwaitStatus?: { type: string; reason?: string; transactionId?: string } }; errors?: { message?: string }[] }; + try { + ev = JSON.parse(m[1]!); + } catch { + throw new GqlError("unreadable status stream", "http"); + } + if (ev.errors?.length) throw new GqlError(ev.errors.map((e) => e.message ?? "").join("; "), "graphql"); + const s = ev.data?.submitAndAwaitStatus; + if (!s) continue; + submitted = true; + if (s.type === "SuccessStatus") return { type: "SuccessStatus", ...(s.transactionId ? { id: s.transactionId } : {}) }; + if (s.type === "FailureStatus" || s.type === "SqueezedOutStatus") return { type: s.type, reason: s.reason ?? "" } as FinalStatus; + continue; // SubmittedStatus / preconfirmations: keep reading + } + const { value, done } = await reader.read(); + if (done) break; + text += dec.decode(value, { stream: true }).replace(/:keep-alive-text\n\n/g, ""); + } + if (!submitted) throw new GqlError("the status stream ended before the node answered", "http"); + return { type: "SubmittedStatus" }; + }, + + async transactionStatus(id: string): Promise<{ type: string; reason?: string } | null> { + const d = await query<{ transaction: { status: { type: string; reason?: string } | null } | null }>( + "query getTransaction($id: TransactionId!) { transaction(id: $id) { status { type: __typename ... on FailureStatus { reason } ... on SqueezedOutStatus { reason } } } }", + { id }, + ); + return d.transaction?.status ?? null; + }, + }; +} + +export type Gql = ReturnType; diff --git a/packages/chains-fuel/src/index.ts b/packages/chains-fuel/src/index.ts new file mode 100644 index 00000000..05e2e68c --- /dev/null +++ b/packages/chains-fuel/src/index.ts @@ -0,0 +1,37 @@ +export { FUEL_METHODS, type FuelModuleOptions, createFuelModule, fuelCompactSignature, normalize, plainFuelError } from "./module.js"; +export { + BASE_ASSET_ID, + FUEL_MAINNET, + FUEL_NETS, + FUEL_NETWORKS, + FUEL_TESTNET, + type FuelNet, + type FuelNetSpec, + type FuelToken, + assetFor, + connectorNetwork, + curatedToken, + ethAsset, + explorerTxUrl, + fuelNetOf, + specFor, +} from "./networks.js"; +export { addressBytesFromPublicKey, fuelAddressFromPublicKey, isFuelAddress, toChecksum } from "./address.js"; +export { type FuelMessage, MESSAGE_PREFIX, fuelMessageOf, hashMessage } from "./message.js"; +export { + type Input, + type Output, + type ScriptTx, + type TransactionRequestJson, + RETURN_ZERO_SCRIPT, + encodeScriptTx, + parseTransactionRequest, + toTransactionRequestJson, + transactionId, +} from "./tx.js"; +export { gasToFee, maxGas, minGas, requiredMaxFee } from "./fee.js"; + +import { createFuelModule } from "./module.js"; + +/** Default instance. */ +export const fuelModule = createFuelModule(); diff --git a/packages/chains-fuel/src/message.ts b/packages/chains-fuel/src/message.ts new file mode 100644 index 00000000..ab42cd34 --- /dev/null +++ b/packages/chains-fuel/src/message.ts @@ -0,0 +1,41 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { concat, fromHex, utf8 } from "./util.js"; + +/** + * Message signing, as fuels-ts `hashMessage` (@fuel-ts/hasher hasher.ts, v0.103): + * - a plain string: SHA-256 of its UTF-8 bytes (no prefix); + * - `{ personalSign }`: SHA-256 of "\x19Fuel Signed Message:\n" + decimal length + the message bytes (EIP-191 style); + * a string `personalSign` is UTF-8 text, bytes are taken as they are. + * The signature is fuels-ts `Signer.sign`'s: r ‖ s (64 bytes) with the recovery id in the top bit of s. + * + * On the wire (1Mask and DappRequest params), the message is one of: + * { text: string } a HashableMessage string + * { personalSign: string } { personalSign: string } + * { personalSignHex: "0x…" } { personalSign: Uint8Array } (JSON can't carry bytes) + */ +export type FuelMessage = { text: string } | { personalSign: string } | { personalSignHex: string }; + +export const MESSAGE_PREFIX = "\x19Fuel Signed Message:\n"; + +export function messageBytes(m: FuelMessage): Uint8Array { + if ("text" in m) return utf8(m.text); + if ("personalSign" in m) return utf8(m.personalSign); + return fromHex(m.personalSignHex); +} + +export function hashMessage(m: FuelMessage): Uint8Array { + const bytes = messageBytes(m); + if ("text" in m) return sha256(bytes); + return sha256(concat(utf8(MESSAGE_PREFIX), utf8(String(bytes.length)), bytes)); +} + +/** Reads the wire form, or a fuels-ts HashableMessage passed straight through (a string or `{ personalSign }`). */ +export function fuelMessageOf(v: unknown): FuelMessage | null { + if (typeof v === "string") return { text: v }; + if (!v || typeof v !== "object" || Array.isArray(v)) return null; + const o = v as Record; + if (typeof o.text === "string") return { text: o.text }; + if (typeof o.personalSign === "string") return { personalSign: o.personalSign }; + if (typeof o.personalSignHex === "string" && /^0x([0-9a-fA-F]{2})*$/.test(o.personalSignHex)) return { personalSignHex: o.personalSignHex }; + return null; +} diff --git a/packages/chains-fuel/src/module.ts b/packages/chains-fuel/src/module.ts new file mode 100644 index 00000000..b86519e2 --- /dev/null +++ b/packages/chains-fuel/src/module.ts @@ -0,0 +1,508 @@ +import { + type AssetRef, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Msg, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + type Warning, + WALLET_ORIGIN, + isWalletOrigin, + msg, + say, + titled, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { fuelAddressFromPublicKey, isFuelAddress } from "./address.js"; +import { amountText, analyze, describeFlows, readReceipts, sendLines, simulationFailed, transferTitle, type Analysis, type Simulated } from "./describe.js"; +import { requiredMaxFee } from "./fee.js"; +import { type ChainInfo, GqlError, type Spendable, gqlFor } from "./gql.js"; +import { type FuelMessage, fuelMessageOf, hashMessage, messageBytes } from "./message.js"; +import { FUEL_NETS, assetFor, ethAsset, fuelNetOf, specFor } from "./networks.js"; +import { + type Input, + type Output, + RETURN_ZERO_SCRIPT, + type ScriptTx, + TxParseError, + ZERO32, + encodeScriptTx, + parseTransactionRequest, + toTransactionRequestJson, + transactionId, +} from "./tx.js"; +import { fromHex, hex0x, hostOf, randomId, short, sleep, textOf } from "./util.js"; + +/** + * Request methods (DappRequest.method). They mirror the Fuel connector standard's signing calls + * (fuels-ts FuelConnector `sendTransaction(address, transaction, params)`, `signTransaction(…)`, + * `signMessage(address, message)`), so 1Mask's Fuel connector hands them over unchanged: + * + * fuel_sendTransaction { address, transaction: TransactionRequest JSON (object or text), provider?: { url } } → tx id + * fuel_signTransaction same params → the signed TransactionRequest JSON (as Fuel Wallet returns it) + * fuel_signMessage { address, message: FuelMessage (message.ts) } → 0x signature (r ‖ s, recovery id in s's top bit) + */ +export const FUEL_METHODS = { + sendTransaction: "fuel_sendTransaction", + signTransaction: "fuel_signTransaction", + signMessage: "fuel_signMessage", +} as const; + +export interface FuelModuleOptions { + /** Dry-run scripts that call contracts in decode() to show what they move (default true). */ + simulate?: boolean; + /** Status polls after the status stream ends early (default 10) and the wait between them (default 1000 ms). */ + pollAttempts?: number; + pollMs?: number; + sleep?: (ms: number) => Promise; +} + +type Normalized = + | { kind: "tx"; tx: ScriptTx; send: boolean; chainId: number } + | { kind: "other-tx"; type: number; send: boolean } + | { kind: "message"; message: FuelMessage }; + +const MALFORMED = "This request from the app is malformed, so we stopped it."; +const WRONG_NETWORK = "This is for a different network, so Clip Wallet stopped it. Nothing was signed."; +const NOT_SIGNER = "This transaction doesn't need your signature."; +const BAD_SIG = "The signature didn't match. Nothing was sent."; +/** Above this the fee gets a caution, ten times it a danger (base units of ETH, 9 decimals: 0.001 ETH). */ +const HIGH_FEE = 1_000_000n; +/** Script gas limit for the dry run that measures a transfer's gas (a "return 0" script uses about 64). */ +const MEASURE_GAS = 100_000n; + +const lower = (s: string) => s.trim().toLowerCase(); + +/** Validates and reads a request. Shared by decode, prepare and finalize, so all three see the same bytes. */ +export function normalize(request: DappRequest, ctx: ChainContext): Normalized { + const methods = Object.values(FUEL_METHODS) as string[]; + if (!methods.includes(request.method)) throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "fuel/unsupported-method"); + const spec = specFor(request.networkId); + if (!spec || request.networkId !== ctx.network.id) throw new ClipError(WRONG_NETWORK, "fuel/network-mismatch"); + const p = request.params && typeof request.params === "object" && !Array.isArray(request.params) ? (request.params as Record) : null; + if (!p) throw new ClipError(MALFORMED, "fuel/malformed"); + if (p.address !== undefined) { + if (typeof p.address !== "string" || !isFuelAddress(p.address)) throw new ClipError(MALFORMED, "fuel/malformed"); + if (lower(p.address) !== lower(ctx.account.address)) throw new ClipError("This request is for a different account than the one you connected.", "fuel/not-your-account"); + } + // The dapp's provider URL (FuelConnectorSendTxParams.provider): refuse a known URL of another Fuel network. + const url = p.provider && typeof p.provider === "object" ? (p.provider as { url?: unknown }).url : undefined; + if (typeof url === "string") { + const n = fuelNetOf(url); + if (n && FUEL_NETS[n].id !== request.networkId) throw new ClipError(WRONG_NETWORK, "fuel/network-mismatch"); + } + + if (request.method === FUEL_METHODS.signMessage) { + const m = fuelMessageOf(p.message); + if (!m) throw new ClipError(MALFORMED, "fuel/malformed"); + return { kind: "message", message: m }; + } + let parsed: ReturnType; + try { + parsed = parseTransactionRequest(p.transaction); + } catch (e) { + if (e instanceof TxParseError || e instanceof RangeError) throw new ClipError(MALFORMED, "fuel/malformed", e); + throw e; + } + const send = request.method === FUEL_METHODS.sendTransaction; + if (parsed.kind === "other") return { kind: "other-tx", type: parsed.type, send }; + try { + encodeScriptTx(parsed.tx); // every field in range + } catch (e) { + throw new ClipError(MALFORMED, "fuel/malformed", e); + } + return { kind: "tx", tx: parsed.tx, send, chainId: spec.chainId }; +} + +function mine(tx: ScriptTx, ctx: ChainContext): Analysis { + const a = analyze(tx, lower(ctx.account.address)); + if (!a.myWitnesses.size) throw new ClipError(NOT_SIGNER, "fuel/not-a-signer"); + for (const w of a.myWitnesses) if (w >= tx.witnesses.length) throw new ClipError(MALFORMED, "fuel/malformed"); + return a; +} + +/** Plain words for a fuel-core error (submit / dry run). Never the raw node text. */ +export function plainFuelError(raw: string): { message: string | Msg; code: string } { + const r = raw.toLowerCase(); + if (/signature/.test(r)) return { message: BAD_SIG, code: "fuel/bad-signature" }; + if (/utxo|not found|already spent|spent|doesn't exist|does not exist|inputcoin/.test(r)) return { message: msg("bg.fuel.coinsSpent"), code: "fuel/coins-spent" }; + if (/max ?fee|maxfee|insufficientfee|gas price|fee amount/.test(r)) return { message: msg("bg.fuel.feeRose"), code: "fuel/fee-too-low" }; + if (/insufficient|not enough|cannot be met/.test(r)) return { message: msg("bg.err.notEnough", { symbol: "ETH" }), code: "fuel/insufficient-funds" }; + if (/expir/.test(r)) return { message: "This request expired. Please try again from the app.", code: "fuel/expired" }; + return { message: "The network rejected this. Nothing was sent.", code: "fuel/rejected" }; +} + +function gqlFailure(e: unknown, fallback: string): ClipError { + if (e instanceof ClipError) return e; + if (e instanceof GqlError && e.kind === "graphql") { + const p = plainFuelError(e.message); + return new ClipError(p.message, p.code, e); + } + return new ClipError("We couldn't reach the network. Check your connection and try again.", fallback, e); +} + +/** r ‖ s with the recovery id in the top bit of s (fuels-ts Signer.sign), after checking it recovers to `pub`. */ +export function fuelCompactSignature(sig: Signature, digest: Uint8Array, pub: Uint8Array): Uint8Array { + if (sig.scheme !== "ecdsa-secp256k1" || sig.bytes.length !== 64) throw new ClipError(BAD_SIG, "fuel/bad-signature"); + if (!secp256k1.verify(sig.bytes, digest, pub, { prehash: false })) throw new ClipError(BAD_SIG, "fuel/bad-signature"); + const want = secp256k1.Point.fromBytes(pub); + const candidates = sig.recovery === undefined ? [0, 1] : [sig.recovery]; + for (const rec of candidates) { + if (rec !== 0 && rec !== 1) continue; + let point; + try { + point = secp256k1.Signature.fromBytes(sig.bytes, "compact").addRecoveryBit(rec).recoverPublicKey(digest); + } catch { + continue; + } + if (!point.equals(want)) continue; + const out = sig.bytes.slice(); + if (out[32]! & 0x80) throw new ClipError(BAD_SIG, "fuel/bad-signature"); // high-S can't carry the recovery bit + out[32] = out[32]! | (rec << 7); + return out; + } + throw new ClipError(BAD_SIG, "fuel/bad-signature"); +} + +function publicKeyOf(ctx: ChainContext): Uint8Array { + const pub = fromHex(ctx.account.publicKey); + if (lower(fuelAddressFromPublicKey(pub)) !== lower(ctx.account.address)) throw new ClipError("This request is for a different account than the one you're using.", "fuel/not-your-account"); + return pub; +} + +export function createFuelModule(options: FuelModuleOptions = {}): ChainModule & { + normalize: typeof normalize; + digestOf(request: DappRequest, ctx: ChainContext): Uint8Array; +} { + const wait = options.sleep ?? sleep; + + function digestOf(request: DappRequest, ctx: ChainContext): Uint8Array { + const n = normalize(request, ctx); + if (n.kind === "message") return hashMessage(n.message); + if (n.kind === "other-tx") throw new ClipError("This transaction can't be read.", "fuel/unsupported-tx"); + mine(n.tx, ctx); + return transactionId(n.tx, n.chainId); + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const host = hostOf(req.origin); + const base = { requestId: req.id, networkId: req.networkId }; + const eth = ethAsset(ctx.network.id); + + if (n.kind === "message") { + const bytes = messageBytes(n.message); + const text = textOf(bytes); + const lines = text !== null ? [{ label: "Message", value: text }] : [{ label: "Message (not text)", value: hex0x(bytes) }]; + // A plain string is hashed with no prefix, so only readable text is signed blind-free: text can't be the + // preimage of a transaction id (that starts with the 8-byte chain id, whose first byte is 0x00). + const raw = "text" in n.message; + const warnings: Warning[] = text === null ? [{ level: "danger", code: "blind-signing", message: say("bg.warn.messageNotTextTrust") }] : []; + return { ...base, ...titled(msg("bg.req.signMessage", { host })), lines, balanceChanges: [], simulated: false, blind: text === null && raw, warnings }; + } + + if (n.kind === "other-tx") { + return { + ...base, + ...titled(msg("bg.req.approveTxFor", { host })), + lines: [], + balanceChanges: [], + simulated: false, + blind: true, + warnings: [{ level: "danger", code: "blind-signing", message: say("bg.warn.cantReadTxTrust") }], + }; + } + + const tx = n.tx; + const a = mine(tx, ctx); + const me = lower(ctx.account.address); + const spec = specFor(ctx.network.id)!; + const maxFee = tx.policies.maxFee; + const warnings: Warning[] = []; + let blind = false; + let sim: Simulated | null = null; + let simulated = false; + + if (a.predicates) { + blind = true; + warnings.push({ level: "danger", code: "blind-signing", message: say("bg.warn.partsUnexplained") }); + } else if (!a.plainScript) { + if (options.simulate === false) { + blind = true; + warnings.push(simulationFailed()); + } else { + const gql = gqlFor(ctx); + // Gas price 0: the preview shouldn't fail because the price moved since the app set its max fee. + const run = await gql.dryRun(hex0x(encodeScriptTx(tx)), false, 0n).catch(() => null); + if (!run || run.status.type !== "DryRunSuccessStatus") { + blind = true; + warnings.push(simulationFailed()); + } else { + sim = readReceipts(run.receipts, me); + simulated = true; + warnings.push({ level: "caution", code: "unknown-call", message: say("bg.warn.unknownPrograms") }); + } + } + } + + const flows = describeFlows(tx, a, sim, { networkId: ctx.network.id, me, baseAssetId: spec.baseAssetId, maxFee }); + warnings.push(...flows.warnings); + + const lines: { label: string; value: string }[] = [...sendLines(a, ctx.network.id)]; + const contracts = [...new Set([...a.contracts, ...(sim?.calls ?? [])])]; + for (const c of contracts) lines.push({ label: "Contract", value: c }); + if (tx.policies.expiration !== undefined) lines.push({ label: "Valid until", value: `block ${tx.policies.expiration}` }); + lines.push({ label: "Network fee at most", value: amountText(eth, maxFee) }); + if (!n.send) lines.push({ label: "Sent by", value: `${host} (it gets the signed transaction)` }); + + if (maxFee > HIGH_FEE) { + warnings.push({ level: maxFee > HIGH_FEE * 10n ? "danger" : "caution", code: "high-fee", message: say("bg.warn.highFee") }); + } + + let title: { title: string; titleMsg?: Msg }; + const t = transferTitle(a, ctx.network.id); + if (blind) title = titled(msg("bg.req.approveTxFor", { host })); + else if (contracts.length) title = titled(msg("bg.req.contractActionFor", { host })); + else if (t) title = t; + else if (a.toMe.size) { + // A transfer to yourself: nothing leaves but the fee. + const [assetId, amount] = [...a.toMe][0]!; + title = titled(msg("bg.req.sendTo", { amount: amountText(assetFor(ctx.network.id, assetId), amount), to: short(ctx.account.address) })); + } else title = titled(msg("bg.req.approveTxFor", { host })); + if (isWalletOrigin(req.origin) && title.title.includes(host)) title = titled(msg("bg.req.approveTx")); + + return { + ...base, + ...title, + lines, + balanceChanges: blind ? [] : flows.balanceChanges, + fee: { asset: eth, amount: maxFee.toString() }, + simulated, + blind, + warnings, + }; + } + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + return [{ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes: digestOf(req, ctx), approvalId }]; + } + + async function confirm(ctx: ChainContext, txHex: string, id: string): Promise { + const gql = gqlFor(ctx); + let status; + try { + status = await gql.submitAndAwait(txHex); + } catch (e) { + throw gqlFailure(e, "fuel/offline"); + } + if (status.type === "FailureStatus") throw new ClipError(msg("bg.fuel.failedOnChain"), "fuel/failed"); + if (status.type === "SqueezedOutStatus") { + const p = plainFuelError(status.reason); + throw new ClipError(p.message, p.code); + } + if (status.type === "SuccessStatus") return; + // Accepted, no final status yet: poll a little, then hand back the id (Activity follows it). + for (let k = 0; k < (options.pollAttempts ?? 10); k++) { + await wait(options.pollMs ?? 1000); + const s = await gql.transactionStatus(id).catch(() => null); + if (s?.type === "SuccessStatus") return; + if (s?.type === "FailureStatus") throw new ClipError(msg("bg.fuel.failedOnChain"), "fuel/failed"); + if (s?.type === "SqueezedOutStatus") { + const p = plainFuelError(s.reason ?? ""); + throw new ClipError(p.message, p.code); + } + } + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const n = normalize(req, ctx); + if (signatures.length !== 1) throw new ClipError("Some signatures are missing. Nothing was sent.", "fuel/bad-signature"); + const digest = digestOf(req, ctx); + const compact = fuelCompactSignature(signatures[0]!, digest, publicKeyOf(ctx)); + if (n.kind === "message") return hex0x(compact); + if (n.kind !== "tx") throw new ClipError("This transaction can't be read.", "fuel/unsupported-tx"); + const a = mine(n.tx, ctx); + const witnesses = n.tx.witnesses.slice(); + for (const w of a.myWitnesses) witnesses[w] = compact; + const signed: ScriptTx = { ...n.tx, witnesses }; + if (!n.send) return toTransactionRequestJson(signed); + const id = hex0x(transactionId(signed, n.chainId)); + await confirm(ctx, hex0x(encodeScriptTx(signed)), id); + return id; + } + + async function getBalances(ctx: ChainContext): Promise { + const spec = specFor(ctx.network.id); + if (!spec) return []; + let list: { assetId: string; amount: string }[]; + try { + list = await gqlFor(ctx).balances(lower(ctx.account.address)); + } catch (e) { + throw gqlFailure(e, "fuel/offline"); + } + const amounts = new Map(list.map((b) => [b.assetId.toLowerCase(), b.amount])); + const out: TokenBalance[] = [{ asset: ethAsset(ctx.network.id), amount: amounts.get(spec.baseAssetId) ?? "0" }]; + // Curated tokens only: anything else anyone can mint and send (spam stays out). + for (const t of spec.tokens) { + const amt = amounts.get(t.assetId); + if (amt && amt !== "0") out.push({ asset: assetFor(ctx.network.id, t.assetId), amount: amt }); + } + return out; + } + + async function getNfts(_ctx: ChainContext): Promise { + return []; + } + + function inputsFrom(coins: Spendable[], me: string, baseAssetId: string): Input[] { + return coins.map((c): Input => { + if (c.type === "Coin") { + const id = fromHex(c.utxoId); + if (id.length !== 34) throw new ClipError("The network sent something unexpected. Try again.", "fuel/bad-response"); + return { + type: 0, + txId: hex0x(id.slice(0, 32)), + outputIndex: (id[32]! << 8) | id[33]!, + owner: me, + amount: BigInt(c.amount), + assetId: lower(c.assetId), + txPointer: { blockHeight: 0, txIndex: 0 }, + witnessIndex: 0, + predicateGasUsed: 0n, + predicate: new Uint8Array(0), + predicateData: new Uint8Array(0), + }; + } + if (lower(c.assetId ?? baseAssetId) !== baseAssetId) throw new ClipError("The network sent something unexpected. Try again.", "fuel/bad-response"); + return { + type: 2, + sender: lower(c.sender), + recipient: me, + amount: BigInt(c.amount), + nonce: lower(c.nonce), + witnessIndex: 0, + predicateGasUsed: 0n, + data: new Uint8Array(0), + predicate: new Uint8Array(0), + predicateData: new Uint8Array(0), + }; + }); + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + const spec = specFor(ctx.network.id); + if (!spec) throw new ClipError("That network isn't available in this wallet.", "fuel/network-mismatch"); + const me = lower(ctx.account.address); + const toRaw = p.to.trim(); + if (!isFuelAddress(toRaw)) throw new ClipError("That address isn't valid. Check it and try again.", "fuel/bad-address"); + const to = lower(toRaw); + if (to === me) throw new ClipError("That's your own address.", "fuel/self-transfer"); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "fuel/bad-amount"); + const amount = BigInt(p.amount); + const assetId = p.asset.address ? lower(p.asset.address) : spec.baseAssetId; + if (!/^0x[0-9a-f]{64}$/.test(assetId)) throw new ClipError("That token couldn't be found.", "fuel/bad-asset"); + const isBase = assetId === spec.baseAssetId; + const symbol = assetFor(ctx.network.id, assetId).symbol; + + const gql = gqlFor(ctx); + let chain: ChainInfo; + let gasPrice: bigint; + try { + [chain, gasPrice] = await Promise.all([gql.chainInfo(), gql.estimateGasPrice(10)]); + } catch (e) { + throw gqlFailure(e, "fuel/offline"); + } + if (chain.chainId !== spec.chainId) throw new ClipError(WRONG_NETWORK, "fuel/network-mismatch"); + + const outputs: Output[] = [{ type: 0, to, amount, assetId }, { type: 2, to: me, amount: 0n, assetId }]; + if (!isBase) outputs.push({ type: 2, to: me, amount: 0n, assetId: spec.baseAssetId }); + const make = (inputs: Input[], gasLimit: bigint, maxFee: bigint): ScriptTx => ({ + type: 0, + scriptGasLimit: gasLimit, + receiptsRoot: ZERO32, + script: fromHex(RETURN_ZERO_SCRIPT), + scriptData: new Uint8Array(0), + policies: { maxFee }, + inputs, + outputs, + witnesses: [new Uint8Array(64)], // placeholder of the signature's size, so size-based gas is right + }); + + let feeBudget = 0n; + let gasLimit = 0n; + for (let round = 0; round < 4; round++) { + const want = isBase ? [{ assetId, amount: amount + feeBudget + 1n }] : [{ assetId, amount }, { assetId: spec.baseAssetId, amount: feeBudget + 1n }]; + let coins: Spendable[]; + try { + coins = (await gql.coinsToSpend(me, want)).flat(); + } catch (e) { + if (e instanceof GqlError && e.kind === "graphql" && /insufficient|cannot be met|not enough/i.test(e.message)) { + // fuel-core names the asset it couldn't cover ("… insufficient coins available for "). + const lacksToken = !isBase && e.message.toLowerCase().includes(assetId.slice(2)); + throw new ClipError(lacksToken ? msg("bg.err.notEnough", { symbol }) : msg("bg.err.notEnoughForFee", { symbol: "ETH" }), "fuel/insufficient-funds", e); + } + throw gqlFailure(e, "fuel/offline"); + } + if (coins.length > chain.maxInputs) throw new ClipError(msg("bg.fuel.tooManyCoins"), "fuel/too-many-inputs"); + const inputs = inputsFrom(coins, me, spec.baseAssetId); + if (gasLimit === 0n) { + // Gas the script uses, measured with a dry run (fuels-ts assembleTx does the same). + let run; + try { + run = await gql.dryRun(hex0x(encodeScriptTx(make(inputs, MEASURE_GAS, 0n))), false, 0n); + } catch (e) { + throw gqlFailure(e, "fuel/offline"); + } + const used = run.receipts.find((r) => r.receiptType === "SCRIPT_RESULT")?.gasUsed; + if (run.status.type !== "DryRunSuccessStatus" || !used) throw new ClipError("The network rejected this. Nothing was sent.", "fuel/rejected"); + gasLimit = BigInt(used); + } + const draft = make(inputs, gasLimit, 0n); + const maxFee = requiredMaxFee(draft, chain, gasPrice) + 1n; + const baseIn = inputs.reduce((t, i) => (i.type === 2 || (i.type === 0 && i.assetId === spec.baseAssetId) ? t + i.amount : t), 0n); + const needBase = (isBase ? amount : 0n) + maxFee; + if (baseIn >= needBase) { + const tx = make(inputs, gasLimit, maxFee); + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "fuel", + networkId: ctx.network.id, + method: FUEL_METHODS.sendTransaction, + params: { address: ctx.account.address, transaction: toTransactionRequestJson(tx) }, + }; + } + feeBudget = maxFee * 2n; + } + throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: "ETH" }), "fuel/insufficient-funds"); + } + + return { + family: "fuel", + curve: "secp256k1", + /** Fuel Wallet / fuels-ts WalletManager path (SLIP-44 coin 1179993420). */ + derivationPath: (index: number) => { + if (!Number.isInteger(index) || index < 0 || index >= 2 ** 31) throw new Error("account index out of range"); + return `m/44'/1179993420'/${index}'/0/0`; + }, + addressFromPublicKey: (publicKey: Uint8Array, _network: Network) => fuelAddressFromPublicKey(publicKey), + isAddress: (value: string) => isFuelAddress(value), + /** The same address is valid on every Fuel network. */ + networksForAddress: (value: string, candidates: Network[]) => (isFuelAddress(value) ? candidates.filter((c) => c.family === "fuel") : []), + getBalances, + getNfts, + decode, + prepare, + finalize, + buildTransfer, + normalize, + digestOf, + }; +} diff --git a/packages/chains-fuel/src/networks.ts b/packages/chains-fuel/src/networks.ts new file mode 100644 index 00000000..b25fc20e --- /dev/null +++ b/packages/chains-fuel/src/networks.ts @@ -0,0 +1,153 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * Fuel networks (Fuel Ignition mainnet and the public Sepolia-anchored testnet). + * + * Ids: there is no ChainAgnostic CAIP-2 namespace for Fuel (github.com/ChainAgnostic/namespaces has none, checked + * Oct 2026). The Fuel connector standard names a network by its numeric chain id (fuels-ts `Network.chainId`, + * `CHAIN_IDS.fuel` = { mainnet: 9889, testnet: 0, devnet: 1119889111 } in @fuel-ts/account providers/chains.ts, the + * same table @fuels/connectors' `DEFAULT_NETWORKS` uses), so the id here is "fuel:". + * + * Chain ids, base asset and endpoints verified with `{ chain { name consensusParameters { chainId baseAssetId } } }` + * on both GraphQL endpoints (Oct 2026: "Ignition" chainId 9889, "Fuel Sepolia Testnet" chainId 0, fuel-core 0.48.3; + * both base assets 0xf8f8…ad07, ETH with 9 decimals). + */ +export type FuelNet = "mainnet" | "testnet"; + +export interface FuelToken { + key: string; + symbol: string; + name: string; + decimals: number; + /** Asset id (b256, lower-case 0x-hex). */ + assetId: string; + bridged?: boolean; +} + +export interface FuelNetSpec { + id: NetworkId; + chainId: number; + graphql: string; + explorer: string; + /** Base asset (ETH, 9 decimals): consensus parameters `baseAssetId`. */ + baseAssetId: string; + /** Asset key of the base asset ("eth" on mainnet; the testnet copy gets its own key, as chains-evm/-starknet do). */ + baseKey: string; + /** Curated tokens (Fuel's verified-assets list, https://verified-assets.fuel.network/assets.json, Oct 2026). */ + tokens: FuelToken[]; + faucet?: string; +} + +export const BASE_ASSET_ID = "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07"; + +/** + * USDC, USDT and FUEL on Fuel are minted by Fuel's canonical bridge (contract 0x4ea6…d0e8 on mainnet, 0xd021…5471 on + * testnet; the verified-assets list names them), so they are bridged copies: own keys, `bridged: true`, never merged + * with the issuer's native token elsewhere (AGENTS.md "Add a token list"). + */ +export const FUEL_NETS: Record = { + testnet: { + id: "fuel:0", + chainId: 0, + graphql: "https://testnet.fuel.network/v1/graphql", + explorer: "https://app-testnet.fuel.network", + baseAssetId: BASE_ASSET_ID, + baseKey: "eth-testnet", + faucet: "https://faucet-testnet.fuel.network/", + tokens: [ + { key: "usdc.e", symbol: "USDC", name: "Bridged USDC (Fuel)", decimals: 6, assetId: "0xc26c91055de37528492e7e97d91c6f4abe34aae26f2c4d25cff6bfe45b5dc9a9", bridged: true }, + { key: "fuel", symbol: "FUEL", name: "Fuel", decimals: 9, assetId: "0x324d0c35a4299ef88138a656d5272c5a3a9ccde2630ae055dacaf9d13443d53b", bridged: true }, + ], + }, + mainnet: { + id: "fuel:9889", + chainId: 9889, + graphql: "https://mainnet.fuel.network/v1/graphql", + explorer: "https://app.fuel.network", + baseAssetId: BASE_ASSET_ID, + baseKey: "eth", + tokens: [ + { key: "usdc.e", symbol: "USDC", name: "Bridged USDC (Fuel)", decimals: 6, assetId: "0x286c479da40dc953bddc3bb4c453b608bba2e0ac483b077bd475174115395e6b", bridged: true }, + { key: "usdt.e", symbol: "USDT", name: "Bridged USDT (Fuel)", decimals: 6, assetId: "0xa0265fb5c32f6e8db3197af3c7eb05c48ae373605b8165b6f4a51c5b0ba4812e", bridged: true }, + { key: "fuel", symbol: "FUEL", name: "Fuel", decimals: 9, assetId: "0x1d5d97005e41cae2187a895fd8eab0506111e0e2f3331cd3912c15c24e3c1d82", bridged: true }, + ], + }, +}; + +export function ethAsset(networkId: NetworkId): AssetRef { + const s = specFor(networkId); + return { key: s?.baseKey ?? "eth", symbol: "ETH", name: "Ether", decimals: 9, networkId }; +} + +function network(net: FuelNet): Network { + const s = FUEL_NETS[net]; + return { + id: s.id, + family: "fuel", + name: net === "mainnet" ? "Fuel" : "Fuel Testnet", + nativeAsset: ethAsset(s.id), + testnet: net !== "mainnet", + rpcUrls: [s.graphql], + explorerUrl: s.explorer, + }; +} + +export const FUEL_TESTNET = network("testnet"); +export const FUEL_MAINNET = network("mainnet"); +export const FUEL_NETWORKS: Network[] = [FUEL_TESTNET, FUEL_MAINNET]; + +/** "fuel:0", "0", 0, or a known GraphQL URL → the network's name; null for anything else. */ +export function fuelNetOf(idOrChainId: string | number): FuelNet | null { + for (const [name, s] of Object.entries(FUEL_NETS) as [FuelNet, FuelNetSpec][]) { + if (typeof idOrChainId === "number") { + if (idOrChainId === s.chainId) return name; + continue; + } + const v = idOrChainId.trim(); + if (v === s.id || v === String(s.chainId) || sameUrl(v, s.graphql)) return name; + } + return null; +} + +export function specFor(networkId: NetworkId): FuelNetSpec | null { + const n = fuelNetOf(networkId); + return n ? FUEL_NETS[n] : null; +} + +/** The Fuel connector's `Network` shape ({ url, chainId }) for a network. */ +export function connectorNetwork(net: Network): { url: string; chainId: number } { + const s = specFor(net.id); + return { url: net.rpcUrls[0] ?? s?.graphql ?? "", chainId: s?.chainId ?? Number(net.id.replace(/^fuel:/, "")) }; +} + +function sameUrl(a: string, b: string): boolean { + try { + const x = new URL(a); + const y = new URL(b); + return x.host === y.host && x.pathname.replace(/\/+$/, "") === y.pathname.replace(/\/+$/, ""); + } catch { + return false; + } +} + +/** Curated token by asset id, or undefined. */ +export function curatedToken(networkId: NetworkId, assetId: string): FuelToken | undefined { + const id = assetId.toLowerCase(); + return specFor(networkId)?.tokens.find((t) => t.assetId === id); +} + +/** AssetRef for an asset id: ETH, a curated token, or an unknown asset shown by its short id (never merged). */ +export function assetFor(networkId: NetworkId, assetId: string): AssetRef { + const id = assetId.toLowerCase(); + const s = specFor(networkId); + if (s && id === s.baseAssetId) return ethAsset(networkId); + const t = curatedToken(networkId, id); + if (t) return { key: t.key, symbol: t.symbol, name: t.name, decimals: t.decimals, networkId, address: t.assetId, ...(t.bridged ? { bridged: true } : {}) }; + const shortId = `${id.slice(0, 6)}…${id.slice(-4)}`; + // Unverified assets: no decimals are known (they live in the minting contract's SRC-20 metadata), so base units. + return { key: `fuel-asset:${id}`, symbol: shortId, name: `Asset ${shortId}`, decimals: 0, networkId, address: id }; +} + +export function explorerTxUrl(net: Network, txId: string): string { + return `${net.explorerUrl}/tx/${txId}`; +} diff --git a/packages/chains-fuel/src/tx.ts b/packages/chains-fuel/src/tx.ts new file mode 100644 index 00000000..084d3a53 --- /dev/null +++ b/packages/chains-fuel/src/tx.ts @@ -0,0 +1,507 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { concat, fromHex, hex0x } from "./util.js"; + +/** + * Fuel transactions, written by hand from the specs (https://docs.fuel.network/docs/specs/tx-format/ and + * /docs/specs/identifiers/transaction-id/), byte-compatible with fuels-ts `TransactionCoder` (the tests compare both + * for every fixture). Only Script transactions are built and signed; the other kinds (Create, Upgrade, Upload, Blob, + * Mint) are recognised so a request for them can be refused in plain words. + * + * Encoding: every field is padded to an 8-byte word (u8/u16/u32 are written as a big-endian u64), byte arrays are + * zero-padded to a multiple of 8, b256 values are 32 raw bytes. + */ + +export const InputType = { Coin: 0, Contract: 1, Message: 2 } as const; +export const OutputType = { Coin: 0, Contract: 1, Change: 2, Variable: 3, ContractCreated: 4 } as const; +export const TransactionType = { Script: 0, Create: 1, Mint: 2, Upgrade: 3, Upload: 4, Blob: 5 } as const; +/** Policy bits (fuel-tx `PolicyType`, fuels-ts PoliciesCoder): values are written in this order. */ +export const PolicyType = { Tip: 1, WitnessLimit: 2, Maturity: 4, MaxFee: 8, Expiration: 16, Owner: 32 } as const; + +export const ZERO32 = `0x${"00".repeat(32)}`; +/** fuels-ts `returnZeroScript` (RET $zero), the script of a plain transfer: it moves nothing itself. */ +export const RETURN_ZERO_SCRIPT = "0x24000000"; + +export interface TxPointer { + blockHeight: number; + txIndex: number; +} + +export interface CoinInput { + type: 0; + txId: string; + outputIndex: number; + owner: string; + amount: bigint; + assetId: string; + txPointer: TxPointer; + witnessIndex: number; + predicateGasUsed: bigint; + predicate: Uint8Array; + predicateData: Uint8Array; +} + +export interface ContractInput { + type: 1; + txId: string; + outputIndex: number; + balanceRoot: string; + stateRoot: string; + txPointer: TxPointer; + contractId: string; +} + +export interface MessageInput { + type: 2; + sender: string; + recipient: string; + amount: bigint; + nonce: string; + witnessIndex: number; + predicateGasUsed: bigint; + data: Uint8Array; + predicate: Uint8Array; + predicateData: Uint8Array; +} + +export type Input = CoinInput | ContractInput | MessageInput; + +export type Output = + | { type: 0; to: string; amount: bigint; assetId: string } + | { type: 1; inputIndex: number; balanceRoot: string; stateRoot: string } + | { type: 2; to: string; amount: bigint; assetId: string } + | { type: 3; to: string; amount: bigint; assetId: string } + | { type: 4; contractId: string; stateRoot: string }; + +export interface Policies { + tip?: bigint; + witnessLimit?: bigint; + maturity?: number; + maxFee: bigint; + expiration?: number; + owner?: bigint; +} + +export interface ScriptTx { + type: 0; + scriptGasLimit: bigint; + receiptsRoot: string; + script: Uint8Array; + scriptData: Uint8Array; + policies: Policies; + inputs: Input[]; + outputs: Output[]; + witnesses: Uint8Array[]; +} + +/* ------------------------------------------------------------------ encoding */ + +const MAX_U64 = (1n << 64n) - 1n; + +function word(v: bigint | number): Uint8Array { + const b = typeof v === "bigint" ? v : BigInt(v); + if (b < 0n || b > MAX_U64) throw new RangeError("value out of u64 range"); + const out = new Uint8Array(8); + new DataView(out.buffer).setBigUint64(0, b, false); + return out; +} + +function small(v: number, bits: 8 | 16 | 32): Uint8Array { + if (!Number.isInteger(v) || v < 0 || v >= 2 ** bits) throw new RangeError(`value out of u${bits} range`); + return word(v); +} + +function b256(h: string): Uint8Array { + const b = fromHex(h); + if (b.length !== 32) throw new RangeError("expected 32 bytes"); + return b; +} + +function padded(bytes: Uint8Array): Uint8Array { + const pad = (8 - (bytes.length % 8)) % 8; + return pad ? concat(bytes, new Uint8Array(pad)) : bytes; +} + +function txPointer(p: TxPointer): Uint8Array { + return concat(small(p.blockHeight, 32), small(p.txIndex, 16)); +} + +function encodeInput(i: Input): Uint8Array { + switch (i.type) { + case 0: + return concat( + word(0), + b256(i.txId), + small(i.outputIndex, 16), + b256(i.owner), + word(i.amount), + b256(i.assetId), + txPointer(i.txPointer), + small(i.witnessIndex, 16), + word(i.predicateGasUsed), + word(i.predicate.length), + word(i.predicateData.length), + padded(i.predicate), + padded(i.predicateData), + ); + case 1: + return concat(word(1), b256(i.txId), small(i.outputIndex, 16), b256(i.balanceRoot), b256(i.stateRoot), txPointer(i.txPointer), b256(i.contractId)); + case 2: + return concat( + word(2), + b256(i.sender), + b256(i.recipient), + word(i.amount), + b256(i.nonce), + small(i.witnessIndex, 16), + word(i.predicateGasUsed), + word(i.data.length), + word(i.predicate.length), + word(i.predicateData.length), + padded(i.data), + padded(i.predicate), + padded(i.predicateData), + ); + } +} + +function encodeOutput(o: Output): Uint8Array { + switch (o.type) { + case 0: + case 2: + case 3: + return concat(word(o.type), b256(o.to), word(o.amount), b256(o.assetId)); + case 1: + return concat(word(1), small(o.inputIndex, 8), b256(o.balanceRoot), b256(o.stateRoot)); + case 4: + return concat(word(4), b256(o.contractId), b256(o.stateRoot)); + } +} + +function policyBits(p: Policies): { types: number; parts: Uint8Array[] } { + let types = 0; + const parts: Uint8Array[] = []; + if (p.tip !== undefined) { + types |= PolicyType.Tip; + parts.push(word(p.tip)); + } + if (p.witnessLimit !== undefined) { + types |= PolicyType.WitnessLimit; + parts.push(word(p.witnessLimit)); + } + if (p.maturity !== undefined) { + types |= PolicyType.Maturity; + parts.push(small(p.maturity, 32)); + } + types |= PolicyType.MaxFee; + parts.push(word(p.maxFee)); + if (p.expiration !== undefined) { + types |= PolicyType.Expiration; + parts.push(small(p.expiration, 32)); + } + if (p.owner !== undefined) { + types |= PolicyType.Owner; + parts.push(word(p.owner)); + } + return { types, parts }; +} + +/** The canonical bytes of a Script transaction (what `submit` and `dryRun` take). */ +export function encodeScriptTx(tx: ScriptTx): Uint8Array { + const { types, parts } = policyBits(tx.policies); + return concat( + word(TransactionType.Script), + word(tx.scriptGasLimit), + b256(tx.receiptsRoot), + word(tx.script.length), + word(tx.scriptData.length), + small(types, 32), + small(tx.inputs.length, 16), + small(tx.outputs.length, 16), + small(tx.witnesses.length, 16), + padded(tx.script), + padded(tx.scriptData), + ...parts, + ...tx.inputs.map(encodeInput), + ...tx.outputs.map(encodeOutput), + ...tx.witnesses.map((w) => concat(small(w.length, 32), padded(w))), + ); +} + +/** + * Transaction id (specs: identifiers/transaction-id): SHA-256 of the chain id (u64, big-endian) followed by the + * transaction serialized with the fields that change after signing zeroed: the receipts root, every input's tx + * pointer and predicate gas used, a contract input's utxo id and roots, a contract output's roots, a change output's + * amount, a variable output's to/amount/asset id, and with no witnesses at all. + */ +export function transactionId(tx: ScriptTx, chainId: number): Uint8Array { + const zp: TxPointer = { blockHeight: 0, txIndex: 0 }; + const stripped: ScriptTx = { + ...tx, + receiptsRoot: ZERO32, + inputs: tx.inputs.map((i): Input => { + if (i.type === 0) return { ...i, txPointer: zp, predicateGasUsed: 0n }; + if (i.type === 2) return { ...i, predicateGasUsed: 0n }; + return { ...i, txPointer: zp, txId: ZERO32, outputIndex: 0, balanceRoot: ZERO32, stateRoot: ZERO32 }; + }), + outputs: tx.outputs.map((o): Output => { + if (o.type === 1) return { ...o, balanceRoot: ZERO32, stateRoot: ZERO32 }; + if (o.type === 2) return { ...o, amount: 0n }; + if (o.type === 3) return { ...o, to: ZERO32, amount: 0n, assetId: ZERO32 }; + return o; + }), + witnesses: [], + }; + return sha256(concat(word(BigInt(chainId)), encodeScriptTx(stripped))); +} + +/* ------------------------------------------------------------------ fuels-ts TransactionRequest JSON */ + +/** + * The wire shape: `JSON.stringify(transactionRequest)` of a fuels-ts ScriptTransactionRequest, as the Fuel Wallet + * connector sends it (@fuels/connectors FuelWalletConnector.sendTransaction). BN fields are 0x-hex strings (BN.toJSON), + * bytes are 0x-hex strings; numbers and decimal strings are accepted too. `flag` (the dapp's own dry-run summary) is + * ignored: nothing a dapp says about the outcome is trusted. + */ +export type TransactionRequestJson = Record; + +export class TxParseError extends Error {} + +const bad = (what: string): never => { + throw new TxParseError(what); +}; + +function bigOf(v: unknown, what: string, dflt?: bigint): bigint { + if (v === undefined || v === null) return dflt !== undefined ? dflt : bad(what); + if (typeof v === "bigint") return v >= 0n ? v : bad(what); + if (typeof v === "number") return Number.isSafeInteger(v) && v >= 0 ? BigInt(v) : bad(what); + if (typeof v === "string") { + if (/^0x[0-9a-fA-F]+$/.test(v)) return BigInt(v); + if (/^\d+$/.test(v)) return BigInt(v); + } + return bad(what); +} + +function numOf(v: unknown, what: string, dflt?: number): number { + const b = bigOf(v, what, dflt === undefined ? undefined : BigInt(dflt)); + if (b > BigInt(Number.MAX_SAFE_INTEGER)) bad(what); + return Number(b); +} + +/** 0x-hex string, a byte array, or the `{ "0": n, … }` object JSON makes of a Uint8Array. */ +export function bytesOf(v: unknown, what: string, dflt?: Uint8Array): Uint8Array { + if (v === undefined || v === null) return dflt ?? bad(what); + if (v instanceof Uint8Array) return v; + if (typeof v === "string") { + if (v === "0x" || v === "") return new Uint8Array(0); + try { + return fromHex(v.startsWith("0x") || v.startsWith("0X") ? v : bad(what)); + } catch { + return bad(what); + } + } + let arr: unknown[] | null = null; + if (Array.isArray(v)) arr = v; + else if (typeof v === "object") { + const o = v as Record; + const n = Object.keys(o).length; + arr = []; + for (let i = 0; i < n; i++) { + if (!Object.prototype.hasOwnProperty.call(o, String(i))) return bad(what); + arr.push(o[String(i)]); + } + } + if (!arr || arr.some((x) => typeof x !== "number" || !Number.isInteger(x) || x < 0 || x > 255)) return bad(what); + return Uint8Array.from(arr as number[]); +} + +function b256Of(v: unknown, what: string, dflt?: string): string { + const b = bytesOf(v, what, dflt === undefined ? undefined : fromHex(dflt)); + if (b.length !== 32) bad(what); + return hex0x(b); +} + +function toNumberBE(bytes: Uint8Array): number { + let n = 0; + for (const b of bytes) n = n * 256 + b; + return n; +} + +/** fuels-ts `inputify`: blockHeight from bytes 0..8 and txIndex from bytes 8..16 of the request's txPointer. */ +function txPointerOf(v: unknown): TxPointer { + const b = bytesOf(v, "txPointer", new Uint8Array(0)); + const p = { blockHeight: toNumberBE(b.slice(0, 8)), txIndex: toNumberBE(b.slice(8, 16)) }; + if (p.blockHeight >= 2 ** 32 || p.txIndex >= 2 ** 16) bad("txPointer"); + return p; +} + +const obj = (v: unknown, what: string): Record => (v && typeof v === "object" && !Array.isArray(v) ? (v as Record) : bad(what)); + +function inputOf(raw: unknown, k: number): Input { + const i = obj(raw, `input ${k}`); + const type = numOf(i.type, `input ${k} type`); + if (type === InputType.Coin) { + const id = bytesOf(i.id, `input ${k} id`); + if (id.length !== 34) bad(`input ${k} id`); + return { + type: 0, + txId: hex0x(id.slice(0, 32)), + outputIndex: toNumberBE(id.slice(32, 34)), + owner: b256Of(i.owner, `input ${k} owner`), + amount: bigOf(i.amount, `input ${k} amount`), + assetId: b256Of(i.assetId, `input ${k} assetId`), + txPointer: txPointerOf(i.txPointer), + witnessIndex: numOf(i.witnessIndex, `input ${k} witnessIndex`, 0), + predicateGasUsed: bigOf(i.predicateGasUsed, `input ${k} predicateGasUsed`, 0n), + predicate: bytesOf(i.predicate, `input ${k} predicate`, new Uint8Array(0)), + predicateData: bytesOf(i.predicateData, `input ${k} predicateData`, new Uint8Array(0)), + }; + } + if (type === InputType.Contract) { + return { + type: 1, + txId: b256Of(i.txID, `input ${k} txID`, ZERO32), + outputIndex: 0, + balanceRoot: ZERO32, + stateRoot: ZERO32, + txPointer: txPointerOf(i.txPointer), + contractId: b256Of(i.contractId, `input ${k} contractId`), + }; + } + if (type === InputType.Message) { + return { + type: 2, + sender: b256Of(i.sender, `input ${k} sender`), + recipient: b256Of(i.recipient, `input ${k} recipient`), + amount: bigOf(i.amount, `input ${k} amount`), + nonce: b256Of(i.nonce, `input ${k} nonce`), + witnessIndex: numOf(i.witnessIndex, `input ${k} witnessIndex`, 0), + predicateGasUsed: bigOf(i.predicateGasUsed, `input ${k} predicateGasUsed`, 0n), + data: bytesOf(i.data, `input ${k} data`, new Uint8Array(0)), + predicate: bytesOf(i.predicate, `input ${k} predicate`, new Uint8Array(0)), + predicateData: bytesOf(i.predicateData, `input ${k} predicateData`, new Uint8Array(0)), + }; + } + return bad(`input ${k} type`); +} + +function outputOf(raw: unknown, k: number): Output { + const o = obj(raw, `output ${k}`); + const type = numOf(o.type, `output ${k} type`); + switch (type) { + case OutputType.Coin: + return { type: 0, to: b256Of(o.to, `output ${k} to`), amount: bigOf(o.amount, `output ${k} amount`), assetId: b256Of(o.assetId, `output ${k} assetId`) }; + case OutputType.Contract: + return { type: 1, inputIndex: numOf(o.inputIndex, `output ${k} inputIndex`), balanceRoot: ZERO32, stateRoot: ZERO32 }; + case OutputType.Change: + return { type: 2, to: b256Of(o.to, `output ${k} to`), amount: 0n, assetId: b256Of(o.assetId, `output ${k} assetId`) }; + case OutputType.Variable: + return { type: 3, to: b256Of(o.to, `output ${k} to`, ZERO32), amount: bigOf(o.amount, `output ${k} amount`, 0n), assetId: b256Of(o.assetId, `output ${k} assetId`, ZERO32) }; + case OutputType.ContractCreated: + return { type: 4, contractId: b256Of(o.contractId, `output ${k} contractId`), stateRoot: b256Of(o.stateRoot, `output ${k} stateRoot`) }; + } + return bad(`output ${k} type`); +} + +/** Reads a request (object or its JSON text). Returns the transaction kind for non-Script requests. */ +export function parseTransactionRequest(raw: unknown): { kind: "script"; tx: ScriptTx } | { kind: "other"; type: number } { + let v = raw; + if (typeof v === "string") { + try { + v = JSON.parse(v); + } catch { + bad("transaction JSON"); + } + } + const r = obj(v, "transaction"); + const type = numOf(r.type, "type"); + if (type !== TransactionType.Script) return { kind: "other", type }; + const list = (x: unknown, what: string): unknown[] => (x === undefined ? [] : Array.isArray(x) ? x : bad(what)); + const policies: Policies = { maxFee: bigOf(r.maxFee, "maxFee", 0n) }; + const tip = bigOf(r.tip, "tip", 0n); + if (tip > 0n) policies.tip = tip; + if (r.witnessLimit !== undefined && r.witnessLimit !== null) policies.witnessLimit = bigOf(r.witnessLimit, "witnessLimit"); + const maturity = numOf(r.maturity, "maturity", 0); + if (maturity > 0) policies.maturity = maturity; + const expiration = numOf(r.expiration, "expiration", 0); + if (expiration > 0) policies.expiration = expiration; + if (r.ownerInputIndex !== undefined && r.ownerInputIndex !== null) policies.owner = bigOf(r.ownerInputIndex, "ownerInputIndex"); + return { + kind: "script", + tx: { + type: 0, + scriptGasLimit: bigOf(r.gasLimit, "gasLimit", 0n), + receiptsRoot: ZERO32, + script: bytesOf(r.script, "script", new Uint8Array(0)), + scriptData: bytesOf(r.scriptData, "scriptData", new Uint8Array(0)), + policies, + inputs: list(r.inputs, "inputs").map(inputOf), + outputs: list(r.outputs, "outputs").map(outputOf), + witnesses: list(r.witnesses, "witnesses").map((w, k) => bytesOf(w, `witness ${k}`)), + }, + }; +} + +const bnHex = (v: bigint) => `0x${v.toString(16)}`; + +/** Back to the fuels-ts request JSON (what `transactionRequestify` reads), e.g. a signed transaction for the dapp. */ +export function toTransactionRequestJson(tx: ScriptTx): TransactionRequestJson { + const out: TransactionRequestJson = { + type: 0, + gasLimit: bnHex(tx.scriptGasLimit), + script: hex0x(tx.script), + scriptData: hex0x(tx.scriptData), + maxFee: bnHex(tx.policies.maxFee), + inputs: tx.inputs.map((i) => { + if (i.type === 0) { + const id = concat(fromHex(i.txId), new Uint8Array([i.outputIndex >> 8, i.outputIndex & 0xff])); + return { + type: 0, + id: hex0x(id), + owner: i.owner, + amount: bnHex(i.amount), + assetId: i.assetId, + txPointer: hex0x(concat(word(i.txPointer.blockHeight), word(i.txPointer.txIndex))), + witnessIndex: i.witnessIndex, + predicateGasUsed: bnHex(i.predicateGasUsed), + predicate: hex0x(i.predicate), + predicateData: hex0x(i.predicateData), + }; + } + if (i.type === 1) return { type: 1, txID: i.txId, txPointer: hex0x(concat(word(i.txPointer.blockHeight), word(i.txPointer.txIndex))), contractId: i.contractId }; + return { + type: 2, + sender: i.sender, + recipient: i.recipient, + amount: bnHex(i.amount), + nonce: i.nonce, + witnessIndex: i.witnessIndex, + predicateGasUsed: bnHex(i.predicateGasUsed), + data: hex0x(i.data), + predicate: hex0x(i.predicate), + predicateData: hex0x(i.predicateData), + }; + }), + outputs: tx.outputs.map((o) => { + switch (o.type) { + case 0: + return { type: 0, to: o.to, amount: bnHex(o.amount), assetId: o.assetId }; + case 1: + return { type: 1, inputIndex: o.inputIndex }; + case 2: + return { type: 2, to: o.to, assetId: o.assetId }; + case 3: + return { type: 3 }; + case 4: + return { type: 4, contractId: o.contractId, stateRoot: o.stateRoot }; + } + }), + witnesses: tx.witnesses.map(hex0x), + }; + const p = tx.policies; + if (p.tip !== undefined) out.tip = bnHex(p.tip); + if (p.witnessLimit !== undefined) out.witnessLimit = bnHex(p.witnessLimit); + if (p.maturity !== undefined) out.maturity = p.maturity; + if (p.expiration !== undefined) out.expiration = p.expiration; + if (p.owner !== undefined) out.ownerInputIndex = Number(p.owner); + return out; +} diff --git a/packages/chains-fuel/src/util.ts b/packages/chains-fuel/src/util.ts new file mode 100644 index 00000000..11aa7fb8 --- /dev/null +++ b/packages/chains-fuel/src/util.ts @@ -0,0 +1,78 @@ +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +/** 0x-prefixed lower-case hex. */ +export function hex0x(bytes: Uint8Array): string { + return `0x${hex(bytes)}`; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.replace(/^0x/i, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +export const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s); + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +export function short(value: string): string { + return value.length > 14 ? `${value.slice(0, 6)}…${value.slice(-4)}` : value; +} + +export function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +/** Printable UTF-8 text (line breaks and tabs allowed), or null. */ +export function textOf(bytes: Uint8Array): string | null { + if (!bytes.length) return null; + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + return /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s) ? null : s; + } catch { + return null; + } +} + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/packages/chains-fuel/test/connector.test.ts b/packages/chains-fuel/test/connector.test.ts new file mode 100644 index 00000000..e5f55cb0 --- /dev/null +++ b/packages/chains-fuel/test/connector.test.ts @@ -0,0 +1,77 @@ +/** + * 1Mask's Fuel connector (packages/1mask/src/inpage/fuel.ts, imported by path: it is dependency-free and 1Mask has no + * fuels-ts dev dependency) against the real fuels-ts `Fuel` connector manager, with this module standing in for the + * wallet behind it: discovery by the FuelConnector event, connect, accounts, Account.signMessage, and a fuels-ts + * TransactionRequest instance crossing as JSON to the same transaction id. + */ +import { Fuel, Signer, hashMessage, transactionRequestify } from "fuels"; +import { describe, expect, it } from "vitest"; +import { installFuelConnector } from "../../1mask/src/inpage/fuel.js"; +import type { InpageTransport } from "../../1mask/src/inpage/transport.js"; +import { DEFAULT_IDENTITY } from "../../1mask/src/shared/config.js"; +import { FUEL_TESTNET, createFuelModule } from "../src/index.js"; +import { ACCOUNT0, SIG, TEXT_MESSAGE, TRANSFER_ID, TRANSFER_TX } from "./fixtures.js"; +import { ctxFor, fakeNode, sig } from "./helpers.js"; + +const mod = createFuelModule(); + +/** The wallet side, minus the router: connect grants, signing goes decode → prepare → (fixture signature) → finalize. */ +function walletTransport() { + let connected = false; + const node = fakeNode({}, [{ data: { submitAndAwaitStatus: { type: "SuccessStatus", transactionId: TRANSFER_ID } } }]); + const ctx = ctxFor(node.fetch); + const decoded: string[] = []; + const t: InpageTransport = { + async request(family, method, params) { + expect(family).toBe("fuel"); + const p = JSON.parse(JSON.stringify(params ?? null)); + switch (method) { + case "1mask_getAccounts": + return connected ? [{ address: ACCOUNT0.address }] : []; + case "fuel:connect": + connected = true; + return [{ address: ACCOUNT0.address }]; + case "fuel:currentNetwork": + return { url: FUEL_TESTNET.rpcUrls[0], chainId: 0 }; + case "fuel:networks": + return [{ url: FUEL_TESTNET.rpcUrls[0], chainId: 0 }]; + } + const req = { id: "r", origin: "https://app.example", via: "injected" as const, family: "fuel" as const, networkId: "fuel:0", method, params: p }; + decoded.push((await mod.decode(req, ctx)).title); + const [payload] = await mod.prepare(req, ctx, "a"); + const digest = Buffer.from(payload!.bytes).toString("hex"); + const s = Object.values(SIG).find((x) => x.digest === digest); + if (!s) throw new Error(`no fixture signature for ${digest}`); + return mod.finalize(req, [sig(s)], ctx); + }, + onEvent: () => () => {}, + destroy: () => {}, + }; + return { t, decoded }; +} + +describe("1Mask Fuel connector with fuels-ts Fuel", () => { + it("is found through the FuelConnector event and works as a connector", async () => { + const target = Object.assign(new EventTarget(), { CustomEvent }) as unknown as Window; + const fuel = new Fuel({ targetObject: target as never, storage: null }); + const { t, decoded } = walletTransport(); + const { connector } = installFuelConnector(target, DEFAULT_IDENTITY, t, { reannounce: false }); + + expect(await fuel.hasConnector()).toBe(true); + expect(fuel.currentConnector()?.name).toBe("Clip Wallet"); + expect((await fuel.connectors()).map((c) => [c.name, c.installed])).toEqual([["Clip Wallet", true]]); + expect(await fuel.isConnected()).toBe(false); + expect(await fuel.connect()).toBe(true); + expect(await fuel.accounts()).toEqual([ACCOUNT0.address]); + expect(await fuel.currentNetwork()).toEqual({ url: FUEL_TESTNET.rpcUrls[0], chainId: 0 }); + + const wallet = await fuel.getWallet(ACCOUNT0.address); + const signature = await wallet.signMessage(TEXT_MESSAGE); + expect(Signer.recoverAddress(hashMessage(TEXT_MESSAGE), signature).toString()).toBe(ACCOUNT0.address); + + const id = await connector.sendTransaction(ACCOUNT0.address, transactionRequestify(TRANSFER_TX as never), { provider: { url: FUEL_TESTNET.rpcUrls[0]! } }); + expect(id).toBe(TRANSFER_ID); + expect(decoded).toEqual(["Sign a message for app.example", "Send 0.000000001 ETH to 0x033d…5e17"]); + fuel.unsubscribe(); + }); +}); diff --git a/packages/chains-fuel/test/fixtures.ts b/packages/chains-fuel/test/fixtures.ts new file mode 100644 index 00000000..164712de --- /dev/null +++ b/packages/chains-fuel/test/fixtures.ts @@ -0,0 +1,98 @@ +import type { Account } from "@clip-wallet/core"; + +/** + * Fixtures for the public BIP-39 test account ("abandon" ×11 + "about"), Fuel path m/44'/1179993420'/i'/0/0. + * Public keys come from the vault's deriveAccount; signatures were made OFFLINE with the vault's signEcdsa over the + * digests below (RFC 6979, low-S, recovery id separate) and pasted here. Nothing secret. + */ +export const ACCOUNT0 = { + publicKey: "026760754232b8ae531b05039f630cf2549027d47dfe6af603f7504616602dfcd2", + address: "0x806EC69A1bC1398877A7327c467EE09511B3f26DcfB55b305D8ea2cd2c285b89", +}; +export const ACCOUNT1 = { + publicKey: "02299ba5cbc5529234b97541ee4067d57a02fad90b2b5778a1012dc0510496a57e", + address: "0x033d76e24F5D3b02966B27932d9D88008175739642951103b1074f90b3785E17", +}; + +export const ME = ACCOUNT0.address.toLowerCase(); +export const OTHER = ACCOUNT1.address.toLowerCase(); +export const ETH = "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07"; +export const USDC_TESTNET = "0xc26c91055de37528492e7e97d91c6f4abe34aae26f2c4d25cff6bfe45b5dc9a9"; +export const CONTRACT = "0xd02112ef9c39f1cea7c8527c26242ca1f5d26bcfe8d1564bee054d3b04175471"; + +export function account(): Account { + return { + id: "fuel:0", + family: "fuel", + index: 0, + curve: "secp256k1", + derivationPath: "m/44'/1179993420'/0'/0/0", + publicKey: ACCOUNT0.publicKey, + address: ACCOUNT0.address, + }; +} + +/** + * A 1-unit ETH transfer to account 1 on the testnet, as createFuelModule().buildTransfer built it against + * testnet.fuel.network (Oct 2026). Its id was checked against fuels-ts `getTransactionId(0)`, and the transaction + * signed with SIG.transfer passed `dryRun(utxoValidation: true)` on the testnet. + */ +export const TRANSFER_TX = { + type: 0, + gasLimit: "0x40", + script: "0x24000000", + scriptData: "0x", + maxFee: "0xa8", + inputs: [ + { + type: 0, + id: "0xb923732656179a9a2f8f6b4af195fccfc7f7c0f11f63f03e512b4dc82b50fbb50001", + owner: ME, + amount: "0x197ae8", + assetId: ETH, + txPointer: "0x00000000000000000000000000000000", + witnessIndex: 0, + predicateGasUsed: "0x0", + predicate: "0x", + predicateData: "0x", + }, + ], + outputs: [ + { type: 0, to: OTHER, amount: "0x1", assetId: ETH }, + { type: 2, to: ME, assetId: ETH }, + ], + witnesses: [`0x${"00".repeat(64)}`], +}; +export const TRANSFER_ID = "0x1c39032b4f6237be5d831674d66198ba847cf258dcfc772afbcb77dec1d630f5"; + +export const TEXT_MESSAGE = "Clip Wallet dapp matrix: sign-in check (testnet)"; + +/** r ‖ s (64 bytes hex) + recovery id, by digest. */ +export const SIG = { + transfer: { digest: TRANSFER_ID.slice(2), rs: "0e8e8250685cf95cf04184ecbfa6036a6f39d18b9f8487d4cd800561abe89a957c844a5bd036c05ac6a556587c0224c4d70d2614d8b8f80bb775c563f89eb8fd", recovery: 1 }, + text: { digest: "b039b8d8acf23193ca9097154740fd177a933f8c4e86b79d00e575a026e93b38", rs: "990aa9ea2bd26a2994dd486b18c11c7fa81f2a68066a93de4ff7bbcf07e5262124664d9fb670ee07f9313f878f192339bf26452a46ada233d029baff7ca98256", recovery: 0 }, + /** { personalSign: "Hello Fuel" } */ + personal: { digest: "3d47a86470d42088052ed1b8253c8825b8b433b3f1e1f71f9cd9e59087159125", rs: "4ebf51e401d4f4d52f27aba898a8bd58d921620f8051c036d83196aa045dd3ad3b90dee33f9673ad8418a12db8ef85be7e04194776d07a9c9caf1deee9f41ed9", recovery: 1 }, + /** { personalSign: bytes 0xdeadbeef } */ + personalHex: { digest: "bee139ab826234d6edba1c8688a15dd51567bb7abebcff664f2f4c217c07d89d", rs: "f132fad4e1451385762c02c837d4a31441d0e5545759d984b299dc26b01da8eb18f2aac7007dda6b6b323d78eb4859fb4c364d6164d3f9b35a25757ab56280a2", recovery: 1 }, + /** Over sha256("not the request"): a valid signature by this key for something else. */ + other: { digest: "be3ddd57830a8596c34e9bae289e0c10b1591311b3ddedaa397f54882ef2d8e6", rs: "ccc8e27d58bf7fb705e36b095a125e17003e4fcf5194b46d2ff0b7d42cf9ce190df9e67bee73b39caae9de34a5373a932a2a1e9418aceb696fbff31dab3c4d57", recovery: 1 }, +}; + +/** Consensus parameters as both endpoints reported them (Oct 2026). */ +export const CHAIN = (chainId: number) => ({ + chain: { + consensusParameters: { + chainId: String(chainId), + baseAssetId: ETH, + txParams: { maxInputs: "255", maxOutputs: "255", maxGasPerTx: "30000000", maxSize: "112640" }, + feeParams: { gasPriceFactor: "1150000", gasPerByte: "1" }, + gasCosts: { + ecr1: "31208", + vmInitialization: { base: "4846", unitsPerGas: "39" }, + s256: { base: "36", unitsPerGas: "3" }, + contractRoot: { base: "36", unitsPerGas: "2" }, + }, + }, + }, +}); diff --git a/packages/chains-fuel/test/fuel.test.ts b/packages/chains-fuel/test/fuel.test.ts new file mode 100644 index 00000000..d43ea58f --- /dev/null +++ b/packages/chains-fuel/test/fuel.test.ts @@ -0,0 +1,318 @@ +import { ClipError, WALLET_ORIGIN } from "@clip-wallet/core"; +import { InputType, OutputType, ScriptTransactionRequest, Signer, hashMessage as fuelsHashMessage, transactionRequestify } from "fuels"; +import { describe, expect, it } from "vitest"; +import { FUEL_MAINNET, FUEL_METHODS, createFuelModule, encodeScriptTx, parseTransactionRequest, requiredMaxFee } from "../src/index.js"; +import { gqlFor } from "../src/gql.js"; +import { hex } from "../src/util.js"; +import { ACCOUNT0, CHAIN, CONTRACT, ETH, ME, OTHER, SIG, TEXT_MESSAGE, TRANSFER_ID, TRANSFER_TX, USDC_TESTNET } from "./fixtures.js"; +import { ctxFor, fakeNode, req, sig } from "./helpers.js"; + +const mod = createFuelModule({ pollMs: 0 }); +const send = (tx: unknown, over = {}) => req(FUEL_METHODS.sendTransaction, { address: ACCOUNT0.address, transaction: tx, ...over }); +const clip = async (p: Promise, code: string) => { + const e = await p.then(() => null, (x: unknown) => x); + expect(e).toBeInstanceOf(ClipError); + expect((e as ClipError).code).toBe(code); + return e as ClipError; +}; + +/** A script that calls a contract, forwarding 1000 ETH units (fuels-ts ScriptTransactionRequest, JSON as sent). */ +function contractCall(over: { changeTo?: string; predicate?: boolean } = {}) { + const r = new ScriptTransactionRequest({ gasLimit: 250_000, maxFee: 9_000, script: "0x724028c0724428985d451000724828a02d41148a24040000", scriptData: "0x00000000000003e8" }); + r.inputs.push({ + type: InputType.Coin, + id: `0x${"33".repeat(32)}0001`, + owner: over.predicate ? OTHER : ME, + amount: 1_669_864, + assetId: ETH, + txPointer: "0x00000000000000000000000000000000", + witnessIndex: 0, + ...(over.predicate ? { predicate: "0x1a40500091000020", predicateData: "0x" } : {}), + } as never); + if (over.predicate) { + r.inputs.push({ type: InputType.Coin, id: `0x${"34".repeat(32)}0000`, owner: ME, amount: 10, assetId: ETH, txPointer: "0x00", witnessIndex: 0 } as never); + } + r.inputs.push({ type: InputType.Contract, contractId: CONTRACT, txPointer: "0x00000000000000000000000000000000" } as never); + r.outputs.push({ type: OutputType.Contract, inputIndex: over.predicate ? 2 : 1 } as never); + r.outputs.push({ type: OutputType.Variable } as never); + r.outputs.push({ type: OutputType.Change, to: over.changeTo ?? ME, assetId: ETH } as never); + r.witnesses.push(new Uint8Array(64)); + return JSON.parse(JSON.stringify(r)); +} + +const receipts = [ + { receiptType: "CALL", id: null, to: CONTRACT, amount: "1000", assetId: ETH }, + { receiptType: "TRANSFER_OUT", id: CONTRACT, toAddress: ME, amount: "5000000", assetId: USDC_TESTNET }, + { receiptType: "RETURN", id: CONTRACT }, + { receiptType: "SCRIPT_RESULT", result: "0", gasUsed: "41000" }, +]; +const dryOk = (r = receipts) => ({ dryRun: [{ id: "0x01", status: { type: "DryRunSuccessStatus", totalGas: "50000", totalFee: "100" }, receipts: r }] }); + +describe("decode", () => { + it("reads a wallet-built ETH transfer without touching the network", async () => { + const node = fakeNode({}); + const d = await mod.decode(send(TRANSFER_TX, {}), ctxFor(node.fetch)); + expect(node.calls).toEqual([]); + expect(d.title).toBe("Send 0.000000001 ETH to 0x033d…5e17"); + expect(d.titleMsg?.id).toBe("bg.req.sendTo"); + expect(d.lines).toEqual([ + { label: "To", value: `${OTHER} (0.000000001 ETH)` }, + { label: "Network fee at most", value: "0.000000168 ETH" }, + ]); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "eth-testnet", symbol: "ETH" }), delta: "-1" }]); + expect(d.fee).toMatchObject({ amount: "168", asset: { symbol: "ETH" } }); + expect(d).toMatchObject({ blind: false, simulated: false, warnings: [] }); + }); + + it("dry-runs a contract call and shows what moves from the receipts", async () => { + const node = fakeNode({ dryRun: (v) => (expect(v.v).toBe(false), expect(v.p).toBe("0"), dryOk()) }); + const d = await mod.decode(send(contractCall()), ctxFor(node.fetch)); + expect(node.calls.map((c) => c.op)).toEqual(["dryRun"]); + expect(d.title).toBe("Approve a smart contract action for app.example"); + expect(d.simulated).toBe(true); + expect(d.blind).toBe(false); + expect(d.balanceChanges).toEqual([ + { asset: expect.objectContaining({ key: "eth-testnet" }), delta: "-1000" }, + { asset: expect.objectContaining({ key: "usdc.e", symbol: "USDC" }), delta: "5000000" }, + ]); + expect(d.lines).toContainEqual({ label: "Contract", value: CONTRACT }); + expect(d.warnings.map((w) => w.code)).toEqual(["unknown-call"]); + }); + + it("is blind when the dry run fails, and when it can't run", async () => { + const failed = fakeNode({ dryRun: { dryRun: [{ id: "0x01", status: { type: "DryRunFailureStatus", totalGas: "1", totalFee: "1", reason: "Revert(123)" }, receipts: [] }] } }); + const d = await mod.decode(send(contractCall()), ctxFor(failed.fetch)); + expect(d).toMatchObject({ blind: true, simulated: false, balanceChanges: [] }); + expect(d.warnings[0]).toMatchObject({ code: "simulation-failed", level: "danger" }); + expect(d.warnings[0]!.message).not.toContain("Revert"); + const offline = fakeNode({ dryRun: { errors: [{ message: "boom" }] } }); + expect((await mod.decode(send(contractCall()), ctxFor(offline.fetch))).blind).toBe(true); + expect((await createFuelModule({ simulate: false }).decode(send(contractCall()), ctxFor(offline.fetch))).blind).toBe(true); + }); + + it("is blind for predicates and for transactions that aren't scripts", async () => { + const node = fakeNode({ dryRun: dryOk() }); + const p = await mod.decode(send(contractCall({ predicate: true })), ctxFor(node.fetch)); + expect(p.blind).toBe(true); + expect(p.warnings[0]!.code).toBe("blind-signing"); + const create = await mod.decode(send({ type: 1, bytecodeWitnessIndex: 0, inputs: [], outputs: [], witnesses: [] }), ctxFor(node.fetch)); + expect(create).toMatchObject({ blind: true, title: "Approve a transaction for app.example" }); + }); + + it("warns in red when the change goes to someone else", async () => { + const node = fakeNode({ dryRun: dryOk() }); + const d = await mod.decode(send(contractCall({ changeTo: OTHER })), ctxFor(node.fetch)); + const w = d.warnings.find((x) => x.msg?.id === "bg.fuel.changeToOther"); + expect(w).toMatchObject({ level: "danger", message: "Everything left of your ETH after this goes to 0x033d…5e17, not back to you." }); + expect(d.balanceChanges.find((c) => c.asset.symbol === "ETH")?.delta).toBe("-1669864"); + }); + + it("warns when nothing returns the leftover", async () => { + const tx = { ...TRANSFER_TX, outputs: [TRANSFER_TX.outputs[0]] }; + const d = await mod.decode(send(tx), ctxFor(fakeNode({}).fetch)); + expect(d.warnings.find((w) => w.msg?.id === "bg.fuel.leftoverLost")?.message).toBe("0.001669695 ETH isn't sent anywhere by this transaction and would be lost."); + }); + + it("refuses the wrong network, another account, other signers and junk", async () => { + const ctx = ctxFor(fakeNode({}).fetch); + await clip(mod.decode(send(TRANSFER_TX, {}), ctxFor(fakeNode({}).fetch, FUEL_MAINNET)), "fuel/network-mismatch"); + await clip(mod.decode(req(FUEL_METHODS.sendTransaction, { transaction: TRANSFER_TX }, { networkId: "fuel:9889" }), ctx), "fuel/network-mismatch"); + await clip(mod.decode(send(TRANSFER_TX, { provider: { url: "https://mainnet.fuel.network/v1/graphql" } }), ctx), "fuel/network-mismatch"); + await clip(mod.decode(req(FUEL_METHODS.sendTransaction, { address: OTHER, transaction: TRANSFER_TX }), ctx), "fuel/not-your-account"); + const theirs = { ...TRANSFER_TX, inputs: [{ ...TRANSFER_TX.inputs[0], owner: OTHER }] }; + await clip(mod.decode(send(theirs), ctx), "fuel/not-a-signer"); + await clip(mod.decode(send({ ...TRANSFER_TX, witnesses: [] }), ctx), "fuel/malformed"); + await clip(mod.decode(send("{"), ctx), "fuel/malformed"); + await clip(mod.decode(req("fuel_signTypedData", {}), ctx), "fuel/unsupported-method"); + }); + + it("says who gets a transaction that is only signed", async () => { + const d = await mod.decode(req(FUEL_METHODS.signTransaction, { address: ACCOUNT0.address, transaction: TRANSFER_TX }), ctxFor(fakeNode({}).fetch)); + expect(d.lines.at(-1)).toEqual({ label: "Sent by", value: "app.example (it gets the signed transaction)" }); + }); + + it("shows messages as text, and refuses unreadable plain strings", async () => { + const ctx = ctxFor(fakeNode({}).fetch); + const t = await mod.decode(req(FUEL_METHODS.signMessage, { address: ACCOUNT0.address, message: { text: TEXT_MESSAGE } }), ctx); + expect(t).toMatchObject({ title: "Sign a message for app.example", lines: [{ label: "Message", value: TEXT_MESSAGE }], blind: false, warnings: [] }); + const raw = await mod.decode(req(FUEL_METHODS.signMessage, { message: { text: "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000tx" } }), ctx); + expect(raw.blind).toBe(true); + const bytes = await mod.decode(req(FUEL_METHODS.signMessage, { message: { personalSignHex: "0xdeadbeef" } }), ctx); + expect(bytes).toMatchObject({ blind: false, lines: [{ label: "Message (not text)", value: "0xdeadbeef" }] }); + expect(bytes.warnings[0]!.code).toBe("blind-signing"); + }); +}); + +describe("prepare", () => { + it("signs the transaction id and fuels-ts' message hash", async () => { + const ctx = ctxFor(fakeNode({}).fetch); + const [p] = await mod.prepare(send(TRANSFER_TX), ctx, "ap1"); + expect(p).toMatchObject({ accountId: "fuel:0", scheme: "ecdsa-secp256k1", approvalId: "ap1" }); + expect(`0x${hex(p!.bytes)}`).toBe(transactionRequestify(TRANSFER_TX as never).getTransactionId(0)); + const [m] = await mod.prepare(req(FUEL_METHODS.signMessage, { message: TEXT_MESSAGE }), ctx, "ap2"); + expect(`0x${hex(m!.bytes)}`).toBe(fuelsHashMessage(TEXT_MESSAGE)); + }); +}); + +describe("finalize", () => { + const status = (s: unknown) => [{ data: { submitAndAwaitStatus: { type: "SubmittedStatus" } } }, { data: { submitAndAwaitStatus: s } }]; + + it("puts a fuels-ts compact signature in the witness and submits", async () => { + let submitted = ""; + const node = fakeNode({}, (v) => ((submitted = v.tx), status({ type: "SuccessStatus", transactionId: TRANSFER_ID }))); + const id = await mod.finalize(send(TRANSFER_TX), [sig(SIG.transfer)], ctxFor(node.fetch)); + expect(id).toBe(TRANSFER_ID); + expect(node.calls.map((c) => c.url)).toEqual(["https://testnet.fuel.network/v1/graphql-sub"]); + const witness = submitted.slice(-128); + expect(Signer.recoverAddress(TRANSFER_ID, `0x${witness}`).toString()).toBe(ACCOUNT0.address); + const p = parseTransactionRequest(TRANSFER_TX); + if (p.kind !== "script") throw new Error(); + expect(submitted.slice(0, -128)).toBe(`0x${hex(encodeScriptTx(p.tx)).slice(0, -128)}`); + }); + + it("refuses a bad signature before anything is sent", async () => { + const node = fakeNode({}, status({ type: "SuccessStatus" })); + const ctx = ctxFor(node.fetch); + await clip(mod.finalize(send(TRANSFER_TX), [sig(SIG.other)], ctx), "fuel/bad-signature"); + await clip(mod.finalize(send(TRANSFER_TX), [sig(SIG.transfer, 0)], ctx), "fuel/bad-signature"); + await clip(mod.finalize(send(TRANSFER_TX), [{ ...sig(SIG.transfer), scheme: "ed25519" }], ctx), "fuel/bad-signature"); + await clip(mod.finalize(send(TRANSFER_TX), [], ctx), "fuel/bad-signature"); + expect(node.calls).toEqual([]); + }); + + it("finds the recovery id when the signer leaves it out", async () => { + const node = fakeNode({}, status({ type: "SuccessStatus" })); + expect(await mod.finalize(send(TRANSFER_TX), [sig(SIG.transfer, undefined)], ctxFor(node.fetch))).toBe(TRANSFER_ID); + }); + + it("returns the signed request for fuel_signTransaction", async () => { + const node = fakeNode({}); + const out = (await mod.finalize(req(FUEL_METHODS.signTransaction, { transaction: TRANSFER_TX }), [sig(SIG.transfer)], ctxFor(node.fetch))) as Record; + expect(node.calls).toEqual([]); + const signed = transactionRequestify(out as never); + expect(signed.getTransactionId(0)).toBe(TRANSFER_ID); + expect(Signer.recoverAddress(TRANSFER_ID, signed.witnesses[0] as string).toString()).toBe(ACCOUNT0.address); + }); + + it("returns a message signature that fuels-ts Signer.recoverAddress accepts", async () => { + const ctx = ctxFor(fakeNode({}).fetch); + const cases: [unknown, { rs: string; recovery: number }, string][] = [ + [TEXT_MESSAGE, SIG.text, fuelsHashMessage(TEXT_MESSAGE)], + [{ personalSign: "Hello Fuel" }, SIG.personal, fuelsHashMessage({ personalSign: "Hello Fuel" })], + [{ personalSignHex: "0xdeadbeef" }, SIG.personalHex, fuelsHashMessage({ personalSign: new Uint8Array([0xde, 0xad, 0xbe, 0xef]) })], + ]; + for (const [message, s, digest] of cases) { + const out = (await mod.finalize(req(FUEL_METHODS.signMessage, { message }), [sig(s)], ctx)) as string; + expect(out).toMatch(/^0x[0-9a-f]{128}$/); + expect(Signer.recoverAddress(digest, out).toString()).toBe(ACCOUNT0.address); + } + }); + + it("says in plain words when the network refuses or the transaction fails", async () => { + const failed = fakeNode({}, status({ type: "FailureStatus", reason: "Revert(0)" })); + const e = await clip(mod.finalize(send(TRANSFER_TX), [sig(SIG.transfer)], ctxFor(failed.fetch)), "fuel/failed"); + expect(e.userMessage).toBe("This transaction failed on the Fuel network. Only the network fee was spent."); + const fee = fakeNode({}, [{ data: null, errors: [{ message: "InsufficientMaxFee { max_fee_from_policies: 1, max_fee_from_gas_price: 28 }" }] }]); + const e2 = await clip(mod.finalize(send(TRANSFER_TX), [sig(SIG.transfer)], ctxFor(fee.fetch)), "fuel/fee-too-low"); + expect(e2.userMessage).not.toContain("InsufficientMaxFee"); + const spent = fakeNode({}, [{ data: null, errors: [{ message: "Transaction validity: Validity(UtxoNotFound(0xb9…))" }] }]); + await clip(mod.finalize(send(TRANSFER_TX), [sig(SIG.transfer)], ctxFor(spent.fetch)), "fuel/coins-spent"); + }); + + it("polls when the status stream ends after the transaction was accepted", async () => { + let polls = 0; + const node = fakeNode({ getTransaction: () => ({ transaction: { status: { type: ++polls < 2 ? "SubmittedStatus" : "SuccessStatus" } } }) }, [{ data: { submitAndAwaitStatus: { type: "SubmittedStatus" } } }]); + expect(await mod.finalize(send(TRANSFER_TX), [sig(SIG.transfer)], ctxFor(node.fetch))).toBe(TRANSFER_ID); + expect(polls).toBe(2); + }); +}); + +describe("buildTransfer", () => { + const coin = (amount: string, assetId = ETH, n = 1) => ({ __typename: "Coin", utxoId: `0x${"b9".repeat(32)}000${n}`, amount, assetId, owner: ME }); + + function node(coins: (v: Record) => unknown) { + return fakeNode({ + getChain: CHAIN(0), + estimateGasPrice: { estimateGasPrice: { gasPrice: "5188" } }, + getCoinsToSpend: (v) => coins(v), + dryRun: (v) => (expect(v.p).toBe("0"), dryOk([{ receiptType: "RETURN" }, { receiptType: "SCRIPT_RESULT", result: "0", gasUsed: "64" }] as never)), + }); + } + + it("builds an ETH send from the account's coins with a fee the node accepts", async () => { + const n = node((v) => (expect(v.owner).toBe(ME), { coinsToSpend: [[coin("1669864")]] })); + const ctx = ctxFor(n.fetch); + const r = await mod.buildTransfer({ asset: ctx.network.nativeAsset, to: OTHER, amount: "250" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, family: "fuel", networkId: "fuel:0", method: FUEL_METHODS.sendTransaction }); + const tx = (r.params as { transaction: Record }).transaction; + expect(tx.gasLimit).toBe("0x40"); + expect(tx.script).toBe("0x24000000"); + expect(tx.outputs).toEqual([ + { type: 0, to: OTHER, amount: "0xfa", assetId: ETH }, + { type: 2, to: ME, assetId: ETH }, + ]); + const p = parseTransactionRequest(tx); + if (p.kind !== "script") throw new Error(); + const chain = await gqlFor(ctx).chainInfo(); + expect(BigInt(tx.maxFee)).toBe(requiredMaxFee(p.tx, chain, 5188n) + 1n); + const d = await mod.decode(r, ctx); + expect(d.title).toBe("Send 0.00000025 ETH to 0x033d…5e17"); + expect(d.balanceChanges.map((c) => c.delta)).toEqual(["-250"]); + }); + + it("sends a token with change for the token and for ETH", async () => { + const n = node((v) => { + expect(v.q).toEqual([{ assetId: USDC_TESTNET, amount: "1500000" }, expect.objectContaining({ assetId: ETH })]); + return { coinsToSpend: [[coin("5000000", USDC_TESTNET, 2)], [coin("1000000")]] }; + }); + const ctx = ctxFor(n.fetch); + const usdc = { key: "usdc.e", symbol: "USDC", name: "Bridged USDC (Fuel)", decimals: 6, networkId: "fuel:0", address: USDC_TESTNET }; + const r = await mod.buildTransfer({ asset: usdc, to: OTHER, amount: "1500000" }, ctx); + const tx = (r.params as { transaction: Record }).transaction; + expect(tx.outputs).toEqual([ + { type: 0, to: OTHER, amount: "0x16e360", assetId: USDC_TESTNET }, + { type: 2, to: ME, assetId: USDC_TESTNET }, + { type: 2, to: ME, assetId: ETH }, + ]); + const d = await mod.decode(r, ctx); + expect(d.title).toBe("Send 1.5 USDC to 0x033d…5e17"); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "usdc.e" }), delta: "-1500000" }]); + }); + + it("refuses bad input and missing funds in plain words", async () => { + const poor = node(() => ({ errors: [{ message: `the target cannot be met due to insufficient coins available for ${ETH.slice(2)}. Collected: 0.` }] })); + const ctx = ctxFor(poor.fetch); + const eth = ctx.network.nativeAsset; + const e = await clip(mod.buildTransfer({ asset: eth, to: OTHER, amount: "1" }, ctx), "fuel/insufficient-funds"); + expect(e.userMessage).toBe("You don't have enough ETH to pay for this and its fee."); + await clip(mod.buildTransfer({ asset: eth, to: "0x1234", amount: "1" }, ctx), "fuel/bad-address"); + await clip(mod.buildTransfer({ asset: eth, to: ACCOUNT0.address, amount: "1" }, ctx), "fuel/self-transfer"); + await clip(mod.buildTransfer({ asset: eth, to: OTHER, amount: "0" }, ctx), "fuel/bad-amount"); + const wrong = fakeNode({ getChain: CHAIN(9889), estimateGasPrice: { estimateGasPrice: { gasPrice: "1" } } }); + await clip(mod.buildTransfer({ asset: eth, to: OTHER, amount: "1" }, ctxFor(wrong.fetch)), "fuel/network-mismatch"); + }); +}); + +describe("getBalances", () => { + it("shows ETH and curated tokens, and leaves unknown assets out", async () => { + const n = fakeNode({ + getBalances: { + balances: { + nodes: [ + { assetId: ETH, amount: "1669864" }, + { assetId: USDC_TESTNET, amount: "2500000" }, + { assetId: `0x${"ab".repeat(32)}`, amount: "999999999" }, + ], + }, + }, + }); + const b = await mod.getBalances(ctxFor(n.fetch)); + expect(b.map((x) => [x.asset.key, x.amount])).toEqual([ + ["eth-testnet", "1669864"], + ["usdc.e", "2500000"], + ]); + const empty = await mod.getBalances(ctxFor(fakeNode({ getBalances: { balances: { nodes: [] } } }).fetch)); + expect(empty).toEqual([{ asset: expect.objectContaining({ symbol: "ETH" }), amount: "0" }]); + expect(await mod.getNfts(ctxFor(n.fetch))).toEqual([]); + }); +}); diff --git a/packages/chains-fuel/test/helpers.ts b/packages/chains-fuel/test/helpers.ts new file mode 100644 index 00000000..95ecf5e4 --- /dev/null +++ b/packages/chains-fuel/test/helpers.ts @@ -0,0 +1,48 @@ +import type { ChainContext, DappRequest, Network, Signature } from "@clip-wallet/core"; +import { FUEL_TESTNET } from "../src/index.js"; +import { ACCOUNT0, account } from "./fixtures.js"; + +export type Handler = (variables: Record, query: string) => unknown; + +export interface Call { + url: string; + op: string; + variables: Record; +} + +/** + * Fake fuel-core: GraphQL operations are routed by operation name ("getChain", "dryRun", …); `sse` answers the + * submitAndAwaitStatus stream on `-sub` with the given events. A handler may return `{ errors }` itself. + */ +export function fakeNode(routes: Record, sse?: unknown[] | ((variables: Record) => unknown[])) { + const calls: Call[] = []; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const body = JSON.parse(String(init?.body ?? "{}")) as { query: string; variables?: Record }; + const op = /(?:query|mutation|subscription)\s+(\w+)/.exec(body.query)?.[1] ?? "?"; + calls.push({ url, op, variables: body.variables ?? {} }); + if (url.endsWith("-sub")) { + const events = typeof sse === "function" ? sse(body.variables ?? {}) : (sse ?? []); + const text = events.map((e) => `data:${JSON.stringify(e)}\n\n`).join(""); + return new Response(text, { status: 200, headers: { "content-type": "text/event-stream" } }); + } + const r = routes[op]; + if (r === undefined) return new Response(JSON.stringify({ errors: [{ message: `no fake for ${op}` }] }), { status: 200 }); + const v = typeof r === "function" ? (r as Handler)(body.variables ?? {}, body.query) : r; + const payload = v && typeof v === "object" && "errors" in (v as object) ? v : { data: v }; + return new Response(JSON.stringify(payload), { status: 200 }); + }) as typeof fetch; + return { fetch: f, calls }; +} + +export function ctxFor(fetchImpl: typeof fetch, network: Network = FUEL_TESTNET): ChainContext { + return { network, account: account(), fetch: fetchImpl }; +} + +export function req(method: string, params: unknown, over: Partial = {}): DappRequest { + return { id: "r1", origin: "https://app.example", via: "injected", family: "fuel", networkId: FUEL_TESTNET.id, method, params, ...over }; +} + +export function sig(s: { rs: string; recovery: number }, recovery: number | undefined = s.recovery): Signature { + return { scheme: "ecdsa-secp256k1", bytes: Uint8Array.from(Buffer.from(s.rs, "hex")), ...(recovery === undefined ? {} : { recovery }), publicKey: ACCOUNT0.publicKey }; +} diff --git a/packages/chains-fuel/test/tx.test.ts b/packages/chains-fuel/test/tx.test.ts new file mode 100644 index 00000000..ccfeb426 --- /dev/null +++ b/packages/chains-fuel/test/tx.test.ts @@ -0,0 +1,224 @@ +/** + * Hand-written serialization, ids, fees, addresses and message hashes, each cross-checked against fuels-ts + * (devDependency only). + */ +import { + Address, + InputType, + OutputType, + ScriptTransactionRequest, + Signer, + bn, + getMaxGas, + getMinGas, + calculateMetadataGasForTxScript, + hashMessage as fuelsHashMessage, + hexlify, + transactionRequestify, +} from "fuels"; +import { describe, expect, it } from "vitest"; +import { + FUEL_MAINNET, + FUEL_TESTNET, + assetFor, + createFuelModule, + encodeScriptTx, + fuelAddressFromPublicKey, + fuelNetOf, + hashMessage, + isFuelAddress, + maxGas, + minGas, + parseTransactionRequest, + toChecksum, + toTransactionRequestJson, + transactionId, + type ScriptTx, +} from "../src/index.js"; +import { gqlFor } from "../src/gql.js"; +import { fromHex, hex } from "../src/util.js"; +import { ACCOUNT0, ACCOUNT1, CHAIN, CONTRACT, ETH, ME, OTHER, TRANSFER_ID, TRANSFER_TX, USDC_TESTNET } from "./fixtures.js"; +import { ctxFor, fakeNode } from "./helpers.js"; + +const script = (json: unknown): ScriptTx => { + const p = parseTransactionRequest(json); + if (p.kind !== "script") throw new Error("not a script"); + return p.tx; +}; + +/** Requests built with fuels-ts itself, then serialized to JSON the way the Fuel connector sends them. */ +function fuelsRequests(): Record { + const coin = (id: string, assetId: string, amount: number, owner = ME, witnessIndex = 0) => ({ + type: InputType.Coin, + id, + owner, + amount: bn(amount), + assetId, + txPointer: "0x00000000000000000000000000000000", + witnessIndex, + }); + const token = new ScriptTransactionRequest({ gasLimit: 64, maxFee: 200 }); + token.inputs.push(coin(`0x${"11".repeat(32)}0002`, USDC_TESTNET, 5_000_000) as never, coin(`0x${"22".repeat(32)}0000`, ETH, 1_000_000) as never); + token.addCoinOutput(new Address(OTHER), 1_500_000, USDC_TESTNET); + token.addChangeOutput(new Address(ME), USDC_TESTNET); + token.addChangeOutput(new Address(ME), ETH); + token.witnesses.push(new Uint8Array(64)); + + const call = new ScriptTransactionRequest({ + gasLimit: 250_000, + maxFee: 9_000, + tip: 7, + maturity: 12, + expiration: 99_999_999, + witnessLimit: 4_096, + script: "0x724028c0724428985d451000724828a02d41148a24040000", + scriptData: "0x00000000000003e8f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07aa", + }); + call.inputs.push(coin(`0x${"33".repeat(32)}0001`, ETH, 1_669_864) as never); + call.inputs.push({ type: InputType.Contract, contractId: CONTRACT, txPointer: "0x00000000000000000000000000000000" } as never); + call.outputs.push({ type: OutputType.Contract, inputIndex: 1 } as never); + call.outputs.push({ type: OutputType.Variable } as never); + call.addChangeOutput(new Address(ME), ETH); + call.witnesses.push(new Uint8Array(64)); + + const message = new ScriptTransactionRequest({ gasLimit: 64, maxFee: 50 }); + message.inputs.push({ + type: InputType.Message, + sender: `0x${"44".repeat(32)}`, + recipient: ME, + amount: bn(42_000), + nonce: `0x${"55".repeat(32)}`, + witnessIndex: 0, + } as never); + message.addCoinOutput(new Address(OTHER), 1_000, ETH); + message.addChangeOutput(new Address(ME), ETH); + message.witnesses.push(new Uint8Array(64)); + + return { + transfer: TRANSFER_TX, + token: JSON.parse(JSON.stringify(token)), + call: JSON.parse(JSON.stringify(call)), + message: JSON.parse(JSON.stringify(message)), + }; +} + +describe("transactions (cross-checked with fuels-ts TransactionCoder and hashTransaction)", () => { + const cases = Object.entries(fuelsRequests()); + + it.each(cases)("%s: same bytes and id on both networks", (_name, json) => { + const ours = script(json); + const theirs = transactionRequestify(json as never); + expect(hex(encodeScriptTx(ours))).toBe(hexlify(theirs.toTransactionBytes()).slice(2)); + for (const chainId of [0, 9889]) expect(`0x${hex(transactionId(ours, chainId))}`).toBe(theirs.getTransactionId(chainId)); + }); + + it.each(cases)("%s: round-trips through toTransactionRequestJson (and fuels reads it back)", (_name, json) => { + const ours = script(json); + const back = toTransactionRequestJson(ours); + expect(hex(encodeScriptTx(script(back)))).toBe(hex(encodeScriptTx(ours))); + expect(transactionRequestify(back as never).getTransactionId(0)).toBe(`0x${hex(transactionId(ours, 0))}`); + }); + + it("the live transfer fixture has the id fuels-ts and the testnet computed", () => { + expect(`0x${hex(transactionId(script(TRANSFER_TX), 0))}`).toBe(TRANSFER_ID); + }); + + it("accepts a JSON string and Uint8Array-shaped objects, and refuses junk", () => { + const asText = script(JSON.stringify(TRANSFER_TX)); + expect(hex(encodeScriptTx(asText))).toBe(hex(encodeScriptTx(script(TRANSFER_TX)))); + const objScript = script({ ...TRANSFER_TX, script: { 0: 0x24, 1: 0, 2: 0, 3: 0 } }); + expect(hex(objScript.script)).toBe("24000000"); + expect(() => parseTransactionRequest({ ...TRANSFER_TX, maxFee: -1 })).toThrow(); + expect(() => parseTransactionRequest({ ...TRANSFER_TX, inputs: [{ type: 0, id: "0x12" }] })).toThrow(); + expect(() => parseTransactionRequest("{not json")).toThrow(); + expect(parseTransactionRequest({ type: 1 })).toEqual({ kind: "other", type: 1 }); + }); +}); + +describe("fees (fuels-ts getMinGas / getMaxGas)", () => { + it.each(Object.entries(fuelsRequests()))("%s: same min and max gas", async (_name, json) => { + const { fetch } = fakeNode({ getChain: CHAIN(0) }); + const chain = await gqlFor(ctxFor(fetch)).chainInfo(); + const tx = script(json); + const req = transactionRequestify(json as never); + const gasCosts = { + ecr1: bn(chain.gasCosts.ecr1.toString()), + vmInitialization: chain.gasCosts.vmInitialization, + s256: chain.gasCosts.s256, + contractRoot: chain.gasCosts.contractRoot, + } as never; + const size = req.toTransactionBytes().length; + const theirMin = getMinGas({ + gasCosts, + gasPerByte: bn(1), + inputs: req.inputs, + txBytesSize: size, + metadataGas: calculateMetadataGasForTxScript({ gasCosts, txBytesSize: size }), + }); + expect(minGas(tx, chain).toString()).toBe(theirMin.toString()); + const witnessesLength = req.toTransaction().witnesses.reduce((a, w) => a + w.dataLength, 0); + const theirMax = getMaxGas({ gasPerByte: bn(1), witnessesLength, witnessLimit: req.witnessLimit, minGas: theirMin, gasLimit: (req as ScriptTransactionRequest).gasLimit, maxGasPerTx: bn(30_000_000) }); + expect(maxGas(tx, chain).toString()).toBe(theirMax.toString()); + }); +}); + +describe("addresses (fuels-ts Address / Signer)", () => { + it("derives the Fuel Wallet addresses of the abandon … about accounts 0 and 1", () => { + for (const a of [ACCOUNT0, ACCOUNT1]) { + const pub = fromHex(a.publicKey); + expect(fuelAddressFromPublicKey(pub)).toBe(a.address); + expect(new Address(Signer.extendPublicKey(`0x${a.publicKey}`)).toString()).toBe(a.address); + expect(createFuelModule().addressFromPublicKey(pub, FUEL_TESTNET)).toBe(a.address); + } + }); + + it("verifies checksums: exact checksum, one case, or refused", () => { + expect(isFuelAddress(ACCOUNT0.address)).toBe(true); + expect(isFuelAddress(ACCOUNT0.address.toLowerCase())).toBe(true); + expect(isFuelAddress(`0x${ACCOUNT0.address.slice(2).toUpperCase()}`)).toBe(true); + const typo = ACCOUNT0.address.replace("EC", "Ec"); + expect(typo).not.toBe(ACCOUNT0.address); + expect(isFuelAddress(typo)).toBe(false); + expect(isFuelAddress(ACCOUNT0.address.slice(0, -1))).toBe(false); + expect(isFuelAddress("fuel1sphvdxsmcyuc3pa8xfaywlhqj5gm8un9e76kkvzc35ze65ec6s3w5f4pkq")).toBe(false); + expect(toChecksum(ACCOUNT1.address.toLowerCase())).toBe(new Address(ACCOUNT1.address.toLowerCase()).toChecksum()); + }); + + it("uses the Fuel Wallet derivation path", () => { + const m = createFuelModule(); + expect(m.derivationPath(0)).toBe("m/44'/1179993420'/0'/0/0"); + expect(m.derivationPath(3)).toBe("m/44'/1179993420'/3'/0/0"); + expect(m.curve).toBe("secp256k1"); + expect(m.networksForAddress(ACCOUNT0.address, [FUEL_TESTNET, FUEL_MAINNET]).map((n) => n.id)).toEqual(["fuel:0", "fuel:9889"]); + expect(m.networksForAddress("0x12", [FUEL_TESTNET])).toEqual([]); + }); +}); + +describe("messages (fuels-ts hashMessage)", () => { + it("hashes plain strings, personalSign text and personalSign bytes the same way", () => { + expect(`0x${hex(hashMessage({ text: "hello" }))}`).toBe(fuelsHashMessage("hello")); + expect(`0x${hex(hashMessage({ personalSign: "Hello Fuel" }))}`).toBe(fuelsHashMessage({ personalSign: "Hello Fuel" })); + expect(`0x${hex(hashMessage({ personalSignHex: "0xdeadbeef" }))}`).toBe(fuelsHashMessage({ personalSign: new Uint8Array([0xde, 0xad, 0xbe, 0xef]) })); + }); +}); + +describe("networks", () => { + it("names the testnet and mainnet by the connector chain ids", () => { + expect(FUEL_TESTNET).toMatchObject({ id: "fuel:0", family: "fuel", testnet: true, rpcUrls: ["https://testnet.fuel.network/v1/graphql"] }); + expect(FUEL_MAINNET).toMatchObject({ id: "fuel:9889", testnet: false, rpcUrls: ["https://mainnet.fuel.network/v1/graphql"] }); + expect(fuelNetOf(0)).toBe("testnet"); + expect(fuelNetOf("9889")).toBe("mainnet"); + expect(fuelNetOf("https://mainnet.fuel.network/v1/graphql/")).toBe("mainnet"); + expect(fuelNetOf("https://evil.example/v1/graphql")).toBeNull(); + }); + + it("knows ETH and the bridged tokens, and never merges an unknown asset", () => { + expect(assetFor("fuel:9889", ETH)).toMatchObject({ key: "eth", symbol: "ETH", decimals: 9 }); + expect(assetFor("fuel:0", ETH)).toMatchObject({ key: "eth-testnet" }); + expect(assetFor("fuel:9889", "0x286c479da40dc953bddc3bb4c453b608bba2e0ac483b077bd475174115395e6b")).toMatchObject({ key: "usdc.e", symbol: "USDC", decimals: 6, bridged: true }); + expect(assetFor("fuel:9889", "0xa0265fb5c32f6e8db3197af3c7eb05c48ae373605b8165b6f4a51c5b0ba4812e")).toMatchObject({ key: "usdt.e", bridged: true }); + const unknown = assetFor("fuel:9889", `0x${"ab".repeat(32)}`); + expect(unknown.key).toBe(`fuel-asset:0x${"ab".repeat(32)}`); + expect(unknown.symbol).not.toBe("USDC"); + }); +}); diff --git a/packages/chains-fuel/tsconfig.json b/packages/chains-fuel/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-fuel/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-hedera/src/networks.ts b/packages/chains-hedera/src/networks.ts index 58b8d5f3..98f9f49d 100644 --- a/packages/chains-hedera/src/networks.ts +++ b/packages/chains-hedera/src/networks.ts @@ -1,5 +1,12 @@ import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; +/** `url` without trailing slashes (a scan, not a regex: linear on any input). */ +function withoutTrailingSlashes(url: string): string { + let end = url.length; + while (end > 0 && url[end - 1] === "/") end--; + return url.slice(0, end); +} + /** * Hedera networks. Ids are the CAIP-2 ids used by Hedera WalletConnect (HIP-30), verified against * `LEDGER_ID_MAPPINGS` in @hashgraph/hedera-wallet-connect 2.1.3 (src/lib/shared/utils.ts): @@ -71,7 +78,7 @@ export function ledgerOf(networkId: NetworkId): HederaLedger { } export function mirrorUrl(net: Network): string { - return (net.indexerUrl ?? MIRROR_NODE_URLS[ledgerOf(net.id)]).replace(/\/+$/, ""); + return withoutTrailingSlashes(net.indexerUrl ?? MIRROR_NODE_URLS[ledgerOf(net.id)]); } /** Canonical asset key for an HTS token: native USDC merges with USDC elsewhere; everything else is per-token. */ diff --git a/packages/chains-icp/LICENSE b/packages/chains-icp/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-icp/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-icp/NOTICE b/packages/chains-icp/NOTICE new file mode 100644 index 00000000..1900fee0 --- /dev/null +++ b/packages/chains-icp/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-icp +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-icp/README.md b/packages/chains-icp/README.md new file mode 100644 index 00000000..7ca17612 --- /dev/null +++ b/packages/chains-icp/README.md @@ -0,0 +1,209 @@ +# @clip-wallet/chains-icp + +Internet Computer `ChainModule` for Clip Wallet: it sends ICP, ckBTC, ckUSDC and ckETH and never touches keys. It +implements the small part of the IC HTTPS interface it needs by hand: CBOR, request ids, signed envelopes, query and +update calls, read_state, and the Candid encoding of the ledger types. + +It doesn't use `@dfinity/agent` at runtime. The runtime dependencies are `@noble/curves` (verification and point maths), +`@noble/hashes` and `@scure/base`. `@dfinity/agent`, `@dfinity/candid`, `@dfinity/principal` and +`@dfinity/identity-secp256k1` (2.4.x) are devDependencies; the tests use them to cross-check the bytes this module +produces. + +## Networks + +There is no public IC test network for ledger transfers. DFINITY instead runs test ledgers on mainnet, so Clip Wallet's +test network is mainnet restricted to those test ledgers: nothing of value can move on it. + +| NetworkId | what | boundary node (`rpcUrls[0]`) | ledgers | +|---|---|---|---| +| `icp:test` (testnet flag) | IC mainnet, test ledgers only | https://icp-api.io | TESTICP `xafvr-biaaa-aaaai-aql5q-cai`, ckTESTBTC `mc6ru-gyaaa-aaaar-qaaaq-cai`, ckSepoliaUSDC `yfumr-cyaaa-aaaar-qaela-cai`, ckSepoliaETH `apia6-jaaaa-aaaar-qabma-cai` | +| `icp:737ba355e855bd4b61279056603e0550` | IC mainnet | https://icp-api.io | ICP `ryjl3-tyaaa-aaaaa-aaaba-cai`, ckBTC `mxzaz-hqaaa-aaaar-qaada-cai`, ckUSDC `xevnm-gaaaa-aaaar-qafnq-cai`, ckETH `ss2fx-dyaaa-aaaar-qacoq-cai` | + +**Network ids.** ChainAgnostic/namespaces has no `icp` namespace yet. +- The mainnet id follows the open proposal (icvc/icp-namespace PR #1, which the identity working group lists as ON HOLD): + "icp:" + the first 32 hex characters of SHA-256 of the IC root key. The root key is the DER key from `/api/v2/status`, + and hashing it gives `737ba355…0550`. +- `icp:test` is Clip Wallet's own id and never leaves the wallet. + +**Endpoints.** https://icp-api.io and https://ic0.app both answered `GET /api/v2/status` (CBOR, with the root key) on +2026-10-06, without a key. + +**Ledgers and asset keys.** +- Canister ids come from dfinity/ic: `rs/nns/canister_ids.json`, `rs/bitcoin/ckbtc/*/canister_ids.json` and + `rs/ethereum/cketh/*/canister_ids.json`. +- Every ledger's `icrc1_symbol`, `icrc1_decimals` and `icrc1_fee` were checked live with this module's own query client: + - ICP: 8 decimals, fee 10,000 + - ckBTC: 8 decimals, fee 10 + - ckUSDC: 6 decimals, fee 10,000 + - ckETH: 18 decimals, fee 2,000,000,000,000 + - TESTICP: 8 decimals, fee 10,000 + - ckSepoliaUSDC: 6 decimals, fee 4,000 + - The fee is always read with `icrc1_fee` at send time, never hard-coded. +- ICP and TESTICP answer the ICP ledger's legacy `symbol` query, so they also take `transfer` to an account + identifier. The ck ledgers don't. +- Asset keys: + - ICP / TESTICP: `icp`, the native asset. + - ckBTC / ckTESTBTC: `ckbtc`, bridged. + - ckUSDC / ckSepoliaUSDC: `ckusdc`, bridged. + - ckETH / ckSepoliaETH: `cketh`, bridged. + - The chain-key twins never merge with BTC, USDC or ETH. + +## Accounts and addresses + +- **Derivation.** `derivationPath(i)` = `m/44'/223'/0'/0/i`, secp256k1. That's what Plug, `dfx identity import` and + `@dfinity/identity-secp256k1` `fromSeedPhrase` use. +- **Address.** `Account.address` is the self-authenticating principal: SHA-224(DER SubjectPublicKeyInfo of the + uncompressed key) ‖ 0x02, as principal text. For the fixtures, the "abandon … about" principal is `tgzar-…-aqe`. +- **`isAddress` accepts three forms:** + - a principal, checksum checked; + - an ICRC-1 account text, `-.` (ICRC-1 `TextualEncoding.md`, all of its examples + tested); + - a 64-hex ICP account identifier, with its CRC-32 checked. +- **`accountIdOf(principal)`** gives the default account identifier. Exchanges ask for this to deposit ICP. + +## Sending (`buildTransfer` → decode → prepare → finalize) + +`buildTransfer({ asset, to, amount })` builds a ledger call. + +**Methods.** +- To a principal (or an ICRC-1 account): `icrc1_transfer`, with arguments `{ to, amount, fee: [current fee], memo: [], + created_at_time: [now], from_subaccount: [] }`. +- To an account identifier (ICP and TESTICP only): the ICP ledger's legacy `transfer`, with arguments `{ to: blob, + amount: { e8s }, fee: { e8s }, memo: 0, created_at_time }`. Exchanges use this form. +- `created_at_time` turns on the ledger's de-duplication, so a call that gets re-sent isn't paid twice. + +**Request shape.** The request uses ICRC-49's `icrc49_call_canister` params (`canisterId`, `sender`, `method`, `arg` +as base64 Candid, `nonce`) plus `ingressExpiry`: now + 4 minutes, in nanoseconds. Fixing the expiry before approval +means decode, prepare and finalize all see the same content. Only the wallet may send this method. + +**Refusals.** +- Your own principal or account id → `icp/self-transfer`. +- An address that doesn't parse → `icp/bad-address`. +- An amount of zero → `icp/bad-amount`. +- A balance below the amount plus the fee → `icp/insufficient-funds`. +- An account id for a ck token → `icp/account-id-unsupported` ("ask for their principal ID"). + +**decode** reads the Candid bytes that will be signed. It never uses a separate summary, and it refuses anything that +doesn't re-encode to exactly the same bytes. +- It shows "Send 1.5 ICP to mnnk5…yqe", the full recipient, the memo if there is one, and the network fee, which is + paid in the token itself. +- The balance change is amount + fee. +- An account-id recipient adds an info note: it's the kind of deposit address exchanges give, so check it matches + exactly. +- An expired request is refused (`icp/expired`). + +**prepare** returns two `ecdsa-secp256k1` payloads. Each is the 32-byte digest SHA-256("\x0Aic-request" ‖ request +id) (spec "Authentication"): +- one for the call; +- one for a single read_state request for `/request_status/`. Only the call's sender may read that path, and the + signed request can be re-sent until it expires, so polling needs no second approval. + +**finalize** works in this order: +1. Both signatures are verified with `secp256k1.verify` against the account's key, and the key's principal must be the + sender. A bad or missing signature → `icp/bad-signature`, and nothing is sent. +2. The CBOR envelope (`content`, `sender_pubkey` = DER key, `sender_sig` = 64-byte r‖s) goes to the synchronous + `POST /api/v4/canister//call`. +3. A 200 response carries a certificate. A 202 means the call is still running: the signed read_state is sent to + `POST /api/v3/canister//read_state` (default 10 × 1 s) until the status is `replied`, `rejected` or `done`. +4. The reply's Candid `TransferResult` is decoded. + - `Ok` → `{ status: "replied", blockIndex, requestId, contentMap, certificate }`, the ICRC-49 response fields. + - `Err` → a plain `ClipError` (`plainTransferError`): insufficient funds with the balance, the fee changed, too old, + created in the future, duplicate, temporarily unavailable, or generic. + - Rejected → `icp/rejected`. + - Still processing after the last poll → `{ status: "pending" }`. + +**Live check (2026-10-06).** A real signed `icrc1_transfer` from the unfunded "abandon … about" principal on the +TESTICP ledger was accepted by `icp-api.io`. The envelope, DER key, signature and request id all passed. The ledger +answered `Err InsufficientFunds`, which came back as "You don't have enough TESTICP…". Nothing moved. + +### Certificates are NOT verified + +Neither the `read_state` / call certificates (BLS over the state tree, with subnet delegations) nor the node +signatures on query responses are verified. Whatever a boundary node returns is a report, never proof: +- balances are for display only; +- `blockIndex` is what the node claimed. + +Nothing in this module bases a security decision on a reply. The user approves the call the wallet itself encoded, +before anything is sent. Before using a reply as proof (for example, before crediting a deposit), verify the +certificate with `@dfinity/agent`'s `Certificate.create`. + +## Balances and NFTs + +- `getBalances` makes an anonymous `icrc1_balance_of` query (sender 0x04, `POST /api/v3/canister//query`) to + every ledger of the network. + - The native token is always listed. + - The others are listed only when the account holds some. + - Only the default subaccount is read. +- `getNfts` returns `[]`. There's no shared index of ICP NFTs (ICRC-7, EXT, DIP-721) to list them from. + +## Dapp connectivity: send and receive only (no injected provider) + +The standards a wallet extension would need aren't finalized. dfinity/wg-identity-authentication at `202e6df` (README +status table) lists: +- **APPROVED by the working group:** ICRC-21 (consent messages), ICRC-25 (signer interaction), ICRC-27 (accounts), + ICRC-29 (window postMessage transport) and ICRC-49 (call canister). None of them is an NNS-approved "standard" yet. +- **DRAFT:** ICRC-94, "Browser Extension Discovery and Transport", is the only way an extension can be discovered under + its own identity. It works through `icrc94:announceProvider` / `icrc94:requestProvider` events modelled on EIP-6963. + +ICRC-94 is already wired up in some dapp libraries: +- `@nfid/identitykit` 2.0.0 calls `BrowserExtensionTransport.discover()` by default. +- `@slide-computer/signer-extension` and `@icp-sdk/signer` implement it. +- `@dfinity/oisy-wallet-signer` doesn't; it uses ICRC-29 popups only. + +Even so, the brief is to implement a provider only on finalized standards. Plug's `window.ic.plug` is proprietary, and +we don't impersonate it. + +**When ICRC-94 is accepted**, the provider would work like this: +- 1Mask announces Clip Wallet's own uuid, name, icon and rdns. +- The background answers ICRC-25 permissions and ICRC-27 accounts. +- ICRC-49 calls come in, with decode calling the target canister's ICRC-21 `icrc21_canister_call_consent_message`. The + ICP ledger and every ck ledger advertise ICRC-21 through `icrc10_supported_standards`. +- `finalize` already returns ICRC-49's `contentMap` and `certificate`. + +**Dapp matrix.** +- L0 (send and receive on `icp:test` with TESTICP) works today. +- L1 (connect) and L2 (sign/call) would use IdentityKit or `@icp-sdk/signer` (`BrowserExtensionTransport.discover()`, + then `Signer.callCanister`, then `SignerAgent`, which verifies the certificate) once ICRC-94 is final. ICRC-32 + (sign challenge) is on hold, so L2 would be an ICRC-49 call, not a message signature. + +## Faucets + +- **TESTICP:** https://faucet.internetcomputer.org. No login and no captcha in the page. Each request gives 10 + TESTICP; you paste a principal (ICRC-1) or an account id. The page calls the faucet canister `nqoci-rqaaa-aaaap-qp53q-cai`. +- **ckTESTBTC:** needs Bitcoin testnet4 coins, sent to the ckBTC minter's testnet deposit address. Testnet4 faucets + usually have captchas. +- **ckSepoliaETH / ckSepoliaUSDC:** Sepolia ETH or USDC deposited through the ckETH minter's Sepolia helper contract. + +## Known gaps + +- No injected provider (see above). No ICRC-21 consent messages yet: nothing needs them until there's a provider. +- Only the default subaccount is used to send and to read balances. You can send *to* any subaccount. +- Certificates aren't verified (see above). +- There's no transaction explorer link: the IC dashboard indexes ledger blocks by index, per ledger. + +## Tests + +`test/icp.test.ts` uses no network. +- Cross-checks: + - the spec's request-id example; + - `@dfinity/agent` `requestIdOf` (call and read_state), `Cbor.decode` (envelopes) and `lookup_path` (certificate + trees); + - `@dfinity/candid` `IDL.encode` (TransferArg, Account, legacy TransferArgs; decoding Ok / Err results); + - `@dfinity/principal` and `@dfinity/identity-secp256k1` (public key → DER → principal); + - the ICRC-1 textual-encoding examples. +- The send path runs against a mocked boundary node: the synchronous reply, the 202 → read_state path, a ledger error, + a bad signature refused before sending, and every refusal. +- Signatures were precomputed offline from the public "abandon … about" vector (`test/signatures.ts`). + +## Sources + +- IC interface spec, HTTPS interface: https://internetcomputer.org/docs/references/ic-interface-spec/https-interface + (call v4, read_state v3, query v3, authentication, request ids, representation-independent hashing) +- Certification (hash trees): https://internetcomputer.org/docs/references/ic-interface-spec/certification +- Candid spec: https://github.com/dfinity/candid/blob/master/spec/Candid.md +- ICRC-1: https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md, TextualEncoding.md +- Ledger interfaces: https://github.com/dfinity/ic/blob/master/rs/ledger_suite/icp/ledger.did, https://github.com/dfinity/ic/blob/master/rs/ledger_suite/icrc1/ledger/ledger.did +- Chain-key canister ids: https://internetcomputer.org/docs/references/chain-key-canister-ids (and dfinity/ic `canister_ids.json`) +- Signer standards and status: https://github.com/dfinity/wg-identity-authentication (README; `topics/icrc_94_multi_injected_provider_discovery.md`, `icrc_49_call_canister.md`, `ICRC-21/`) +- CAIP-2 proposal: https://github.com/icvc/icp-namespace/pull/1 +- TESTICP faucet: https://faucet.internetcomputer.org (developer docs `guides/digital-assets/rosetta.md`) diff --git a/packages/chains-icp/package.json b/packages/chains-icp/package.json new file mode 100644 index 00000000..74a879ea --- /dev/null +++ b/packages/chains-icp/package.json @@ -0,0 +1,83 @@ +{ + "name": "@clip-wallet/chains-icp", + "version": "0.1.0", + "description": "Internet Computer (ICP) ChainModule for Clip Wallet: builds and decodes ledger transfers in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "icp", + "internet-computer", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-icp#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-icp" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "devDependencies": { + "@dfinity/agent": "^2.4.1", + "@dfinity/candid": "^2.4.1", + "@dfinity/identity-secp256k1": "^2.4.1", + "@dfinity/principal": "^2.4.1" + } +} diff --git a/packages/chains-icp/src/agent.ts b/packages/chains-icp/src/agent.ts new file mode 100644 index 00000000..a8844ee0 --- /dev/null +++ b/packages/chains-icp/src/agent.ts @@ -0,0 +1,195 @@ +import { ClipError } from "@clip-wallet/core"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { type CborMap, type CborValue, cborDecode, cborEncode } from "./cbor.js"; +import { uleb } from "./candid.js"; +import { ANONYMOUS, principalToText } from "./principal.js"; +import { bytesEqual, concat, utf8 } from "./util.js"; + +/** + * A minimal client for the IC HTTPS interface (https://internetcomputer.org/docs/references/ic-interface-spec, + * "HTTPS Interface"): request ids, envelopes, anonymous query calls, update calls through the synchronous + * /api/v4/…/call endpoint (falling back to polling /api/v3/…/read_state on 202), and reading the request status + * out of a certificate. + * + * NOT VERIFIED: certificates (BLS signatures over the state tree, with subnet delegations) and query-response node + * signatures are NOT checked here. What a boundary node returns is treated as a report, never as proof: balances + * are display-only, and a transfer result only says what the node claimed. Nothing in this package relies on a reply + * for a security decision (the user approves what the wallet itself encoded, before anything is sent). + */ + +export type Content = Record; + +/** Domain separator for request signatures (spec "Authentication"). */ +export const IC_REQUEST_DOMAIN = concat(Uint8Array.of(0x0a), utf8("ic-request")); + +function hashValue(v: Uint8Array | string | bigint | number | unknown[]): Uint8Array { + if (v instanceof Uint8Array) return sha256(v); + if (typeof v === "string") return sha256(utf8(v)); + if (typeof v === "bigint" || typeof v === "number") return sha256(uleb(v)); + if (Array.isArray(v)) return sha256(concat(...v.map((x) => hashValue(x as never)))); + throw new Error("request id: unsupported value"); +} + +function compare(a: Uint8Array, b: Uint8Array): number { + for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) return a[i]! - b[i]!; + return a.length - b.length; +} + +/** Representation-independent hash of a map (spec "Representation-independent hashing of structured data"). */ +export function requestId(content: Content): Uint8Array { + const parts = Object.entries(content) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => concat(sha256(utf8(k)), hashValue(v as never))) + .sort(compare); + return sha256(concat(...parts)); +} + +/** The 32 bytes an IC request signature covers: SHA-256("\x0Aic-request" ‖ request id). ECDSA secp256k1 signs this digest. */ +export function signDigest(id: Uint8Array): Uint8Array { + return sha256(concat(IC_REQUEST_DOMAIN, id)); +} + +export function envelope(content: Content, signed?: { publicKeyDer: Uint8Array; signature: Uint8Array }): Uint8Array { + const map: CborMap = { content: content as unknown as CborValue }; + if (signed) { + map.sender_pubkey = signed.publicKeyDer; + map.sender_sig = signed.signature; + } + return cborEncode(map); +} + +export function callContent(p: { canisterId: Uint8Array; method: string; arg: Uint8Array; sender: Uint8Array; ingressExpiry: bigint; nonce?: Uint8Array }): Content { + const c: Content = { request_type: "call", canister_id: p.canisterId, method_name: p.method, arg: p.arg, sender: p.sender, ingress_expiry: p.ingressExpiry }; + if (p.nonce) c.nonce = p.nonce; + return c; +} + +export function readStateContent(p: { requestId: Uint8Array; sender: Uint8Array; ingressExpiry: bigint }): Content { + return { request_type: "read_state", paths: [[utf8("request_status"), p.requestId]], sender: p.sender, ingress_expiry: p.ingressExpiry }; +} + +/* ------------------------------------------------------------------ certificates (unverified) */ + +type Tree = CborValue[]; + +/** Looks a path up in a certificate's hash tree: the leaf, null when absent, "unknown" when pruned away. */ +export function lookup(tree: CborValue, path: Uint8Array[]): Uint8Array | null | "unknown" { + const t = tree as Tree; + if (!path.length) { + if (t[0] === 3n && t[1] instanceof Uint8Array) return t[1]; + if (t[0] === 4n) return "unknown"; + return null; + } + const [head, ...rest] = path; + let pruned = false; + const find = (n: Tree): Tree | null => { + switch (n[0]) { + case 1n: + return find(n[1] as Tree) ?? find(n[2] as Tree); + case 2n: + return bytesEqual(n[1] as Uint8Array, head!) ? (n[2] as Tree) : null; + case 4n: + pruned = true; + return null; + default: + return null; + } + }; + const sub = find(t); + if (!sub) return pruned ? "unknown" : null; + return lookup(sub, rest); +} + +export interface RequestStatus { + status: string; + reply?: Uint8Array; + rejectCode?: number; + rejectMessage?: string; + errorCode?: string; +} + +/** The request's status as a (NOT verified) certificate states it, or null if the certificate doesn't say. */ +export function requestStatus(certificate: Uint8Array, id: Uint8Array): RequestStatus | null { + const cert = cborDecode(certificate) as CborMap; + const tree = cert.tree; + if (!tree) return null; + const at = (label: string) => lookup(tree, [utf8("request_status"), id, utf8(label)]); + const status = at("status"); + if (!(status instanceof Uint8Array)) return null; + const out: RequestStatus = { status: new TextDecoder().decode(status) }; + const reply = at("reply"); + if (reply instanceof Uint8Array) out.reply = reply; + const code = at("reject_code"); + if (code instanceof Uint8Array) out.rejectCode = code.reduceRight((n, b) => n * 128 + (b & 0x7f), 0); + const msg = at("reject_message"); + if (msg instanceof Uint8Array) out.rejectMessage = new TextDecoder().decode(msg); + const err = at("error_code"); + if (err instanceof Uint8Array) out.errorCode = new TextDecoder().decode(err); + return out; +} + +/* ------------------------------------------------------------------ HTTP */ + +export class IcRejectError extends Error { + constructor( + message: string, + public readonly rejectCode?: number, + public readonly errorCode?: string, + ) { + super(message); + } +} + +const offline = (cause: unknown) => new ClipError("We couldn't reach the Internet Computer. Check your connection and try again.", "icp/offline", cause); + +export class IcClient { + constructor( + private readonly host: string, + private readonly fetchImpl: typeof fetch, + ) {} + + private async post(path: string, body: Uint8Array): Promise<{ status: number; body: Uint8Array }> { + let r: Response; + try { + r = await this.fetchImpl(`${this.host}${path}`, { method: "POST", headers: { "content-type": "application/cbor" }, body: body as BodyInit }); + } catch (e) { + throw offline(e); + } + return { status: r.status, body: new Uint8Array(await r.arrayBuffer()) }; + } + + /** Anonymous query (sender 0x04): the reply's Candid bytes. */ + async query(canisterId: Uint8Array, method: string, arg: Uint8Array, ingressExpiry: bigint): Promise { + const content: Content = { request_type: "query", canister_id: canisterId, method_name: method, arg, sender: ANONYMOUS, ingress_expiry: ingressExpiry }; + const r = await this.post(`/api/v3/canister/${principalToText(canisterId)}/query`, envelope(content)); + if (r.status !== 200) throw new IcRejectError(`HTTP ${r.status}: ${new TextDecoder().decode(r.body).slice(0, 200)}`); + const m = cborDecode(r.body) as CborMap; + if (m.status === "replied") { + const arg = (m.reply as CborMap | undefined)?.arg; + if (arg instanceof Uint8Array) return arg; + } + throw new IcRejectError(String(m.reject_message ?? "query rejected"), Number(m.reject_code ?? 0), typeof m.error_code === "string" ? m.error_code : undefined); + } + + /** + * Submits a signed call to the synchronous endpoint. Returns the certificate when the call finished in time, null on + * 202 (accepted, still running: poll read_state). A non-replicated rejection or HTTP error throws IcRejectError. + */ + async call(canisterId: Uint8Array, body: Uint8Array): Promise { + const r = await this.post(`/api/v4/canister/${principalToText(canisterId)}/call`, body); + if (r.status === 202) return null; + if (r.status !== 200) throw new IcRejectError(`HTTP ${r.status}: ${new TextDecoder().decode(r.body).slice(0, 300)}`); + const m = cborDecode(r.body) as CborMap; + if (m.status === "replied" && m.certificate instanceof Uint8Array) return m.certificate; + throw new IcRejectError(String(m.reject_message ?? "call rejected"), Number(m.reject_code ?? 0), typeof m.error_code === "string" ? m.error_code : undefined); + } + + /** Signed read_state for a request's status: the certificate. */ + async readState(canisterId: Uint8Array, body: Uint8Array): Promise { + const r = await this.post(`/api/v3/canister/${principalToText(canisterId)}/read_state`, body); + if (r.status !== 200) throw new IcRejectError(`HTTP ${r.status}: ${new TextDecoder().decode(r.body).slice(0, 300)}`); + const m = cborDecode(r.body) as CborMap; + if (!(m.certificate instanceof Uint8Array)) throw new IcRejectError("no certificate"); + return m.certificate; + } +} diff --git a/packages/chains-icp/src/candid.ts b/packages/chains-icp/src/candid.ts new file mode 100644 index 00000000..145258b4 --- /dev/null +++ b/packages/chains-icp/src/candid.ts @@ -0,0 +1,377 @@ +/** + * The Candid subset this module needs (https://github.com/dfinity/candid/blob/master/spec/Candid.md): nat, nat8, + * nat64, int, text, bool, null, principal, opt, vec (blob = vec nat8), record and variant. Encoding builds the type + * table the way @dfinity/candid does (children before parents, structurally equal types once), so the bytes match + * `IDL.encode` (the tests check). Decoding reads any type table and returns values keyed by field hash; `named()` + * maps them back to the names an expected type knows. Unknown fields and cases are kept, never guessed at. + */ + +export type CType = + | { k: "nat" } + | { k: "nat8" } + | { k: "nat64" } + | { k: "int" } + | { k: "text" } + | { k: "bool" } + | { k: "null" } + | { k: "principal" } + | { k: "opt"; t: CType } + | { k: "vec"; t: CType } + | { k: "record"; fields: Record } + | { k: "variant"; fields: Record }; + +export const C = { + nat: { k: "nat" } as CType, + nat8: { k: "nat8" } as CType, + nat64: { k: "nat64" } as CType, + int: { k: "int" } as CType, + text: { k: "text" } as CType, + bool: { k: "bool" } as CType, + null: { k: "null" } as CType, + principal: { k: "principal" } as CType, + opt: (t: CType): CType => ({ k: "opt", t }), + vec: (t: CType): CType => ({ k: "vec", t }), + blob: { k: "vec", t: { k: "nat8" } } as CType, + record: (fields: Record): CType => ({ k: "record", fields }), + variant: (fields: Record): CType => ({ k: "variant", fields }), +}; + +/** Values: nat/int/nat64 bigint, nat8 number, text string, blob/principal Uint8Array, opt [] | [v], record/variant objects. */ +export type CValue = bigint | number | string | boolean | null | Uint8Array | CValue[] | { [k: string]: CValue }; + +/** idl_hash: h = h·223 + byte, mod 2^32, over the UTF-8 field name. */ +export function idlHash(name: string): number { + let h = 0; + for (const b of new TextEncoder().encode(name)) h = (h * 223 + b) >>> 0; + return h; +} + +/** Field id: a name's hash, or a "__" / numeric name's number. */ +function fieldId(name: string): number { + const m = /^_(\d+)_$/.exec(name) ?? /^(\d+)$/.exec(name); + return m ? Number(m[1]) >>> 0 : idlHash(name); +} + +const sortedFields = (fields: Record) => Object.entries(fields).map(([n, t]) => [n, t, fieldId(n)] as const).sort((a, b) => a[2] - b[2]); + +export function uleb(n: bigint | number): Uint8Array { + let v = BigInt(n); + if (v < 0n) throw new Error("uleb: negative"); + const out: number[] = []; + do { + let b = Number(v & 0x7fn); + v >>= 7n; + if (v !== 0n) b |= 0x80; + out.push(b); + } while (v !== 0n); + return Uint8Array.from(out); +} + +export function sleb(n: bigint | number): Uint8Array { + let v = BigInt(n); + const out: number[] = []; + for (;;) { + const b = Number(v & 0x7fn); + v >>= 7n; + const done = (v === 0n && (b & 0x40) === 0) || (v === -1n && (b & 0x40) !== 0); + out.push(done ? b : b | 0x80); + if (done) return Uint8Array.from(out); + } +} + +const OP: Record = { null: -1, bool: -2, nat: -3, int: -4, nat8: -5, nat64: -8, text: -15, principal: -24 }; +const PRIM_BY_OP = new Map(Object.entries(OP).map(([k, v]) => [v, k as CType["k"]])); +// Primitive types this decoder can also skip/read without being asked for them. +const SIZE_BY_OP = new Map([[-6, 2], [-7, 4], [-9, 1], [-10, 2], [-11, 4], [-12, 8], [-13, 4], [-14, 8]]); + +function cat(parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +function typeKey(t: CType): string { + switch (t.k) { + case "opt": + case "vec": + return `${t.k} ${typeKey(t.t)}`; + case "record": + case "variant": + return `${t.k} {${sortedFields(t.fields).map(([, ft, id]) => `${id}:${typeKey(ft)}`).join("; ")}}`; + default: + return t.k; + } +} + +/** Encodes `IDL.encode(types, values)`. */ +export function encode(types: CType[], values: CValue[]): Uint8Array { + if (types.length !== values.length) throw new Error("candid: arity"); + const table: Uint8Array[] = []; + const index = new Map(); + const ref = (t: CType): Uint8Array => { + if (t.k in OP) return sleb(OP[t.k]!); + const key = typeKey(t); + let i = index.get(key); + if (i === undefined) { + let body: Uint8Array; + if (t.k === "opt" || t.k === "vec") { + const inner = ref(t.t); + body = cat([sleb(t.k === "opt" ? -18 : -19), inner]); + } else if (t.k === "record" || t.k === "variant") { + const fs = sortedFields(t.fields).map(([, ft, id]) => cat([uleb(id), ref(ft)])); + body = cat([sleb(t.k === "record" ? -20 : -21), uleb(fs.length), ...fs]); + } else throw new Error(`candid: ${t.k}`); + i = table.length; + index.set(key, i); + table.push(body); + } + return sleb(i); + }; + const argRefs = types.map(ref); + const vals = types.map((t, i) => encValue(t, values[i]!)); + return cat([new TextEncoder().encode("DIDL"), uleb(table.length), ...table, uleb(types.length), ...argRefs, ...vals]); +} + +function encValue(t: CType, v: CValue): Uint8Array { + switch (t.k) { + case "nat": + return uleb(v as bigint); + case "int": + return sleb(v as bigint); + case "nat8": + return Uint8Array.of(Number(v) & 0xff); + case "nat64": { + const out = new Uint8Array(8); + new DataView(out.buffer).setBigUint64(0, BigInt(v as bigint), true); + return out; + } + case "text": { + const b = new TextEncoder().encode(v as string); + return cat([uleb(b.length), b]); + } + case "bool": + return Uint8Array.of(v ? 1 : 0); + case "null": + return new Uint8Array(); + case "principal": { + const b = v as Uint8Array; + return cat([Uint8Array.of(1), uleb(b.length), b]); + } + case "opt": { + const a = v as CValue[]; + return a.length ? cat([Uint8Array.of(1), encValue(t.t, a[0]!)]) : Uint8Array.of(0); + } + case "vec": { + if (t.t.k === "nat8") { + const b = v as Uint8Array; + return cat([uleb(b.length), b]); + } + const a = v as CValue[]; + return cat([uleb(a.length), ...a.map((x) => encValue(t.t, x))]); + } + case "record": { + const o = v as Record; + return cat(sortedFields(t.fields).map(([n, ft]) => encValue(ft, o[n]!))); + } + case "variant": { + const o = v as Record; + const name = Object.keys(o)[0]!; + const fs = sortedFields(t.fields); + const i = fs.findIndex(([n]) => n === name); + if (i < 0) throw new Error(`candid: no case ${name}`); + return cat([uleb(i), encValue(fs[i]![1], o[name]!)]); + } + } +} + +/* ------------------------------------------------------------------ decoding */ + +type Wire = number | { op: number; t?: Wire; fields?: [number, Wire][] }; + +/** A decoded value: records/variants are keyed by field id ("#"), so `named()` can map them. */ +export type Decoded = bigint | number | string | boolean | null | Uint8Array | Decoded[] | { [k: string]: Decoded }; + +class Reader { + i = 0; + constructor(readonly b: Uint8Array) {} + byte(): number { + if (this.i >= this.b.length) throw new Error("candid: truncated"); + return this.b[this.i++]!; + } + bytes(n: number): Uint8Array { + if (n < 0 || this.i + n > this.b.length) throw new Error("candid: truncated"); + const out = this.b.slice(this.i, this.i + n); + this.i += n; + return out; + } + uleb(): bigint { + let n = 0n; + let shift = 0n; + for (;;) { + const b = this.byte(); + n |= BigInt(b & 0x7f) << shift; + if (!(b & 0x80)) return n; + shift += 7n; + if (shift > 700n) throw new Error("candid: number too long"); + } + } + sleb(): bigint { + let n = 0n; + let shift = 0n; + let b: number; + do { + b = this.byte(); + n |= BigInt(b & 0x7f) << shift; + shift += 7n; + if (shift > 700n) throw new Error("candid: number too long"); + } while (b & 0x80); + if (b & 0x40) n -= 1n << shift; + return n; + } + len(): number { + const n = this.uleb(); + if (n > BigInt(this.b.length)) throw new Error("candid: bad length"); + return Number(n); + } +} + +/** Decodes a whole Candid message into its argument values (records/variants keyed "#"). */ +export function decode(bytes: Uint8Array): Decoded[] { + const r = new Reader(bytes); + if (new TextDecoder().decode(r.bytes(4)) !== "DIDL") throw new Error("candid: no magic"); + const n = r.len(); + const raw: { op: number; t?: number; fields?: [number, number][] }[] = []; + for (let k = 0; k < n; k++) { + const op = Number(r.sleb()); + if (op === -18 || op === -19) raw.push({ op, t: Number(r.sleb()) }); + else if (op === -20 || op === -21) { + const count = r.len(); + const fields: [number, number][] = []; + for (let f = 0; f < count; f++) fields.push([Number(r.uleb()), Number(r.sleb())]); + raw.push({ op, fields }); + } else throw new Error(`candid: unsupported type ${op}`); + } + const argc = r.len(); + const args: number[] = []; + for (let k = 0; k < argc; k++) args.push(Number(r.sleb())); + const check = (t: number) => { + if (t >= 0 && t >= raw.length) throw new Error("candid: bad type index"); + }; + raw.forEach((e) => (e.t !== undefined ? check(e.t) : e.fields?.forEach(([, t]) => check(t)))); + args.forEach(check); + + const value = (t: number, depth: number): Decoded => { + if (depth > 100) throw new Error("candid: too deep"); + if (t < 0) { + const k = PRIM_BY_OP.get(t); + switch (k) { + case "null": + return null; + case "bool": + return r.byte() === 1; + case "nat": + return r.uleb(); + case "int": + return r.sleb(); + case "nat8": + return r.byte(); + case "nat64": + return new DataView(r.bytes(8).buffer).getBigUint64(0, true); + case "text": + return new TextDecoder("utf-8", { fatal: true }).decode(r.bytes(r.len())); + case "principal": { + if (r.byte() !== 1) throw new Error("candid: opaque principal"); + return r.bytes(r.len()); + } + } + const size = SIZE_BY_OP.get(t); + if (size !== undefined) { + const b = r.bytes(size); + let v = 0n; + for (let i = b.length - 1; i >= 0; i--) v = (v << 8n) | BigInt(b[i]!); + return v; + } + if (t === -16) return null; // reserved + throw new Error(`candid: unsupported type ${t}`); + } + const e = raw[t]!; + if (e.op === -18) return r.byte() === 1 ? [value(e.t!, depth + 1)] : []; + if (e.op === -19) { + const len = r.len(); + if (e.t === -5) return r.bytes(len); + const out: Decoded[] = []; + for (let k = 0; k < len; k++) out.push(value(e.t!, depth + 1)); + return out; + } + if (e.op === -20) { + const out: Record = {}; + for (const [id, ft] of e.fields!) out[`#${id}`] = value(ft, depth + 1); + return out; + } + const i = r.len(); + const f = e.fields![i]; + if (!f) throw new Error("candid: bad variant index"); + return { [`#${f[0]}`]: value(f[1], depth + 1) }; + }; + const out = args.map((t) => value(t, 0)); + if (r.i !== bytes.length) throw new Error("candid: trailing bytes"); + return out; +} + +/** Maps "#" keys of a decoded value back to the names `t` knows (unknown ones stay "#"). Throws on a kind mismatch. */ +export function named(t: CType, v: Decoded): CValue { + switch (t.k) { + case "nat": + case "int": + case "nat64": + if (typeof v !== "bigint") throw new Error(`candid: expected ${t.k}`); + return v; + case "nat8": + if (typeof v !== "number") throw new Error("candid: expected nat8"); + return v; + case "text": + if (typeof v !== "string") throw new Error("candid: expected text"); + return v; + case "bool": + if (typeof v !== "boolean") throw new Error("candid: expected bool"); + return v; + case "null": + return null; + case "principal": + if (!(v instanceof Uint8Array)) throw new Error("candid: expected principal"); + return v; + case "opt": + if (!Array.isArray(v)) throw new Error("candid: expected opt"); + return v.length ? [named(t.t, v[0]!)] : []; + case "vec": + if (t.t.k === "nat8") { + if (!(v instanceof Uint8Array)) throw new Error("candid: expected blob"); + return v; + } + if (!Array.isArray(v)) throw new Error("candid: expected vec"); + return v.map((x) => named(t.t, x)); + case "record": + case "variant": { + if (!v || typeof v !== "object" || Array.isArray(v) || v instanceof Uint8Array) throw new Error(`candid: expected ${t.k}`); + const byId = new Map(Object.entries(t.fields).map(([n, ft]) => [`#${fieldId(n)}`, [n, ft] as const])); + const out: Record = {}; + for (const [k, x] of Object.entries(v)) { + const f = byId.get(k); + out[f ? f[0] : k] = f ? named(f[1], x) : (x as CValue); + } + if (t.k === "record") { + for (const [n, ft] of Object.entries(t.fields)) { + if (n in out) continue; + if (ft.k === "opt") out[n] = []; + else if (ft.k === "null") out[n] = null; + else throw new Error(`candid: missing ${n}`); + } + } + return out; + } + } +} diff --git a/packages/chains-icp/src/cbor.ts b/packages/chains-icp/src/cbor.ts new file mode 100644 index 00000000..f18f5296 --- /dev/null +++ b/packages/chains-icp/src/cbor.ts @@ -0,0 +1,151 @@ +/** + * The CBOR subset the IC HTTPS interface uses (RFC 8949; IC interface spec "CBOR"): unsigned integers, byte and text + * strings, arrays, maps with text keys, and the self-describing tag 55799 the IC puts in front of every message. + * Requests are encoded with the tag and definite lengths; replies are decoded with or without the tag, and with + * definite or indefinite-length arrays and maps (boundary nodes answer queries with an indefinite map). No floats. + */ +export type CborValue = bigint | number | string | Uint8Array | CborValue[] | CborMap | null | boolean; +export interface CborMap { + [key: string]: CborValue; +} + +const SELF_DESCRIBE = Uint8Array.of(0xd9, 0xd9, 0xf7); + +function head(major: number, n: bigint): Uint8Array { + const m = major << 5; + if (n < 24n) return Uint8Array.of(m | Number(n)); + if (n < 0x100n) return Uint8Array.of(m | 24, Number(n)); + if (n < 0x10000n) return Uint8Array.of(m | 25, Number(n >> 8n), Number(n & 0xffn)); + if (n < 0x100000000n) { + const out = new Uint8Array(5); + out[0] = m | 26; + new DataView(out.buffer).setUint32(1, Number(n)); + return out; + } + const out = new Uint8Array(9); + out[0] = m | 27; + new DataView(out.buffer).setBigUint64(1, n); + return out; +} + +function enc(v: CborValue, parts: Uint8Array[]): void { + if (typeof v === "bigint" || typeof v === "number") { + const n = BigInt(v); + if (n < 0n) throw new Error("cbor: negative numbers aren't used"); + parts.push(head(0, n)); + } else if (v instanceof Uint8Array) { + parts.push(head(2, BigInt(v.length)), v); + } else if (typeof v === "string") { + const b = new TextEncoder().encode(v); + parts.push(head(3, BigInt(b.length)), b); + } else if (Array.isArray(v)) { + parts.push(head(4, BigInt(v.length))); + for (const x of v) enc(x, parts); + } else if (v === null) { + parts.push(Uint8Array.of(0xf6)); + } else if (typeof v === "boolean") { + parts.push(Uint8Array.of(v ? 0xf5 : 0xf4)); + } else { + const keys = Object.keys(v).filter((k) => v[k] !== undefined); + parts.push(head(5, BigInt(keys.length))); + for (const k of keys) { + enc(k, parts); + enc(v[k]!, parts); + } + } +} + +/** Encodes with the self-describing tag 55799 in front, as the IC (and @dfinity/agent) send it. */ +export function cborEncode(v: CborValue): Uint8Array { + const parts: Uint8Array[] = [SELF_DESCRIBE]; + enc(v, parts); + const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function cborDecode(bytes: Uint8Array): CborValue { + let i = 0; + const need = (n: number) => { + if (i + n > bytes.length) throw new Error("cbor: truncated"); + }; + const arg = (info: number): bigint => { + if (info < 24) return BigInt(info); + const len = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : info === 27 ? 8 : -1; + if (len < 0) throw new Error("cbor: unsupported length"); + need(len); + let n = 0n; + for (let k = 0; k < len; k++) n = (n << 8n) | BigInt(bytes[i++]!); + return n; + }; + const item = (depth: number): CborValue => { + if (depth > 64) throw new Error("cbor: too deep"); + need(1); + const b = bytes[i++]!; + const major = b >> 5; + const info = b & 31; + switch (major) { + case 0: + return arg(info); + case 2: + case 3: { + const len = Number(arg(info)); + need(len); + const s = bytes.slice(i, i + len); + i += len; + return major === 2 ? s : new TextDecoder("utf-8", { fatal: true }).decode(s); + } + case 4: { + if (info === 31) { + const out: CborValue[] = []; + while ((need(1), bytes[i] !== 0xff)) out.push(item(depth + 1)); + i++; + return out; + } + const len = Number(arg(info)); + if (len > bytes.length) throw new Error("cbor: bad array"); + const out: CborValue[] = []; + for (let k = 0; k < len; k++) out.push(item(depth + 1)); + return out; + } + case 5: { + if (info === 31) { + const out: CborMap = {}; + while ((need(1), bytes[i] !== 0xff)) { + const key = item(depth + 1); + if (typeof key !== "string") throw new Error("cbor: map keys must be text"); + out[key] = item(depth + 1); + } + i++; + return out; + } + const len = Number(arg(info)); + if (len > bytes.length) throw new Error("cbor: bad map"); + const out: CborMap = {}; + for (let k = 0; k < len; k++) { + const key = item(depth + 1); + if (typeof key !== "string") throw new Error("cbor: map keys must be text"); + out[key] = item(depth + 1); + } + return out; + } + case 6: + arg(info); // tag (55799 self-describe, or others): the tagged item follows + return item(depth + 1); + case 7: + if (info === 20) return false; + if (info === 21) return true; + if (info === 22 || info === 23) return null; + throw new Error("cbor: unsupported simple value"); + default: + throw new Error("cbor: negative integers aren't used"); + } + }; + const v = item(0); + if (i !== bytes.length) throw new Error("cbor: trailing bytes"); + return v; +} diff --git a/packages/chains-icp/src/index.ts b/packages/chains-icp/src/index.ts new file mode 100644 index 00000000..3b42c2a3 --- /dev/null +++ b/packages/chains-icp/src/index.ts @@ -0,0 +1,12 @@ +export { type Content, IC_REQUEST_DOMAIN, IcClient, IcRejectError, type RequestStatus, callContent, envelope, lookup, readStateContent, requestId, requestStatus, signDigest } from "./agent.js"; +export { C, type CType, type CValue, type Decoded, decode as candidDecode, encode as candidEncode, idlHash, named } from "./candid.js"; +export { type CborValue, cborDecode, cborEncode } from "./cbor.js"; +export { Account, LegacyTransferArgs, LegacyTransferResult, METHODS, TransferArg, TransferError, TransferResult, plainTransferError } from "./ledger.js"; +export * from "./module.js"; +export * from "./networks.js"; +export { ANONYMOUS, type IcrcAccount, accountIdFromHex, accountIdentifier, crc32, icrcAccountFromText, icrcAccountToText, principalFromText, principalToText } from "./principal.js"; + +import { createIcpModule } from "./module.js"; + +/** Default instance. */ +export const icpModule = createIcpModule(); diff --git a/packages/chains-icp/src/ledger.ts b/packages/chains-icp/src/ledger.ts new file mode 100644 index 00000000..9356bc43 --- /dev/null +++ b/packages/chains-icp/src/ledger.ts @@ -0,0 +1,94 @@ +import { C, type CType, type CValue } from "./candid.js"; +import { formatUnits } from "./util.js"; + +/** + * Candid types of the ledger methods this module calls. ICRC-1: https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/ICRC-1.did + * (the ICRC-1 ledger's rs/ledger_suite/icrc1/ledger/ledger.did and the ICP ledger's rs/ledger_suite/icp/ledger.did in + * dfinity/ic). ICP's legacy `transfer` (to a 32-byte account identifier): rs/ledger_suite/icp/ledger.did. + */ +export const Account = C.record({ owner: C.principal, subaccount: C.opt(C.blob) }); + +export const TransferArg = C.record({ + from_subaccount: C.opt(C.blob), + to: Account, + amount: C.nat, + fee: C.opt(C.nat), + memo: C.opt(C.blob), + created_at_time: C.opt(C.nat64), +}); + +export const TransferError = C.variant({ + BadFee: C.record({ expected_fee: C.nat }), + BadBurn: C.record({ min_burn_amount: C.nat }), + InsufficientFunds: C.record({ balance: C.nat }), + TooOld: C.null, + CreatedInFuture: C.record({ ledger_time: C.nat64 }), + TemporarilyUnavailable: C.null, + Duplicate: C.record({ duplicate_of: C.nat }), + GenericError: C.record({ error_code: C.nat, message: C.text }), +}); + +export const TransferResult = C.variant({ Ok: C.nat, Err: TransferError }); + +const Tokens = C.record({ e8s: C.nat64 }); + +export const LegacyTransferArgs = C.record({ + memo: C.nat64, + amount: Tokens, + fee: Tokens, + from_subaccount: C.opt(C.blob), + to: C.blob, + created_at_time: C.opt(C.record({ timestamp_nanos: C.nat64 })), +}); + +export const LegacyTransferError = C.variant({ + BadFee: C.record({ expected_fee: Tokens }), + InsufficientFunds: C.record({ balance: Tokens }), + TxTooOld: C.record({ allowed_window_nanos: C.nat64 }), + TxCreatedInFuture: C.null, + TxDuplicate: C.record({ duplicate_of: C.nat64 }), +}); + +export const LegacyTransferResult = C.variant({ Ok: C.nat64, Err: LegacyTransferError }); + +export interface TransferMethod { + method: "icrc1_transfer" | "transfer"; + arg: CType; + result: CType; +} + +export const METHODS: Record<"icrc1_transfer" | "transfer", TransferMethod> = { + icrc1_transfer: { method: "icrc1_transfer", arg: TransferArg, result: TransferResult }, + transfer: { method: "transfer", arg: LegacyTransferArgs, result: LegacyTransferResult }, +}; + +/** A ledger's Err case in plain words. */ +export function plainTransferError(err: Record, symbol: string, decimals: number): string { + const [kind, v] = Object.entries(err)[0] ?? ["", null]; + const rec = (v ?? {}) as Record; + const amount = (x: CValue | undefined) => { + const n = typeof x === "bigint" ? x : x && typeof x === "object" && "e8s" in x ? (x.e8s as bigint) : 0n; + return `${formatUnits(n, decimals)} ${symbol}`; + }; + switch (kind) { + case "InsufficientFunds": + return `You don't have enough ${symbol} for this and its fee. Your balance is ${amount(rec.balance)}. Nothing was sent.`; + case "BadFee": + return `The ${symbol} network fee changed to ${amount(rec.expected_fee)}. Nothing was sent. Try again.`; + case "TooOld": + case "TxTooOld": + return "This transfer waited too long and expired. Nothing was sent. Try again."; + case "CreatedInFuture": + case "TxCreatedInFuture": + return "Your device's clock is ahead of the network's. Nothing was sent. Check your clock and try again."; + case "Duplicate": + case "TxDuplicate": + return "This exact transfer was already sent, so it wasn't sent again."; + case "TemporarilyUnavailable": + return "The ledger is busy right now. Nothing was sent. Try again in a minute."; + case "BadBurn": + return `That amount is too small to send to the token's minting account. Nothing was sent.`; + default: + return `The ${symbol} ledger refused this transfer. Nothing was sent.`; + } +} diff --git a/packages/chains-icp/src/module.ts b/packages/chains-icp/src/module.ts new file mode 100644 index 00000000..d5670892 --- /dev/null +++ b/packages/chains-icp/src/module.ts @@ -0,0 +1,415 @@ +import { + type AssetRef, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + type Warning, + WALLET_ORIGIN, + isWalletOrigin, + msg, + titled, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha224 } from "@noble/hashes/sha2.js"; +import { type Content, IcClient, IcRejectError, callContent, envelope, readStateContent, requestId, requestStatus, signDigest } from "./agent.js"; +import { type CValue, decode as candidDecode, encode as candidEncode, named } from "./candid.js"; +import { cborEncode } from "./cbor.js"; +import { Account, METHODS, plainTransferError } from "./ledger.js"; +import { type LedgerSpec, assetOfLedger, ledgerOf, ledgersFor, netOf } from "./networks.js"; +import { + type IcrcAccount, + accountIdFromHex, + accountIdentifier, + icrcAccountFromText, + icrcAccountToText, + isSelfAuthenticating, + principalFromText, + principalToText, +} from "./principal.js"; +import { b64decode, b64encode, bytesEqual, concat, formatUnits, hex, hostOf, randomBytes, randomId, short, sleep } from "./util.js"; + +/** + * Request method. The wallet's own transfers use the ICRC-49 `icrc49_call_canister` params ({ canisterId, sender, + * method, arg (base64 Candid), nonce? }) plus `ingressExpiry` (nanoseconds, decimal string), so the content the user + * approves is fixed before signing. Only the wallet may send it: there's no injected ICP provider (see README). + * https://github.com/dfinity/wg-identity-authentication/blob/main/topics/icrc_49_call_canister.md + */ +export const ICP_METHODS = { callCanister: "icrc49_call_canister" } as const; + +/** How long a wallet-built call stays valid (the IC refuses ingress_expiry more than 5 minutes ahead). */ +export const INGRESS_WINDOW_MS = 4 * 60 * 1000; + +/** DER SubjectPublicKeyInfo prefix of an uncompressed secp256k1 key (id-ecPublicKey, secp256k1), as @dfinity/identity-secp256k1. */ +const DER_PREFIX = Uint8Array.from([0x30, 0x56, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, 0x03, 0x42, 0x00]); + +export function derPublicKey(publicKey: Uint8Array): Uint8Array { + if (publicKey.length !== 33 && publicKey.length !== 65) throw new Error("ICP needs a secp256k1 public key"); + return concat(DER_PREFIX, secp256k1.Point.fromBytes(publicKey).toBytes(false)); +} + +/** Self-authenticating principal: SHA-224(DER key) ‖ 0x02. */ +export function principalOfKey(publicKey: Uint8Array): Uint8Array { + return concat(sha224(derPublicKey(publicKey)), Uint8Array.of(0x02)); +} + +/** The ICP ledger account identifier (hex) of a principal's default subaccount: what exchanges ask for. */ +export function accountIdOf(principalText: string): string { + const p = principalFromText(principalText); + if (!p) throw new ClipError("That address isn't valid. Check it and try again.", "icp/bad-address"); + return hex(accountIdentifier(p)); +} + +export type Recipient = { kind: "account"; account: IcrcAccount; text: string } | { kind: "accountId"; id: Uint8Array; text: string }; + +/** A principal, an ICRC-1 account text ("principal-checksum.subaccount") or a 64-hex ICP account identifier. */ +export function parseRecipient(value: string): Recipient | null { + const t = value.trim(); + const id = accountIdFromHex(t); + if (id) return { kind: "accountId", id, text: t.toLowerCase() }; + const account = icrcAccountFromText(t); + return account ? { kind: "account", account, text: icrcAccountToText(account) } : null; +} + +export interface IcpModuleOptions { + /** read_state polls after a 202 (default 10) and the wait between them (default 1000 ms). */ + pollAttempts?: number; + pollIntervalMs?: number; + sleep?: (ms: number) => Promise; + /** Clock (ms) for ingress expiry and created_at_time. */ + now?: () => number; + /** Nonce source (16 bytes per call). */ + random?: (n: number) => Uint8Array; +} + +export interface Normalized { + ledger: LedgerSpec; + method: "icrc1_transfer" | "transfer"; + arg: Record; + canisterId: Uint8Array; + sender: Uint8Array; + call: Content; + callId: Uint8Array; + readState: Content; + readStateId: Uint8Array; + ingressExpiry: bigint; +} + +function bad(): ClipError { + return new ClipError("This request from the app is malformed, so we stopped it.", "icp/bad-params"); +} + +/** Checks a wallet-built ledger call against the account and network and parses it. */ +export function normalize(request: DappRequest, ctx: ChainContext): Normalized { + if (!netOf(request.networkId) || request.networkId !== ctx.network.id) { + throw new ClipError("This is for a different network, so Clip Wallet stopped it. Nothing was signed.", "icp/network-mismatch"); + } + if (request.method !== ICP_METHODS.callCanister || !isWalletOrigin(request.origin)) { + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "icp/unsupported-method"); + } + const p = (request.params ?? {}) as Record; + if (typeof p.sender !== "string" || p.sender !== ctx.account.address) { + throw new ClipError("This request is for a different account than the one you're using.", "icp/wrong-account"); + } + const ledger = typeof p.canisterId === "string" ? ledgerOf(ctx.network.id, p.canisterId) : null; + if (!ledger) throw new ClipError("That token couldn't be found.", "icp/unknown-ledger"); + if (p.method !== "icrc1_transfer" && !(p.method === "transfer" && ledger.accountIds)) throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "icp/unsupported-method"); + if (typeof p.arg !== "string" || typeof p.ingressExpiry !== "string" || !/^\d{1,20}$/.test(p.ingressExpiry)) throw bad(); + const method = p.method; + let argBytes: Uint8Array; + let arg: Record; + let nonce: Uint8Array | undefined; + try { + argBytes = b64decode(p.arg); + const values = candidDecode(argBytes); + if (values.length !== 1) throw new Error("arity"); + arg = named(METHODS[method].arg, values[0]!) as Record; + // Byte-for-byte canonical: re-encoding must give the same bytes, so nothing hides in extra fields. + if (!bytesEqual(candidEncode([METHODS[method].arg], [arg]), argBytes)) throw new Error("not canonical"); + if (p.nonce !== undefined) { + nonce = b64decode(String(p.nonce)); + if (nonce.length > 32) throw new Error("nonce"); + } + } catch (cause) { + throw new ClipError("This transaction can't be read.", "icp/bad-transaction", cause); + } + if ((arg.from_subaccount as CValue[]).length) throw new ClipError("This transaction can't be read.", "icp/bad-transaction"); + const canisterId = principalFromText(ledger.canisterId)!; + const sender = principalFromText(ctx.account.address); + if (!sender) throw new ClipError("That address isn't valid. Check it and try again.", "icp/bad-address"); + const ingressExpiry = BigInt(p.ingressExpiry); + const call = callContent({ canisterId, method, arg: argBytes, sender, ingressExpiry, ...(nonce ? { nonce } : {}) }); + const callId = requestId(call); + const readState = readStateContent({ requestId: callId, sender, ingressExpiry }); + return { ledger, method, arg, canisterId, sender, call, callId, readState, readStateId: requestId(readState), ingressExpiry }; +} + +interface Transfer { + to: string; + toAccountId: boolean; + amount: bigint; + fee: bigint; + memo: string | null; +} + +function transferOf(n: Normalized): Transfer { + if (n.method === "transfer") { + const a = n.arg as { to: Uint8Array; amount: { e8s: bigint }; fee: { e8s: bigint }; memo: bigint }; + return { to: hex(a.to), toAccountId: true, amount: a.amount.e8s, fee: a.fee.e8s, memo: a.memo ? a.memo.toString() : null }; + } + const a = n.arg as { to: { owner: Uint8Array; subaccount: Uint8Array[] }; amount: bigint; fee: bigint[]; memo: Uint8Array[] }; + return { + to: icrcAccountToText({ owner: a.to.owner, ...(a.to.subaccount[0] ? { subaccount: a.to.subaccount[0] } : {}) }), + toAccountId: false, + amount: a.amount, + fee: a.fee[0] ?? -1n, + memo: a.memo[0] ? hex(a.memo[0]) : null, + }; +} + +export interface IcpModule extends ChainModule { + normalize: typeof normalize; +} + +export function createIcpModule(options: IcpModuleOptions = {}): IcpModule { + const now = options.now ?? (() => Date.now()); + const random = options.random ?? randomBytes; + const wait = options.sleep ?? sleep; + const attempts = options.pollAttempts ?? 10; + const pollMs = options.pollIntervalMs ?? 1000; + + function clientFor(ctx: ChainContext): IcClient { + const host = ctx.network.rpcUrls[0]; + if (!host) throw new ClipError("No connection is set up for this network.", "icp/no-rpc"); + return new IcClient(host.replace(/\/$/, ""), ctx.fetch); + } + + const queryExpiry = () => BigInt(Math.floor((now() + 3 * 60 * 1000) / 60_000) * 60_000) * 1_000_000n; + + async function query(ctx: ChainContext, ledger: LedgerSpec, method: string, arg: Uint8Array): Promise { + let bytes: Uint8Array; + try { + bytes = await clientFor(ctx).query(principalFromText(ledger.canisterId)!, method, arg, queryExpiry()); + } catch (e) { + if (e instanceof ClipError) throw e; + throw new ClipError("We couldn't reach the Internet Computer. Check your connection and try again.", "icp/offline", e); + } + const v = candidDecode(bytes); + return v[0] as CValue; + } + + async function balanceOf(ctx: ChainContext, ledger: LedgerSpec, owner: Uint8Array): Promise { + const v = await query(ctx, ledger, "icrc1_balance_of", candidEncode([Account], [{ owner, subaccount: [] }])); + if (typeof v !== "bigint") throw new ClipError("We couldn't read your balance right now. Try again.", "icp/bad-reply"); + return v; + } + + async function feeOf(ctx: ChainContext, ledger: LedgerSpec): Promise { + const v = await query(ctx, ledger, "icrc1_fee", candidEncode([], [])); + if (typeof v !== "bigint") throw new ClipError("We couldn't read the network fee right now. Try again.", "icp/bad-reply"); + return v; + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const n = normalize(req, ctx); + if (n.ingressExpiry <= BigInt(now()) * 1_000_000n) throw new ClipError("This request expired. Try again.", "icp/expired"); + const t = transferOf(n); + if (t.fee < 0n) throw new ClipError("This transaction can't be read.", "icp/bad-transaction"); + const asset = assetOfLedger(ctx.network.id, n.ledger); + const amount = `${formatUnits(t.amount, asset.decimals)} ${asset.symbol}`; + const lines = [{ label: "To", value: t.to }]; + if (t.memo) lines.push({ label: "Memo", value: t.memo }); + lines.push({ label: "Network fee", value: `${formatUnits(t.fee, asset.decimals)} ${asset.symbol}` }); + if (!isWalletOrigin(req.origin)) lines.push({ label: "Requested by", value: hostOf(req.origin) }); + const warnings: Warning[] = []; + if (t.toAccountId) { + const w = msg("bg.icp.toAccountId"); + warnings.push({ level: "info", code: "new-recipient", message: w.fallback, msg: w }); + } + return { + requestId: req.id, + networkId: req.networkId, + ...titled(msg("bg.req.sendTo", { amount, to: short(t.to) })), + lines, + balanceChanges: [{ asset, delta: (-(t.amount + t.fee)).toString() }], + fee: { asset, amount: t.fee.toString() }, + simulated: false, + blind: false, + warnings, + }; + } + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + const n = normalize(req, ctx); + // Two signatures: the call itself, and one read_state request for its status (only the call's sender may read + // it; it can be re-sent until it expires), so polling needs no further approval. + return [n.callId, n.readStateId].map((id) => ({ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes: signDigest(id), approvalId })); + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const pub = Uint8Array.from(ctx.account.publicKey.match(/../g)?.map((x) => parseInt(x, 16)) ?? []); + let der: Uint8Array; + try { + der = derPublicKey(pub); + } catch { + throw new ClipError("The signature didn't match. Nothing was sent.", "icp/bad-signature"); + } + if (!bytesEqual(principalOfKey(pub), n.sender)) throw new ClipError("This request is for a different account than the one you're using.", "icp/wrong-account"); + const digests = [signDigest(n.callId), signDigest(n.readStateId)]; + if (signatures.length !== 2) throw new ClipError("Some signatures are missing. Nothing was sent.", "icp/bad-signature"); + digests.forEach((d, i) => { + const s = signatures[i]!; + if (s.scheme !== "ecdsa-secp256k1" || s.bytes.length !== 64 || !secp256k1.verify(s.bytes, d, pub, { prehash: false })) { + throw new ClipError("The signature didn't match. Nothing was sent.", "icp/bad-signature"); + } + }); + if (n.ingressExpiry <= BigInt(now()) * 1_000_000n) throw new ClipError(msg("bg.icp.expired"), "icp/expired"); + + const client = clientFor(ctx); + const callBody = envelope(n.call, { publicKeyDer: der, signature: signatures[0]!.bytes }); + const readBody = envelope(n.readState, { publicKeyDer: der, signature: signatures[1]!.bytes }); + let certificate: Uint8Array | null; + try { + certificate = await client.call(n.canisterId, callBody); + } catch (e) { + if (e instanceof ClipError) throw e; + throw new ClipError(plainRejection(e), "icp/send-failed", e); + } + let status = certificate ? requestStatus(certificate, n.callId) : null; + for (let k = 0; k < attempts && !isFinal(status); k++) { + if (pollMs > 0) await wait(pollMs); + try { + certificate = await client.readState(n.canisterId, readBody); + status = requestStatus(certificate, n.callId); + } catch { + /* keep polling */ + } + } + const base = { requestId: hex(n.callId), contentMap: b64encode(cborEncode(n.call as never)), ...(certificate ? { certificate: b64encode(certificate) } : {}) }; + if (!status || !isFinal(status)) return { ...base, status: "pending" }; + if (status.status !== "replied" || !status.reply) { + throw new ClipError("The Internet Computer rejected this transfer. Nothing was sent.", "icp/rejected", status.rejectMessage); + } + // The reply is read from an UNVERIFIED certificate: it reports the outcome; nothing is decided on it. + let result: Record; + try { + result = named(METHODS[n.method].result, candidDecode(status.reply)[0]!) as Record; + } catch { + return { ...base, status: "replied" }; + } + if ("Err" in result) { + const asset = assetOfLedger(ctx.network.id, n.ledger); + throw new ClipError(plainTransferError(result.Err as Record, asset.symbol, asset.decimals), "icp/transfer-failed", result.Err); + } + return { ...base, status: "replied", blockIndex: String(result.Ok) }; + } + + async function getBalances(ctx: ChainContext): Promise { + const owner = principalFromText(ctx.account.address); + if (!owner) throw new ClipError("That address isn't valid. Check it and try again.", "icp/bad-address"); + const ledgers = ledgersFor(ctx.network.id); + const results = await Promise.all(ledgers.map((l, i) => balanceOf(ctx, l, owner).catch((e) => (i === 0 ? Promise.reject(e) : null)))); + const out: TokenBalance[] = []; + ledgers.forEach((l, i) => { + const v = results[i]; + if (v === null || v === undefined) return; + if (i > 0 && v === 0n) return; // only tokens the account holds + out.push({ asset: assetOfLedger(ctx.network.id, l), amount: v.toString() }); + }); + return out; + } + + async function getNfts(): Promise { + // ICP NFTs (ICRC-7, EXT, DIP-721) have no shared index to list them from yet. + return []; + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + const me = principalFromText(ctx.account.address); + if (!me) throw new ClipError("That address isn't valid. Check it and try again.", "icp/bad-address"); + const ledger = ledgerOf(ctx.network.id, p.asset); + if (!ledger) throw new ClipError("That token couldn't be found.", "icp/unknown-ledger"); + const to = parseRecipient(p.to); + if (!to) throw new ClipError("That address isn't valid. Check it and try again.", "icp/bad-address"); + if (to.kind === "accountId" && !ledger.accountIds) { + throw new ClipError(`${ledger.symbol} can only be sent to a principal ID, not an account ID. Ask for their principal ID.`, "icp/account-id-unsupported"); + } + const self = to.kind === "accountId" ? bytesEqual(to.id, accountIdentifier(me)) : bytesEqual(to.account.owner, me) && !to.account.subaccount; + if (self) throw new ClipError("That's your own address.", "icp/self-transfer"); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "icp/bad-amount"); + const amount = BigInt(p.amount); + const [fee, balance] = await Promise.all([feeOf(ctx, ledger), balanceOf(ctx, ledger, me)]); + if (balance < amount + fee) throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: ledger.symbol }), "icp/insufficient-funds"); + const t = now(); + const createdAt = BigInt(t) * 1_000_000n; + let method: "icrc1_transfer" | "transfer"; + let arg: Uint8Array; + if (to.kind === "accountId") { + method = "transfer"; + arg = candidEncode([METHODS.transfer.arg], [{ memo: 0n, amount: { e8s: amount }, fee: { e8s: fee }, from_subaccount: [], to: to.id, created_at_time: [{ timestamp_nanos: createdAt }] }]); + } else { + method = "icrc1_transfer"; + arg = candidEncode([METHODS.icrc1_transfer.arg], [ + { from_subaccount: [], to: { owner: to.account.owner, subaccount: to.account.subaccount ? [to.account.subaccount] : [] }, amount, fee: [fee], memo: [], created_at_time: [createdAt] }, + ]); + } + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "icp", + networkId: ctx.network.id, + method: ICP_METHODS.callCanister, + params: { + canisterId: ledger.canisterId, + sender: ctx.account.address, + method, + arg: b64encode(arg), + nonce: b64encode(random(16)), + ingressExpiry: (BigInt(t + INGRESS_WINDOW_MS) * 1_000_000n).toString(), + }, + }; + } + + return { + family: "icp", + curve: "secp256k1", + /** Plug, `dfx identity import`, @dfinity/identity-secp256k1 fromSeedPhrase: BIP-44 coin 223. */ + derivationPath: (index: number) => `m/44'/223'/0'/0/${index}`, + addressFromPublicKey(publicKey: Uint8Array, _network: Network): string { + return principalToText(principalOfKey(publicKey)); + }, + /** A principal (with its checksum), an ICRC-1 account text, or a 64-hex ICP account identifier (checksum checked). */ + isAddress: (value: string) => parseRecipient(value) !== null, + networksForAddress: (value: string, candidates: Network[]) => (parseRecipient(value) ? candidates.filter((c) => c.family === "icp") : []), + getBalances, + getNfts, + decode, + prepare, + finalize, + buildTransfer, + normalize, + }; +} + +function isFinal(s: { status: string } | null): boolean { + return !!s && (s.status === "replied" || s.status === "rejected" || s.status === "done"); +} + +function plainRejection(e: unknown): string { + const m = e instanceof Error ? e.message.toLowerCase() : ""; + if (/expir/.test(m)) return "This transfer expired before it reached the network. Nothing was sent. Try again."; + if (/signature|invalid.*(sig|key)/.test(m)) return "The network didn't accept the signature. Nothing was sent."; + if (/429|rate/.test(m)) return "The Internet Computer is busy right now. Nothing was sent. Try again in a minute."; + if (e instanceof IcRejectError && e.rejectCode) return "The Internet Computer rejected this transfer. Nothing was sent."; + return "The Internet Computer didn't accept this transfer. Nothing was sent."; +} + +export { isSelfAuthenticating }; diff --git a/packages/chains-icp/src/networks.ts b/packages/chains-icp/src/networks.ts new file mode 100644 index 00000000..bbf13e0b --- /dev/null +++ b/packages/chains-icp/src/networks.ts @@ -0,0 +1,105 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * The Internet Computer has one public network (mainnet); there's no public test network for ledger transfers. + * DFINITY runs TEST ledgers on mainnet instead (TESTICP with a free faucet, ckTESTBTC on Bitcoin testnet4, + * ckSepoliaETH / ckSepoliaUSDC on Sepolia), so Clip Wallet's "test" ICP network is mainnet restricted to those + * test ledgers: nothing of value can move on it. + * + * Ids: there's no `icp` namespace in ChainAgnostic/namespaces yet. The open proposal (icvc/icp-namespace, listed + * ON HOLD by the identity working group) uses "icp:" + the first 32 hex characters of SHA-256 of the IC root key + * (DER, from https://icp-api.io/api/v2/status), which is `icp:737ba355e855bd4b61279056603e0550`. The test network + * is Clip Wallet's own `icp:test` (same chain, test ledgers only); it never leaves the wallet. + * + * Boundary nodes (no key): https://icp-api.io (also https://ic0.app); both answered GET /api/v2/status on + * 2026-10-06 with the root key. + */ +export type IcpNet = "test" | "mainnet"; + +export interface LedgerSpec { + /** Ledger canister id (principal text). */ + canisterId: string; + symbol: string; + name: string; + decimals: number; + key: string; + /** Chain-key twin of an asset native elsewhere (ckBTC, ckETH, ckUSDC). */ + bridged?: boolean; + /** ICP-ledger API: also takes 32-byte account identifiers through the legacy `transfer` method. */ + accountIds?: boolean; +} + +/** + * Canister ids from dfinity/ic (rs/nns/canister_ids.json, rs/bitcoin/ckbtc/{mainnet,testnet}/canister_ids.json, + * rs/ethereum/cketh/{mainnet,testnet}/canister_ids.json) and the developer docs "Chain-key token canister IDs"; + * symbol, decimals and fee checked with anonymous icrc1_symbol / icrc1_decimals / icrc1_fee queries. + */ +export const LEDGERS: Record = { + mainnet: [ + { canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", symbol: "ICP", name: "Internet Computer", decimals: 8, key: "icp", accountIds: true }, + { canisterId: "mxzaz-hqaaa-aaaar-qaada-cai", symbol: "ckBTC", name: "ckBTC", decimals: 8, key: "ckbtc", bridged: true }, + { canisterId: "xevnm-gaaaa-aaaar-qafnq-cai", symbol: "ckUSDC", name: "ckUSDC", decimals: 6, key: "ckusdc", bridged: true }, + { canisterId: "ss2fx-dyaaa-aaaar-qacoq-cai", symbol: "ckETH", name: "ckETH", decimals: 18, key: "cketh", bridged: true }, + ], + test: [ + // TESTICP: faucet https://faucet.internetcomputer.org (no login). + { canisterId: "xafvr-biaaa-aaaai-aql5q-cai", symbol: "TESTICP", name: "Test ICP", decimals: 8, key: "icp", accountIds: true }, + { canisterId: "mc6ru-gyaaa-aaaar-qaaaq-cai", symbol: "ckTESTBTC", name: "ckTESTBTC", decimals: 8, key: "ckbtc", bridged: true }, + { canisterId: "yfumr-cyaaa-aaaar-qaela-cai", symbol: "ckSepoliaUSDC", name: "ckSepoliaUSDC", decimals: 6, key: "ckusdc", bridged: true }, + { canisterId: "apia6-jaaaa-aaaar-qabma-cai", symbol: "ckSepoliaETH", name: "ckSepoliaETH", decimals: 18, key: "cketh", bridged: true }, + ], +}; + +export const ICP_IDS: Record = { + mainnet: "icp:737ba355e855bd4b61279056603e0550", + test: "icp:test", +}; + +export const ICP_BOUNDARY = "https://icp-api.io"; + +export function ledgerAsset(networkId: NetworkId, l: LedgerSpec, native: boolean): AssetRef { + const a: AssetRef = { key: l.key, symbol: l.symbol, name: l.name, decimals: l.decimals, networkId }; + if (!native) a.address = l.canisterId; + if (l.bridged) a.bridged = true; + return a; +} + +function network(net: IcpNet): Network { + const id = ICP_IDS[net]; + return { + id, + family: "icp", + name: net === "mainnet" ? "Internet Computer" : "Internet Computer (test tokens)", + nativeAsset: ledgerAsset(id, LEDGERS[net][0]!, true), + testnet: net !== "mainnet", + rpcUrls: [ICP_BOUNDARY], + explorerUrl: "https://dashboard.internetcomputer.org", + }; +} + +export const ICP_TEST = network("test"); +export const ICP_MAINNET = network("mainnet"); +export const ICP_NETWORKS: Network[] = [ICP_TEST, ICP_MAINNET]; + +export function netOf(networkId: NetworkId): IcpNet | null { + if (networkId === ICP_IDS.mainnet) return "mainnet"; + if (networkId === ICP_IDS.test) return "test"; + return null; +} + +export function ledgersFor(networkId: NetworkId): LedgerSpec[] { + const n = netOf(networkId); + return n ? LEDGERS[n] : []; +} + +/** The ledger an asset lives on (native asset → the network's first ledger), or null. */ +export function ledgerOf(networkId: NetworkId, asset: Pick | string | undefined): LedgerSpec | null { + const list = ledgersFor(networkId); + const id = typeof asset === "string" ? asset : asset?.address; + if (!id) return list[0] ?? null; + return list.find((l) => l.canisterId === id) ?? null; +} + +export function assetOfLedger(networkId: NetworkId, l: LedgerSpec): AssetRef { + return ledgerAsset(networkId, l, ledgersFor(networkId)[0] === l); +} diff --git a/packages/chains-icp/src/principal.ts b/packages/chains-icp/src/principal.ts new file mode 100644 index 00000000..33070e7b --- /dev/null +++ b/packages/chains-icp/src/principal.ts @@ -0,0 +1,100 @@ +import { sha224 } from "@noble/hashes/sha2.js"; +import { base32nopad } from "@scure/base"; +import { concat, fromHex, hex, utf8 } from "./util.js"; + +/** + * Principals, ledger account identifiers and ICRC-1 account text (IC interface spec "Principals" / "Textual + * representation of principals"; ICP ledger "Account identifiers"; ICRC-1 "Textual encoding of accounts": + * https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/TextualEncoding.md). + */ + +/** CRC-32 (IEEE, as zlib). */ +export function crc32(data: Uint8Array): number { + let c = 0xffffffff; + for (const b of data) { + c ^= b; + for (let k = 0; k < 8; k++) c = c & 1 ? (c >>> 1) ^ 0xedb88320 : c >>> 1; + } + return (c ^ 0xffffffff) >>> 0; +} + +const be32 = (n: number) => Uint8Array.of((n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff); + +/** Principal text: base32(CRC-32 ‖ bytes), lower case, groups of 5 joined by "-". */ +export function principalToText(bytes: Uint8Array): string { + const s = base32nopad.encode(concat(be32(crc32(bytes)), bytes)).toLowerCase(); + return s.match(/.{1,5}/g)!.join("-"); +} + +/** Principal bytes from text (checksum and grouping checked), or null. */ +export function principalFromText(text: string): Uint8Array | null { + const t = text.trim(); + if (!/^[a-z2-7]{1,5}(-[a-z2-7]{1,5})*$/.test(t)) return null; + let raw: Uint8Array; + try { + raw = base32nopad.decode(t.replace(/-/g, "").toUpperCase()); + } catch { + return null; + } + if (raw.length < 4 || raw.length > 33) return null; + const bytes = raw.slice(4); + if (principalToText(bytes) !== t) return null; + return bytes; +} + +/** The anonymous principal (0x04). */ +export const ANONYMOUS = Uint8Array.of(4); + +/** Self-authenticating principals end with 0x02 (29 bytes): what a key-backed account is. */ +export function isSelfAuthenticating(bytes: Uint8Array): boolean { + return bytes.length === 29 && bytes[28] === 0x02; +} + +/** ICP ledger account identifier: CRC-32 ‖ SHA-224("\x0Aaccount-id" ‖ principal ‖ subaccount). */ +export function accountIdentifier(principal: Uint8Array, subaccount: Uint8Array = new Uint8Array(32)): Uint8Array { + if (subaccount.length !== 32) throw new Error("subaccount must be 32 bytes"); + const h = sha224(concat(Uint8Array.of(0x0a), utf8("account-id"), principal, subaccount)); + return concat(be32(crc32(h)), h); +} + +/** A 64-hex account identifier with a valid checksum, as bytes, or null. */ +export function accountIdFromHex(text: string): Uint8Array | null { + const t = text.trim().toLowerCase(); + if (!/^[0-9a-f]{64}$/.test(t)) return null; + const b = fromHex(t); + const crc = (b[0]! << 24) | (b[1]! << 16) | (b[2]! << 8) | b[3]!; + return crc >>> 0 === crc32(b.slice(4)) ? b : null; +} + +export interface IcrcAccount { + owner: Uint8Array; + /** 32 bytes, or undefined for the default (all-zero) subaccount. */ + subaccount?: Uint8Array; +} + +/** ICRC-1 account text: "" or "-.". */ +export function icrcAccountToText(a: IcrcAccount): string { + const owner = principalToText(a.owner); + if (!a.subaccount || a.subaccount.every((b) => b === 0)) return owner; + const check = base32nopad.encode(be32(crc32(concat(a.owner, a.subaccount)))).toLowerCase(); + return `${owner}-${check}.${hex(a.subaccount).replace(/^0+/, "")}`; +} + +export function icrcAccountFromText(text: string): IcrcAccount | null { + const t = text.trim(); + const dot = t.lastIndexOf("."); + if (dot < 0) { + const owner = principalFromText(t); + return owner ? { owner } : null; + } + const head = t.slice(0, dot); + const subHex = t.slice(dot + 1); + if (!/^[0-9a-f]{1,64}$/.test(subHex) || subHex.startsWith("0")) return null; + const dash = head.lastIndexOf("-"); + if (dash < 0) return null; + const owner = principalFromText(head.slice(0, dash)); + if (!owner) return null; + const subaccount = fromHex(subHex.padStart(64, "0")); + const a = { owner, subaccount }; + return icrcAccountToText(a) === t ? a : null; +} diff --git a/packages/chains-icp/src/util.ts b/packages/chains-icp/src/util.ts new file mode 100644 index 00000000..fd5ffd08 --- /dev/null +++ b/packages/chains-icp/src/util.ts @@ -0,0 +1,80 @@ +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.replace(/^0x/i, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export function b64encode(bytes: Uint8Array): string { + let s = ""; + for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + return btoa(s); +} + +export function b64decode(text: string): Uint8Array { + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(text)) throw new Error("bad base64"); + const s = atob(text); + const out = new Uint8Array(s.length); + for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); + return out; +} + +export const utf8 = (s: string) => new TextEncoder().encode(s); + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +/** "tgzar-4lpln-…-aqe" → "tgzar…aqe"; 64-hex account ids → "1c7a48ba…dc79". */ +export function short(text: string): string { + if (text.length <= 16) return text; + return /^[0-9a-f]{64}$/.test(text) ? `${text.slice(0, 8)}…${text.slice(-4)}` : `${text.slice(0, 5)}…${text.slice(-3)}`; +} + +export function randomBytes(n: number): Uint8Array { + const b = new Uint8Array(n); + crypto.getRandomValues(b); + return b; +} + +export const randomId = () => hex(randomBytes(12)); + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/packages/chains-icp/test/helpers.ts b/packages/chains-icp/test/helpers.ts new file mode 100644 index 00000000..abbc6b93 --- /dev/null +++ b/packages/chains-icp/test/helpers.ts @@ -0,0 +1,75 @@ +import type { Account, ChainContext, Network, SignablePayload, Signature } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha224 } from "@noble/hashes/sha2.js"; +import { C, type CType, type CValue, ICP_TEST, cborDecode, cborEncode, candidDecode, candidEncode, createIcpModule, principalToText } from "../src/index.js"; +import type { CborMap, CborValue } from "../src/cbor.js"; +import { concat, fromHex, hex, utf8 } from "../src/util.js"; +import { FIX } from "./signatures.js"; + +/** Vault stand-in: answers with a precomputed signature that verifies for the payload (verification only). */ +export function fixtureSigner(publicKeyHex: string, signatures: readonly string[]) { + const pub = fromHex(publicKeyHex); + const sigs = signatures.map(fromHex); + return { + sign(p: SignablePayload): Signature { + const sig = sigs.find((s) => secp256k1.verify(s, p.bytes, pub, { prehash: false })); + if (!sig) throw new Error("no fixture signature for this payload"); + return { scheme: "ecdsa-secp256k1", bytes: sig, recovery: 0, publicKey: hex(pub) }; + }, + }; +} + +export const signer = fixtureSigner(FIX.publicKey, [...FIX.toPrincipalSigs, ...FIX.toAccountIdSigs]); + +export function makeAccount(): Account { + return { id: "icp:0", family: "icp", index: 0, curve: "secp256k1", derivationPath: "m/44'/223'/0'/0/0", publicKey: FIX.publicKey, address: FIX.me }; +} + +/** bob: a self-authenticating-looking principal nobody holds a key for (SHA-224("bob") ‖ 0x02). */ +export const BOB = principalToText(concat(sha224(utf8("bob")), Uint8Array.of(2))); + +/** Fixed clock and nonce, so wallet-built requests (and their fixture signatures) are reproducible. */ +export const NOW = 1_790_000_000_000; +export const module = createIcpModule({ now: () => NOW, random: (n) => new Uint8Array(n).fill(7), pollIntervalMs: 0, pollAttempts: 3 }); + +export type Handler = (body: CborMap, url: string) => { status: number; body?: CborValue } | CborValue; + +/** Mock boundary node: routes by endpoint kind and the request's method_name / request_type. */ +export function mockIc(handlers: { query?: Record CValue)>; queryTypes?: Record; call?: Handler; readState?: Handler }) { + const calls: { url: string; content: CborMap }[] = []; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const env = cborDecode(new Uint8Array(init!.body as Uint8Array)) as CborMap; + const content = env.content as CborMap; + calls.push({ url, content }); + const respond = (r: ReturnType) => { + const x = r && typeof r === "object" && !Array.isArray(r) && !(r instanceof Uint8Array) && "status" in r && typeof r.status === "number" ? (r as { status: number; body?: CborValue }) : { status: 200, body: r as CborValue }; + return new Response(x.body === undefined ? null : (cborEncode(x.body) as BodyInit), { status: x.status }); + }; + if (url.endsWith("/query")) { + const m = String(content.method_name); + const h = handlers.query?.[m]; + if (h === undefined) return respond({ status: "rejected", reject_code: 3n, reject_message: `no mock for ${m}` }); + const v = typeof h === "function" ? h(content.arg as Uint8Array) : h; + return respond({ status: "replied", reply: { arg: candidEncode([handlers.queryTypes?.[m] ?? C.nat], [v]) } }); + } + if (url.endsWith("/call")) return respond(handlers.call ? handlers.call(env, url) : { status: 202 }); + if (url.endsWith("/read_state")) return respond(handlers.readState ? handlers.readState(env, url) : { status: 500 }); + return new Response(null, { status: 404 }); + }) as typeof fetch; + return { fetch: f, calls }; +} + +/** A certificate (no signature: this module doesn't verify them) whose tree has request_status//… */ +export function certificate(id: Uint8Array, fields: Record): Uint8Array { + const leaves = Object.entries(fields).map(([k, v]) => [2n, utf8(k), [3n, v]] as CborValue); + const fork = (xs: CborValue[]): CborValue => (xs.length === 1 ? xs[0]! : [1n, xs[0]!, fork(xs.slice(1))]); + const tree: CborValue = [1n, [4n, new Uint8Array(32)], [2n, utf8("request_status"), [2n, id, fork(leaves)]]]; + return cborEncode({ tree, signature: new Uint8Array(48) }); +} + +export function ctxFor(fetchImpl: typeof fetch, network: Network = ICP_TEST): ChainContext { + return { network, account: makeAccount(), fetch: fetchImpl }; +} + +export { candidDecode }; diff --git a/packages/chains-icp/test/icp.test.ts b/packages/chains-icp/test/icp.test.ts new file mode 100644 index 00000000..83b5ddbc --- /dev/null +++ b/packages/chains-icp/test/icp.test.ts @@ -0,0 +1,302 @@ +import { describe, expect, it } from "vitest"; +import { type DappRequest, WALLET_ORIGIN } from "@clip-wallet/core"; +import { Cbor, lookup_path, requestIdOf } from "@dfinity/agent"; +import { IDL } from "@dfinity/candid"; +import { Secp256k1PublicKey } from "@dfinity/identity-secp256k1"; +import { Principal } from "@dfinity/principal"; +import { + Account, + C, + ICP_MAINNET, + ICP_METHODS, + ICP_NETWORKS, + ICP_TEST, + LEDGERS, + LegacyTransferArgs, + TransferArg, + TransferResult, + accountIdFromHex, + accountIdOf, + accountIdentifier, + callContent, + candidDecode, + candidEncode, + cborDecode, + derPublicKey, + envelope, + icrcAccountFromText, + icrcAccountToText, + lookup, + named, + parseRecipient, + principalFromText, + readStateContent, + requestId, + signDigest, +} from "../src/index.js"; +import type { CborMap, CborValue } from "../src/cbor.js"; +import { b64decode, fromHex, hex, utf8 } from "../src/util.js"; +import { BOB, NOW, certificate, ctxFor, makeAccount, mockIc, module, signer } from "./helpers.js"; +import { FIX } from "./signatures.js"; + +const TESTICP = LEDGERS.test[0]!; +/** @dfinity/agent 2.x takes ArrayBuffers. */ +const ab = (b: Uint8Array): ArrayBuffer => b.slice().buffer as ArrayBuffer; +const ckTESTBTC = LEDGERS.test[1]!; + +// @dfinity/candid equivalents of the types in src/ledger.ts. +const dAccount = IDL.Record({ owner: IDL.Principal, subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)) }); +const dTransferArg = IDL.Record({ + from_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)), + to: dAccount, + amount: IDL.Nat, + fee: IDL.Opt(IDL.Nat), + memo: IDL.Opt(IDL.Vec(IDL.Nat8)), + created_at_time: IDL.Opt(IDL.Nat64), +}); +const dTransferError = IDL.Variant({ + BadFee: IDL.Record({ expected_fee: IDL.Nat }), + BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }), + InsufficientFunds: IDL.Record({ balance: IDL.Nat }), + TooOld: IDL.Null, + CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }), + TemporarilyUnavailable: IDL.Null, + Duplicate: IDL.Record({ duplicate_of: IDL.Nat }), + GenericError: IDL.Record({ error_code: IDL.Nat, message: IDL.Text }), +}); +const dTokens = IDL.Record({ e8s: IDL.Nat64 }); +const dLegacyArgs = IDL.Record({ + memo: IDL.Nat64, + amount: dTokens, + fee: dTokens, + from_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)), + to: IDL.Vec(IDL.Nat8), + created_at_time: IDL.Opt(IDL.Record({ timestamp_nanos: IDL.Nat64 })), +}); + +describe("networks, principals and account ids", () => { + it("mainnet and the test-token network, each with its ledgers", () => { + expect(ICP_NETWORKS.map((n) => n.id)).toEqual(["icp:test", "icp:737ba355e855bd4b61279056603e0550"]); + expect(ICP_TEST.testnet && !ICP_MAINNET.testnet).toBe(true); + expect(ICP_MAINNET.nativeAsset).toMatchObject({ key: "icp", symbol: "ICP", decimals: 8 }); + expect(ICP_TEST.nativeAsset).toMatchObject({ key: "icp", symbol: "TESTICP", decimals: 8 }); + expect(LEDGERS.mainnet.map((l) => l.symbol)).toEqual(["ICP", "ckBTC", "ckUSDC", "ckETH"]); + expect(LEDGERS.mainnet.filter((l) => l.bridged).map((l) => l.key)).toEqual(["ckbtc", "ckusdc", "cketh"]); + }); + + it("the abandon-about principal matches @dfinity (public key → DER → self-authenticating principal)", () => { + expect(module.derivationPath(0)).toBe("m/44'/223'/0'/0/0"); + expect(module.curve).toBe("secp256k1"); + const pub = fromHex(FIX.publicKey); + expect(module.addressFromPublicKey(pub, ICP_TEST)).toBe(FIX.me); + const uncompressed = derPublicKey(pub).slice(-65); + const dk = Secp256k1PublicKey.fromRaw(ab(uncompressed)); + expect(hex(derPublicKey(pub))).toBe(hex(new Uint8Array(dk.toDer()))); + expect(Principal.selfAuthenticating(new Uint8Array(dk.toDer())).toText()).toBe(FIX.me); + }); + + it("principal text round-trips with its checksum; account ids and ICRC-1 account text too", () => { + expect(principalFromText(FIX.me)).not.toBeNull(); + expect(principalFromText(FIX.me.replace(/^t/, "u"))).toBeNull(); + expect(principalFromText("ryjl3-tyaaa-aaaaa-aaaba-cai")).toEqual(Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai").toUint8Array()); + // Vector from packages/vault: the anonymous principal's default account identifier. + expect(hex(accountIdentifier(Uint8Array.of(4)))).toBe("1c7a48ba6a562aa9eaa2481a9049cdf0433b9738c992d698c31d8abf89cadc79"); + expect(accountIdOf(BOB)).toBe(FIX.bobAccountId); + expect(accountIdFromHex(FIX.bobAccountId)).not.toBeNull(); + expect(accountIdFromHex(FIX.bobAccountId.replace(/^3/, "4"))).toBeNull(); + // ICRC-1 TextualEncoding.md examples. + const owner = principalFromText("k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae")!; + const sub = Uint8Array.from({ length: 32 }, (_, i) => i + 1); + expect(icrcAccountToText({ owner, subaccount: sub })).toBe("k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"); + expect(icrcAccountFromText("k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.01")).toBeNull(); + expect(icrcAccountFromText("k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae.1")).toBeNull(); + expect(icrcAccountFromText("k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-q6bn32y.")).toBeNull(); + expect(icrcAccountFromText("k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.1")?.subaccount?.[31]).toBe(1); + expect(icrcAccountFromText("k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627j.1")).toBeNull(); + expect(module.isAddress(FIX.me) && module.isAddress(FIX.bobAccountId)).toBe(true); + expect(module.isAddress("0x" + FIX.bobAccountId)).toBe(false); + expect(parseRecipient(FIX.bobAccountId)?.kind).toBe("accountId"); + expect(module.networksForAddress(FIX.bob, ICP_NETWORKS)).toHaveLength(2); + }); +}); + +describe("wire formats (cross-checked with @dfinity/agent and @dfinity/candid)", () => { + it("request ids: the spec's example and @dfinity/agent requestIdOf", () => { + const spec = callContent({ canisterId: fromHex("00000000000004d2"), method: "hello", arg: utf8("DIDL\x00\xfd*").slice(0, 7), sender: Uint8Array.of(4), ingressExpiry: 1685570400000000000n }); + // The spec's arg is the 7 bytes "DIDL", 0x00, 0xFD, "*". + spec.arg = Uint8Array.of(0x44, 0x49, 0x44, 0x4c, 0x00, 0xfd, 0x2a); + expect(hex(requestId(spec))).toBe("1d1091364d6bb8a6c16b203ee75467d59ead468f523eb058880ae8ec80e2b101"); + const call = callContent({ canisterId: principalFromText(TESTICP.canisterId)!, method: "icrc1_transfer", arg: utf8("DIDL"), sender: principalFromText(FIX.me)!, ingressExpiry: 1790000240000000000n, nonce: new Uint8Array(16).fill(7) }); + expect(hex(requestId(call))).toBe(hex(new Uint8Array(requestIdOf(call as never)))); + const rs = readStateContent({ requestId: requestId(call), sender: principalFromText(FIX.me)!, ingressExpiry: 1790000240000000000n }); + expect(hex(requestId(rs))).toBe(hex(new Uint8Array(requestIdOf(rs as never)))); + }); + + it("Candid encoding equals IDL.encode; decoding reads IDL.encode's TransferResult", () => { + const to = { owner: principalFromText(BOB)!, subaccount: [] }; + const arg = { from_subaccount: [], to, amount: 150000000n, fee: [10000n], memo: [], created_at_time: [1790000000000000000n] }; + const dArg = { ...arg, to: { owner: Principal.fromText(BOB), subaccount: [] } }; + expect(hex(candidEncode([TransferArg], [arg]))).toBe(hex(new Uint8Array(IDL.encode([dTransferArg], [dArg])))); + expect(hex(candidEncode([Account], [to]))).toBe(hex(new Uint8Array(IDL.encode([dAccount], [{ owner: Principal.fromText(BOB), subaccount: [] }])))); + const legacy = { memo: 0n, amount: { e8s: 150000000n }, fee: { e8s: 10000n }, from_subaccount: [], to: fromHex(FIX.bobAccountId), created_at_time: [{ timestamp_nanos: 1790000000000000000n }] }; + expect(hex(candidEncode([LegacyTransferArgs], [legacy]))).toBe(hex(new Uint8Array(IDL.encode([dLegacyArgs], [legacy])))); + const ok = new Uint8Array(IDL.encode([IDL.Variant({ Ok: IDL.Nat, Err: dTransferError })], [{ Ok: 42n }])); + expect(named(TransferResult, candidDecode(ok)[0]!)).toEqual({ Ok: 42n }); + const err = new Uint8Array(IDL.encode([IDL.Variant({ Ok: IDL.Nat, Err: dTransferError })], [{ Err: { GenericError: { error_code: 7n, message: "nope" } } }])); + expect(named(TransferResult, candidDecode(err)[0]!)).toEqual({ Err: { GenericError: { error_code: 7n, message: "nope" } } }); + expect(named(C.nat8, candidDecode(new Uint8Array(IDL.encode([IDL.Nat8], [8])))[0]!)).toBe(8); + }); + + it("envelopes are CBOR @dfinity/agent reads back; certificate lookups agree with lookup_path", () => { + const call = callContent({ canisterId: principalFromText(TESTICP.canisterId)!, method: "icrc1_transfer", arg: utf8("DIDL"), sender: principalFromText(FIX.me)!, ingressExpiry: 5n }); + const env = Cbor.decode<{ content: { method_name: string; ingress_expiry: unknown }; sender_sig: Uint8Array }>(ab(envelope(call, { publicKeyDer: derPublicKey(fromHex(FIX.publicKey)), signature: new Uint8Array(64).fill(1) }))); + expect(env.content.method_name).toBe("icrc1_transfer"); + expect(new Uint8Array(env.sender_sig)).toEqual(new Uint8Array(64).fill(1)); + const id = requestId(call); + const cert = cborDecode(certificate(id, { status: utf8("replied"), reply: utf8("DIDL") })) as CborMap; + const path = [utf8("request_status"), id, utf8("status")]; + expect(lookup(cert.tree as CborValue, path)).toEqual(utf8("replied")); + const theirs = lookup_path(path.map(ab), Cbor.decode<{ tree: never }>(ab(certificate(id, { status: utf8("replied") }))).tree) as { status?: string; value?: Uint8Array } | Uint8Array; + expect(new TextDecoder().decode(new Uint8Array((theirs as { value?: Uint8Array }).value ?? (theirs as Uint8Array)))).toBe("replied"); + expect(lookup(cert.tree as CborValue, [utf8("request_status"), new Uint8Array(32), utf8("status")])).toBe(null); + }); +}); + +function transferRequest(over: Partial = {}, params: Record = {}): DappRequest { + return { + id: "r1", + origin: WALLET_ORIGIN, + via: "injected", + family: "icp", + networkId: "icp:test", + method: ICP_METHODS.callCanister, + params: { canisterId: TESTICP.canisterId, sender: FIX.me, method: "icrc1_transfer", arg: "", ingressExpiry: String(BigInt(NOW + 240000) * 1000000n), ...params }, + ...over, + }; +} + +const queries = { icrc1_fee: 10000n, icrc1_balance_of: 1000000000n }; + +describe("send (buildTransfer → decode → prepare → finalize)", () => { + it("to a principal: plain words, two signatures, sent through the synchronous call endpoint", async () => { + let callId: Uint8Array | undefined; + const ok = new Uint8Array(IDL.encode([IDL.Variant({ Ok: IDL.Nat, Err: dTransferError })], [{ Ok: 1234n }])); + const ic = mockIc({ + query: queries, + call: (env) => { + callId = requestId((env as CborMap).content as never); + return { status: "replied", certificate: certificate(callId, { status: utf8("replied"), reply: ok }) }; + }, + }); + const ctx = ctxFor(ic.fetch); + const r = await module.buildTransfer({ asset: ICP_TEST.nativeAsset, to: FIX.bob, amount: "150000000" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, method: "icrc49_call_canister", params: { canisterId: TESTICP.canisterId, method: "icrc1_transfer", sender: FIX.me } }); + const d = await module.decode(r, ctx); + expect(d.title).toBe("Send 1.5 TESTICP to mnnk5…yqe"); + expect(d.titleMsg?.id).toBe("bg.req.sendTo"); + expect(d.blind).toBe(false); + expect(d.lines).toEqual([ + { label: "To", value: FIX.bob }, + { label: "Network fee", value: "0.0001 TESTICP" }, + ]); + expect(d.balanceChanges).toEqual([{ asset: ICP_TEST.nativeAsset, delta: "-150010000" }]); + expect(d.fee).toEqual({ asset: ICP_TEST.nativeAsset, amount: "10000" }); + const payloads = await module.prepare(r, ctx, "ap"); + expect(payloads.map((p) => [p.scheme, hex(p.bytes)])).toEqual(FIX.toPrincipalDigests.map((x) => ["ecdsa-secp256k1", x])); + const out = (await module.finalize(r, payloads.map((p) => signer.sign(p)), ctx)) as Record; + expect(out).toMatchObject({ status: "replied", blockIndex: "1234", requestId: hex(callId!) }); + // The submitted envelope carries the DER key and the call signature; its content is what was decoded. + const sent = ic.calls.find((c) => c.url.endsWith("/call"))!; + expect(sent.url).toBe(`https://icp-api.io/api/v4/canister/${TESTICP.canisterId}/call`); + const arg = named(TransferArg, candidDecode(sent.content.arg as Uint8Array)[0]!) as Record; + expect(arg).toMatchObject({ amount: 150000000n, fee: [10000n], created_at_time: [BigInt(NOW) * 1000000n] }); + expect(hex(signDigest(requestId(sent.content as never)))).toBe(FIX.toPrincipalDigests[0]); + }); + + it("to an exchange's account id: the ICP ledger's legacy transfer, with a note about account ids", async () => { + const ok = new Uint8Array(IDL.encode([IDL.Variant({ Ok: IDL.Nat64, Err: IDL.Variant({ TxCreatedInFuture: IDL.Null }) })], [{ Ok: 99n }])); + let reads = 0; + const ic = mockIc({ + query: queries, + call: () => ({ status: 202 }), + readState: (env) => { + reads++; + const c = (env as CborMap).content as CborMap; + const id = ((c.paths as Uint8Array[][])[0]!)[1]!; + return { certificate: certificate(id, reads < 2 ? { status: utf8("processing") } : { status: utf8("replied"), reply: ok }) }; + }, + }); + const ctx = ctxFor(ic.fetch); + const r = await module.buildTransfer({ asset: ICP_TEST.nativeAsset, to: FIX.bobAccountId, amount: "150000000" }, ctx); + expect((r.params as { method: string }).method).toBe("transfer"); + const d = await module.decode(r, ctx); + expect(d.title).toBe("Send 1.5 TESTICP to 39837cd2…7a18"); + expect(d.warnings).toEqual([expect.objectContaining({ level: "info", code: "new-recipient", msg: expect.objectContaining({ id: "bg.icp.toAccountId" }) })]); + const payloads = await module.prepare(r, ctx, "ap"); + expect(payloads.map((p) => hex(p.bytes))).toEqual([...FIX.toAccountIdDigests]); + const out = await module.finalize(r, payloads.map((p) => signer.sign(p)), ctx); + expect(out).toMatchObject({ status: "replied", blockIndex: "99" }); + expect(reads).toBe(2); + }); + + it("a bad signature is refused before anything is sent", async () => { + const ic = mockIc({ query: queries, call: () => ({ status: 500 }) }); + const ctx = ctxFor(ic.fetch); + const r = await module.buildTransfer({ asset: ICP_TEST.nativeAsset, to: FIX.bob, amount: "150000000" }, ctx); + const sigs = (await module.prepare(r, ctx, "ap")).map((p) => signer.sign(p)); + await expect(module.finalize(r, [sigs[1]!, sigs[0]!], ctx)).rejects.toMatchObject({ code: "icp/bad-signature" }); + await expect(module.finalize(r, [sigs[0]!], ctx)).rejects.toMatchObject({ code: "icp/bad-signature" }); + const flipped = { ...sigs[0]!, bytes: Uint8Array.from(sigs[0]!.bytes, (b, i) => (i === 5 ? b ^ 1 : b)) }; + await expect(module.finalize(r, [flipped, sigs[1]!], ctx)).rejects.toMatchObject({ code: "icp/bad-signature" }); + expect(ic.calls.some((c) => c.url.endsWith("/call"))).toBe(false); + }); + + it("a ledger error comes back in plain words", async () => { + const err = new Uint8Array(IDL.encode([IDL.Variant({ Ok: IDL.Nat, Err: dTransferError })], [{ Err: { InsufficientFunds: { balance: 5000n } } }])); + const ic = mockIc({ query: queries, call: (env) => ({ status: "replied", certificate: certificate(requestId((env as CborMap).content as never), { status: utf8("replied"), reply: err }) }) }); + const ctx = ctxFor(ic.fetch); + const r = await module.buildTransfer({ asset: ICP_TEST.nativeAsset, to: FIX.bob, amount: "150000000" }, ctx); + const sigs = (await module.prepare(r, ctx, "ap")).map((p) => signer.sign(p)); + await expect(module.finalize(r, sigs, ctx)).rejects.toMatchObject({ + code: "icp/transfer-failed", + userMessage: "You don't have enough TESTICP for this and its fee. Your balance is 0.00005 TESTICP. Nothing was sent.", + }); + }); + + it("refuses what it can't or shouldn't send", async () => { + const ctx = ctxFor(mockIc({ query: queries }).fetch); + const asset = ICP_TEST.nativeAsset; + await expect(module.buildTransfer({ asset, to: FIX.me, amount: "1" }, ctx)).rejects.toMatchObject({ code: "icp/self-transfer" }); + await expect(module.buildTransfer({ asset, to: accountIdOf(FIX.me), amount: "1" }, ctx)).rejects.toMatchObject({ code: "icp/self-transfer" }); + await expect(module.buildTransfer({ asset, to: "not-a-principal", amount: "1" }, ctx)).rejects.toMatchObject({ code: "icp/bad-address" }); + await expect(module.buildTransfer({ asset, to: FIX.bob, amount: "0" }, ctx)).rejects.toMatchObject({ code: "icp/bad-amount" }); + await expect(module.buildTransfer({ asset, to: FIX.bob, amount: "999995000" }, ctx)).rejects.toMatchObject({ code: "icp/insufficient-funds" }); + const btc = { key: "ckbtc", symbol: "ckTESTBTC", name: "ckTESTBTC", decimals: 8, networkId: "icp:test", address: ckTESTBTC.canisterId }; + await expect(module.buildTransfer({ asset: btc, to: FIX.bobAccountId, amount: "1" }, ctx)).rejects.toMatchObject({ code: "icp/account-id-unsupported" }); + }); + + it("refuses requests from sites, other networks, other accounts, unknown ledgers, unreadable or expired args", async () => { + const ctx = ctxFor(mockIc({ query: queries }).fetch); + const r = await module.buildTransfer({ asset: ICP_TEST.nativeAsset, to: FIX.bob, amount: "150000000" }, ctx); + const p = r.params as Record; + await expect(module.decode({ ...r, origin: "https://app.example" }, ctx)).rejects.toMatchObject({ code: "icp/unsupported-method" }); + await expect(module.decode(r, ctxFor(ctx.fetch, ICP_MAINNET))).rejects.toMatchObject({ code: "icp/network-mismatch" }); + await expect(module.decode({ ...r, params: { ...p, sender: FIX.bob } }, ctx)).rejects.toMatchObject({ code: "icp/wrong-account" }); + await expect(module.decode({ ...r, params: { ...p, canisterId: LEDGERS.mainnet[0]!.canisterId } }, ctx)).rejects.toMatchObject({ code: "icp/unknown-ledger" }); + await expect(module.decode({ ...r, params: { ...p, method: "approve" } }, ctx)).rejects.toMatchObject({ code: "icp/unsupported-method" }); + await expect(module.decode(transferRequest({}, { arg: "RElETAAA" }), ctx)).rejects.toMatchObject({ code: "icp/bad-transaction" }); + const padded = new Uint8Array([...b64decode(p.arg as string), 0]); + await expect(module.decode({ ...r, params: { ...p, arg: btoa(String.fromCharCode(...padded)) } }, ctx)).rejects.toMatchObject({ code: "icp/bad-transaction" }); + await expect(module.decode({ ...r, params: { ...p, ingressExpiry: String(BigInt(NOW - 1) * 1000000n) } }, ctx)).rejects.toMatchObject({ code: "icp/expired" }); + }); +}); + +describe("balances", () => { + it("TESTICP always, other ledgers only when held", async () => { + const ic = mockIc({ query: { icrc1_balance_of: (arg) => (hex(arg).includes(hex(principalFromText(FIX.me)!)) ? 250000000n : 0n) } }); + const b = await module.getBalances(ctxFor(ic.fetch)); + expect(b).toEqual([{ asset: ICP_TEST.nativeAsset, amount: "250000000" }, ...LEDGERS.test.slice(1).map((l) => ({ asset: expect.objectContaining({ address: l.canisterId, bridged: true }), amount: "250000000" }))]); + expect(ic.calls.every((c) => c.url.startsWith("https://icp-api.io/api/v3/canister/") && hex(c.content.sender as Uint8Array) === "04")).toBe(true); + expect(makeAccount().address).toBe(FIX.me); + }); +}); diff --git a/packages/chains-icp/test/signatures.ts b/packages/chains-icp/test/signatures.ts new file mode 100644 index 00000000..ab78f8c6 --- /dev/null +++ b/packages/chains-icp/test/signatures.ts @@ -0,0 +1,29 @@ +/** + * Signature fixtures, computed once OFFLINE (outside the repo) from the public BIP-39 test vector ("abandon" ×11 + + * "about", empty passphrase), secp256k1 at m/44'/223'/0'/0/0 (derivationPath(0)), with the vault's own derive + + * signEcdsa (RFC 6979, low-S). The principal equals @dfinity/identity-secp256k1 `fromSeedPhrase(…).getPrincipal()` + * (packages/vault test/families87.test.ts checks that). Only the public key, principals, payload digests and + * signatures live here. + * + * Both requests are buildTransfer on the test network (TESTICP ledger xafvr-…-cai) with the helpers' fixed clock + * (NOW = 1_790_000_000_000 ms) and nonce (16 × 0x07), fee 10,000 e8s, amount 1.5 TESTICP: + * - toPrincipal: icrc1_transfer to bob's principal. [call, read_state] signatures over + * SHA-256("\x0Aic-request" ‖ request id). + * - toAccountId: the ICP ledger's legacy `transfer` to bob's account identifier. + */ +export const FIX = { + publicKey: "03abdb60eb7c96408414d1e251d41ca0ecf89a4541768cba7eed8174c53246d58c", + me: "tgzar-4lpln-fq34h-6hxo4-wlm3x-6g3or-6hxvr-d6jbw-ooh2b-lzsw4-aqe", + bob: "mnnk5-gfqmo-4omau-3uj75-wcco3-qwour-lsvsv-tmcw3-2kwze-f6orv-yqe", + bobAccountId: "39837cd2d2dc28c9f9d1445ca4ed1115d1e72bd200cc7f463537aad571ad7a18", + toPrincipalDigests: ["7f2f39c8874f40368760653fb7f504f817caf0098ff48cdbf528eebf62c602f9", "e7cf41d82f2594757f723739d724cbfad0a1e533378b75e375f9837e97bb6e8d"], + toPrincipalSigs: [ + "bb2d386cd39d5eda8ce2762bab212afc5aa50a43b84ce6fcbb8270372a0c4ac325761149e52ed868a60fd1e46075fd0a7a795c5529a0fed5f575567fb5186364", + "e9c695d912ba28744ef1dfa0ec9833a61bb0f55bda089ef4e0a72e0f211a3de71dee25de8b531b4134cdfd1232db1222fcd4c024c71c3622d8c4b39a6a2ecfb8", + ], + toAccountIdDigests: ["5bdfeea9c488597a26f42c2739b117eb75304f1045cf7ff5c1395e89e9bd5463", "ba6a9ff48a78dc81f68564749f65beaf55148ddf147510e938fd4cc4045d9064"], + toAccountIdSigs: [ + "7c2d8009a277acc3de1205cad754641bed81c7cc56e7f29d186168bb47ce171b5a1eea764f4154505948016ed915a9b63d3120aea8e0421b47eb8be33b6cd5aa", + "ff3e7bb1718eed3f5fcfc352261ee31c2e87351c2486acdb82626e2ca7bfebfb4694e40b12c7913790a9bc5aaf60d800a121423704f53e8deaf314e07f10fd64", + ], +} as const; diff --git a/packages/chains-icp/tsconfig.json b/packages/chains-icp/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-icp/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-multiversx/LICENSE b/packages/chains-multiversx/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-multiversx/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-multiversx/NOTICE b/packages/chains-multiversx/NOTICE new file mode 100644 index 00000000..40a8b25e --- /dev/null +++ b/packages/chains-multiversx/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-multiversx +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-multiversx/README.md b/packages/chains-multiversx/README.md new file mode 100644 index 00000000..26c48a39 --- /dev/null +++ b/packages/chains-multiversx/README.md @@ -0,0 +1,227 @@ +# @clip-wallet/chains-multiversx + +MultiversX `ChainModule` for Clip Wallet. It builds, decodes and dry-runs EGLD and ESDT transactions, and it never +touches keys. `prepare()` returns the exact bytes a MultiversX ed25519 signature covers. `finalize()` checks the vault's +signature with `ed25519.verify` before it sends anything. + +There's no SDK at runtime. The package uses hand-written serialization with `@noble/curves` (verify only), +`@noble/hashes` (keccak) and `@scure/base` (bech32). `@multiversx/sdk-core` is a devDependency: the tests check the +bytes against it. + +## Networks + +| NetworkId | chain ID | gateway (`rpcUrls[0]`) | API (`indexerUrl`) | explorer | +|---|---|---|---|---| +| `mvx:D` (devnet, testnet flag) | `D` | https://devnet-gateway.multiversx.com | https://devnet-api.multiversx.com | https://devnet-explorer.multiversx.com | +| `mvx:T` (testnet, testnet flag) | `T` | https://testnet-gateway.multiversx.com | https://testnet-api.multiversx.com | https://testnet-explorer.multiversx.com | +| `mvx:1` (mainnet) | `1` | https://gateway.multiversx.com | https://api.multiversx.com | https://explorer.multiversx.com | + +- **Ids.** These are the ChainAgnostic `mvx` namespace ids (Draft: `mvx/caip2.md`), resolved from `erd_chain_id`. They're also the + chain ids of MultiversX's WalletConnect namespace `mvx`. +- **Checked live.** On 2026-10-06 every gateway answered `GET /network/config` without a key. Each reported min gas price + 1,000,000,000, min gas limit 50,000, 1,500 gas per data byte and gas price modifier 0.01. Every API answered `/about`. +- **Devnet first.** Devnet is listed first because MultiversX's faucet and dapps' test deployments are there. +- **Asset keys.** + - EGLD is `egld` (18 decimals). + - USDC is `usdc.mvx` and marked `bridged`. MultiversX USDC is Ethereum USDC moved over a bridge ("WrappedUSDC"), + so it never merges with Circle's native USDC. It is `USDC-c76f1f` on mainnet and `USDC-350c4e` on devnet. + `USDC-350c4e` is the devnet USDC with a registered assets entry and xExchange's EGLD/USDC pool. The other devnet + `USDC-*` tokens are unbranded copies. + - Testnet has no known USDC. + - Other ESDTs are `esdt:`. +- **Spam.** An ESDT is marked `spam` if it isn't registered in the MultiversX assets repository (no `assets` in the + API), or if it calls itself USDC, USD or EGLD without being the real one. + +## Accounts and addresses + +- **Derivation.** `derivationPath(i)` = `m/44'/508'/0'/0'/i'`: SLIP-10 ed25519, all hardened. This is what xPortal, the + DeFi Wallet extension and sdk-core `Mnemonic.deriveKey(i)` use. +- **Addresses.** `addressFromPublicKey` returns `erd1…`, which is bech32 of the 32-byte key. +- **Validation.** `isAddress` checks the bech32 checksum and the `erd` prefix. Contracts (`erd1qqqqqqqqqqqqqpgq…`) + are valid addresses too. +- **Cross-check.** The fixtures are the public "abandon … about" account `erd1sqhjrt…ytny9g`. `packages/vault` + cross-checks the derivation with sdk-core. + +## Methods (`MULTIVERSX_METHODS`) + +The names and params are those of MultiversX's WalletConnect provider (`mx-sdk-js-wallet-connect-provider`, +`src/operation.ts` and `src/walletConnectV2Provider.ts`). + +| method | params | result | +|---|---|---| +| `mvx_signTransaction` | `{ transaction }` (sdk-core plain object) | `{ signature, transaction }` (signed plain object) | +| `mvx_signTransactions` | `{ transactions: [...] }` (up to 20) | `{ signatures: [{ signature }], transactions }` | +| `mvx_signMessage` | `{ message, address }` | `{ signature, address }` (hex) | +| `mvx_signAndSendTransactions` | `{ transactions }` (Clip Wallet's own; wallet-built requests only) | `{ txHash, txHashes, status: "success" }` | + +**Refusals:** +- A transaction whose `sender` isn't this account → `multiversx/wrong-account`. +- A `chainID` that isn't this network's → `multiversx/network-mismatch`. +- `mvx_signLoginToken`, `mvx_signNativeAuthToken` and `mvx_cancelAction` aren't supported yet → + `multiversx/unsupported-method`. + +### What gets signed (`prepare`) + +- **Transactions.** The bytes are the UTF-8 JSON of sdk-core's `TransactionComputer.toPlainObject(tx)`, which is what + `computeBytesForSigning` produces. + - Fields come in this order: nonce, value (string), receiver, sender, senderUsername, receiverUsername, gasPrice, + gasLimit, data (base64), chainID, version, options, guardian, relayer. + - Empty fields are left out, and `options: 0` is left out. + - If options bit 0 (hash signing) is set, the bytes are `keccak256` of that JSON instead, as `computeBytesForVerifying` + does. + - Guardian and relayer addresses are part of the signed bytes. Their own signatures are carried through untouched. +- **Messages.** The bytes are `keccak256("\x17Elrond Signed Message:\n" ‖ decimal byte length ‖ message)`, from sdk-core + `MessageComputer`. The message is taken as UTF-8 text. +- **Tests.** They check every case against sdk-core: plain, ESDT, usernames + guardian + relayer + hash signing, + version 1 with a note, and `MessageComputer`. +- **Live check.** The devnet gateway's `/transaction/simulate?checkSignature=true` accepted our signatures for both + normal and hash signing, and refused a flipped bit as `ed25519: invalid signature`. + +### Sending (`finalize`) + +1. Each signature is verified against the account's key first. A bad or missing signature → + `multiversx/bad-signature`, and nothing is sent. +2. The transactions go one at a time to `POST /transaction/send`. +3. After each one, `GET /transaction/{hash}/process-status` is polled (default 10 × 1.5 s). process-status is used + instead of `/transaction/{hash}?withResults=true` because it follows asynchronous cross-shard calls: a call whose + callback failed reports `fail` there, while `withResults` still says `success`. + - `fail` or `invalid` → a plain `ClipError` (`multiversx/transaction-failed`). + - Still pending after the last poll → the transaction was accepted, and the hash is returned. +4. Gateway rejections become plain words (`plainMultiversXError`): insufficient funds, a used nonce, a nonce too far + ahead, the wrong chain ID, not enough gas, a low gas price, a missing guardian co-signature, a busy pool, or a + rejected signature. + +## decode() + +Each request shows the full recipient, the token identifier, "Network fee: up to X EGLD" and "Sent by" (for +`mvx_sign*`, since the app gets the signed transaction). + +**The fee.** `TransactionComputer.computeTransactionFee` defines it as: (50,000 + 1,500 × data bytes) gas at the full +gas price, plus the rest of the gas limit at gas price × 0.01. Refunds can make the real fee lower. A relayed +transaction (one with a `relayer`) has its fee shown as sponsored. + +**Dry run.** Single transactions are dry-run with `POST /transaction/simulate?checkSignature=false`, using a zero +placeholder signature (the node accepts one when signature checks are off). A success marks the request +`simulated`. A failure adds a `simulation-failed` caution. + +| data field | title | warnings | +|---|---|---| +| empty | "Send 1.5 EGLD to erd1sxm…kwzk" | | +| text, to a person | the same, plus a "Note" line (exchanges use the data field as a memo) | | +| `ESDTTransfer@token@amount` | "Send 2.5 USDC to …" | look-alike USDC/EGLD → danger `known-scam` | +| `ESDTNFTTransfer@token@nonce@amount@dest` (to self) | "Send NFT #5 from NICENFT to …", or fungible amounts | | +| `MultiESDTNFTTransfer@dest@n@(token@nonce@amount)×n` (to self) | "Send 1 EGLD + 1 WEGLD + 1 × NICENFT #5 to …" (`EGLD-000000` = EGLD) | | +| a token payment followed by a function | "Approve {fn} on contract …" with "Also sends" | caution `unknown-call` | +| staking provider: `delegate` | "Stake 1 EGLD with castlestake" (provider identity from the API) | | +| `unDelegate@amount` / `claimRewards` / `withdraw` / `reDelegateRewards` | "Unstake 1 EGLD from …" / "Claim your staking rewards from …" / "Withdraw your unstaked EGLD from …" / "Restake your rewards with …" | | +| `SetGuardian@guardian@service` (to self) | "Make erd1… your account's guardian" | **danger `account-takeover`** | +| `GuardAccount` (to self) | "Turn on your account's guardian" | **danger `account-takeover`**: Clip Wallet can't provide the co-signature afterwards | +| `UnGuardAccount` (to self) | "Turn off your account's guardian" | | +| `ChangeOwnerAddress@owner` (to a contract) | "Give contract … to …" | **danger `account-takeover`** | +| any other `fn@args` to a contract | "Approve {fn} on contract erd1qqq…3rax" | caution `unknown-call` | +| anything else (binary data to a contract, unknown built-ins to yourself, malformed arguments) | "Approve an unreadable request from {host}" | `blind: true` + danger `blind-signing` | + +**How contracts are recognised.** A staking provider is a system smart contract (`00…01…`) that isn't one of the +fixed system contracts (staking, ESDT, governance, delegation manager, …). Only the five delegation functions get +plain words there. Contracts are any address that starts with 8 zero bytes. + +## Builders + +- **Transfers.** `buildTransfer({ asset, to, amount })` → `mvx_signAndSendTransactions` with one transaction: + - EGLD: an empty data field and gas 50,000. + - ESDT: `ESDTTransfer@@` and gas 50,000 + 1,500 × data bytes + 300,000. This is sdk-core's + `gasLimitESDTTransfer` (200,000) plus `ADDITIONAL_GAS_FOR_ESDT_TRANSFER` (100,000). + - The fields: version 2, options 0, the nonce from the gateway, and the network's minimum gas price. +- **Refusals:** + - A bad address (`multiversx/bad-address`), your own address (`multiversx/self-transfer`), or a zero amount. + - Not enough EGLD for the amount and fee (`multiversx/insufficient-funds`), or not enough of the token + (`multiversx/insufficient-token`). + - A guarded account (`multiversx/guarded`): its transactions need a guardian co-signature that Clip Wallet can't get. + +## Balances and NFTs + +- **`getBalances`.** EGLD comes from the gateway's `/address/{a}`. Fungible ESDTs come from the API's + `/accounts/{a}/tokens` (MetaESDTs, such as LP or farm positions, are skipped), with spam marked as above. +- **`getNfts`.** It returns `[]` for now. The API lists NFTs and SFTs at `/accounts/{a}/nfts`. + +## Dapp connectivity: a best-effort sdk-dapp provider (undocumented hook) + +There's no documented way for a third-party browser-extension wallet to be discovered by MultiversX dapps under its own +identity. Sources (checked 2026-10-06): + +- **sdk-dapp v5 has a fixed provider list.** `ProviderTypeEnum` is extension, metamask, passkey, walletConnect, ledger, + crossWindow, webview or none (`mx-sdk-dapp` `src/providers/types/providerFactory.types.ts`). Custom providers are + `ICustomProvider` objects (`{ name, type, iconUrl?, constructor(options) => Promise }`) passed to + `initApp({ customProviders })`. +- **The extension provider is the MultiversX DeFi Wallet's own channel.** `mx-sdk-js-extension-provider` looks for + `window.elrondWallet` / `window.multiversxWallet` and talks over `postMessage` targets `erdw-inpage` / + `erdw-contentScript`. Answering it would mean posing as the DeFi Wallet (AGENTS rule 8), so 1Mask doesn't. +- **The other providers don't apply.** The webview provider is for dapps embedded in xPortal / Hub (iframe or React + Native webview). The cross-window and iframe providers open a wallet URL that the dapp configures. + +**What 1Mask does instead (best effort).** `initApp` also merges `window.multiversx?.providers` with the dapp's +`customProviders`, deduplicated by `type` (sdk-dapp 5.7.3 `out/methods/initApp/initApp.mjs`), and +`ProviderFactory.create({ type })` calls the matching entry's `constructor({ address, anchor })`, then `init()` +(`out/providers/ProviderFactory.mjs`). This hook is **UNDOCUMENTED** as a wallet channel: sdk-dapp's README presents it +as something the dapp sets, and MultiversX's template dapp replaces `window.multiversx`. So it works only on sdk-dapp +dapps that don't override it, and only while sdk-dapp keeps reading it. + +- `packages/1mask/src/inpage/multiversx.ts` pushes Clip's own entry (type = the global key, "clipwallet"; the wallet's + name and icon) onto that array, keeping the array and other wallets' entries, and exposes the provider at + `window.clipwallet.multiversx`. +- `login({ token })` signs `address + token` as a MultiversX message: what sdk-native-auth-server verifies + (`signedMessage = address + body`, MessageComputer). decode() shows it as "Sign in to {site}" with the token's + origin, and a danger `domain-mismatch` warning if the token names another site; the 1Mask background refuses such a + token outright. +- Transactions and messages go through `mvx_signTransactions` / `mvx_signMessage` above. +- Checked against the real sdk-dapp 5.7.3 (scratch run, happy-dom): `initApp` listed the entry, + `ProviderFactory.create({ type: "clipwallet" })` + `login()` produced a native-auth token that + `@multiversx/sdk-native-auth-server` validated against the devnet API, and the transaction and message signatures + verified with sdk-core 15. + +**WalletConnect** (namespace `mvx`, chains `mvx:1` / `mvx:D` / `mvx:T`) works across dapps without any hook: this module +decodes and signs those requests. Stock sdk-dapp labels any WalletConnect wallet "xPortal App", and always asks for +`mvx_signLoginToken`, which isn't wired yet. + +**Dapp matrix** (`apps/extension/e2e/matrix/dapps/multiversx.ts`, devnet): sdk-dapp driven headless: `initApp` + +`ProviderFactory.create({ type: "clipwallet" })` + native-auth `login()` (L1), `signMessage` checked with sdk-dapp's +`verifyMessage` (L2), and a 1-attoEGLD self-transfer signed through `DappProvider.signTransactions` and sent to the devnet +gateway (L0, needs devnet EGLD). + +## Faucet + +The devnet wallet's Faucet tab (https://devnet-wallet.multiversx.com) gives 5 EGLD per 24 hours to accounts below +1 EGLD. It needs a logged-in wallet and a reCAPTCHA: `POST devnet-extras-api.multiversx.com/faucet {captcha}` with a +native-auth token, and `/faucet/settings` reports `recaptchaBypass: false`. The MultiversX docs also list +https://r3d4.fr/faucet. + +## Known gaps + +- The injected provider relies on sdk-dapp's undocumented `window.multiversx.providers` hook (see above). +- WalletConnect `mvx_signLoginToken` / `mvx_signNativeAuthToken` aren't handled. +- NFTs aren't listed. MetaESDT balances (LP, farm and staking positions) aren't shown. +- Guarded accounts can't send from Clip Wallet: the guardian co-signature (2FA service) isn't requested. +- Staked EGLD isn't shown as a balance. + +## Tests + +`test/multiversx.test.ts` uses no network. +- Byte-for-byte checks against `@multiversx/sdk-core` 16 (`TransactionComputer`, `MessageComputer`, `UserPublicKey.verify`). +- Decode cases for every row above, plus wrong network, account and method. +- The build → decode → prepare → finalize path with fixture signatures and a mocked gateway: send, failure, + rejection, and a bad signature refused before broadcast. +- Signatures were precomputed offline from the public "abandon … about" vector (`test/signatures.ts`). + +## Sources + +- sdk-core: https://github.com/multiversx/mx-sdk-js-core: `src/core/transactionComputer.ts`, `src/core/message.ts`, + `src/core/constants.ts`, `src/transfers/transferTransactionsFactory.ts`, `src/core/transactionsFactoryConfig.ts` +- CAIP-2: https://github.com/ChainAgnostic/namespaces/blob/main/mvx/caip2.md +- WalletConnect provider: https://github.com/multiversx/mx-sdk-js-wallet-connect-provider (`src/operation.ts`, `src/constants.ts`) +- sdk-dapp: https://github.com/multiversx/mx-sdk-dapp (`src/providers/ProviderFactory.ts`, `src/methods/initApp/initApp.ts`, README "custom providers") +- Extension provider: https://github.com/multiversx/mx-sdk-js-extension-provider (`src/extensionProvider.ts`) +- Webview provider: https://github.com/multiversx/mx-sdk-js-webview-provider +- Token transfers: https://docs.multiversx.com/tokens/fungible-tokens#transfers, https://docs.multiversx.com/tokens/nft-tokens#transfers +- Guardians: https://docs.multiversx.com/developers/built-in-functions#setguardian +- Gateway (proxy) API: https://docs.multiversx.com/sdk-and-tools/proxy, https://github.com/multiversx/mx-chain-proxy-go +- Faucet: https://github.com/multiversx/mx-docs/blob/main/docs/developers/tutorials/your-first-dapp.md diff --git a/packages/chains-multiversx/package.json b/packages/chains-multiversx/package.json new file mode 100644 index 00000000..114eece9 --- /dev/null +++ b/packages/chains-multiversx/package.json @@ -0,0 +1,80 @@ +{ + "name": "@clip-wallet/chains-multiversx", + "version": "0.1.0", + "description": "MultiversX ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "multiversx", + "egld", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-multiversx#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-multiversx" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "devDependencies": { + "@multiversx/sdk-core": "^16.0.0" + } +} diff --git a/packages/chains-multiversx/src/api.ts b/packages/chains-multiversx/src/api.ts new file mode 100644 index 00000000..3b1ad878 --- /dev/null +++ b/packages/chains-multiversx/src/api.ts @@ -0,0 +1,198 @@ +import { ClipError } from "@clip-wallet/core"; +import type { PlainTransaction } from "./tx.js"; +import type { TokenInfo } from "./networks.js"; + +/** + * The MultiversX gateway (proxy, mx-chain-proxy-go) and API (mx-api-service) over plain fetch. Only read calls, + * plus POST /transaction/send. Every failure becomes a ClipError in plain words. + * - gateway: GET /address/{a}, /address/{a}/esdt/{id}, /address/{a}/guardian-data, /network/config, + * /transaction/{hash}/process-status; POST /transaction/send + * - api: GET /accounts/{a}/tokens, /tokens/{id}, /providers/{a} + */ +export interface NetworkConfig { + chainId: string; + minGasPrice: bigint; + minGasLimit: bigint; + gasPerDataByte: bigint; + /** e.g. 0.01: the share of the gas price charged for gas beyond moving the data. */ + gasPriceModifier: number; +} + +export interface GatewayAccount { + address: string; + nonce: number; + balance: string; + username?: string; +} + +export interface ApiToken extends TokenInfo { + balance?: string; + type?: string; +} + +export interface ProcessStatus { + status: "success" | "fail" | "pending" | "invalid" | string; + reason?: string; +} + +interface SimResult { + status?: string; + failReason?: string; +} + +export class GatewayError extends Error { + constructor( + message: string, + public readonly status: number, + public readonly code?: string, + ) { + super(message); + } +} + +const unreachable = (cause: unknown) => new ClipError("We couldn't reach the MultiversX network. Check your connection and try again.", "multiversx/offline", cause); + +export class MultiversXClient { + constructor( + private readonly gateway: string, + private readonly api: string, + private readonly fetchImpl: typeof fetch, + ) {} + + private async gw(path: string, body?: unknown): Promise { + let r: Response; + try { + r = await this.fetchImpl(`${this.gateway}${path}`, body === undefined ? undefined : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); + } catch (e) { + throw unreachable(e); + } + const j = (await r.json().catch(() => null)) as { data?: T; error?: string; code?: string } | null; + if (!r.ok || !j || (j.code && j.code !== "successful") || j.error) throw new GatewayError(j?.error || `HTTP ${r.status}`, r.status, j?.code); + return j.data as T; + } + + private async getApi(path: string): Promise { + let r: Response; + try { + r = await this.fetchImpl(`${this.api}${path}`); + } catch (e) { + throw unreachable(e); + } + if (r.status === 404) return null; + if (!r.ok) throw new GatewayError(`HTTP ${r.status}`, r.status); + return (await r.json()) as T; + } + + async networkConfig(): Promise { + const d = await this.gw<{ config: Record }>("/network/config").catch((e) => { + throw e instanceof ClipError ? e : unreachable(e); + }); + const c = d.config; + return { + chainId: String(c.erd_chain_id), + minGasPrice: BigInt(String(c.erd_min_gas_price)), + minGasLimit: BigInt(String(c.erd_min_gas_limit)), + gasPerDataByte: BigInt(String(c.erd_gas_per_data_byte)), + gasPriceModifier: Number(c.erd_gas_price_modifier ?? 1), + }; + } + + async account(address: string): Promise { + const d = await this.gw<{ account: GatewayAccount }>(`/address/${address}`).catch((e) => { + throw e instanceof ClipError ? e : unreachable(e); + }); + return d.account; + } + + /** Fungible ESDT balance in base units ("0" when the account holds none). */ + async esdtBalance(address: string, identifier: string): Promise { + try { + const d = await this.gw<{ tokenData?: { balance?: string } }>(`/address/${address}/esdt/${encodeURIComponent(identifier)}`); + const b = d.tokenData?.balance ?? "0"; + return /^\d+$/.test(b) ? BigInt(b) : 0n; + } catch (e) { + if (e instanceof ClipError) throw e; + return 0n; + } + } + + /** True when the account has an active guardian (every transaction then needs the guardian's co-signature). */ + async isGuarded(address: string): Promise { + const d = await this.gw<{ guardianData?: { guarded?: boolean } }>(`/address/${address}/guardian-data`).catch((e) => { + throw e instanceof ClipError ? e : unreachable(e); + }); + return d.guardianData?.guarded === true; + } + + async send(tx: PlainTransaction): Promise { + const d = await this.gw<{ txHash: string }>("/transaction/send", tx); + if (!d?.txHash || !/^[0-9a-f]{64}$/.test(d.txHash)) throw new GatewayError("no transaction hash", 200); + return d.txHash; + } + + /** + * Dry run of an unsigned transaction (POST /transaction/simulate?checkSignature=false with a zero placeholder + * signature, which the node accepts when signature checks are off). Returns null when the node can't simulate. + */ + async simulate(tx: PlainTransaction): Promise<{ ok: boolean; reason?: string } | null> { + try { + const d = await this.gw<{ result?: SimResult & { senderShard?: SimResult; receiverShard?: SimResult } }>("/transaction/simulate?checkSignature=false", { ...tx, signature: "00".repeat(64) }); + const r = d?.result; + if (!r) return null; + const parts = [r, r.senderShard, r.receiverShard].filter((x): x is SimResult => !!x && typeof x.status === "string"); + if (!parts.length) return null; + const failed = parts.find((x) => x.status === "fail" || x.status === "invalid"); + return failed ? { ok: false, ...(failed.failReason ? { reason: failed.failReason } : {}) } : { ok: true }; + } catch (e) { + if (e instanceof ClipError) return null; + return null; + } + } + + /** + * process-status follows the whole transaction, including asynchronous calls between shards, so a call that fails + * in a callback reports "fail" (GET /transaction/{hash}?withResults=true can still say "success" there). + */ + async processStatus(hash: string): Promise { + try { + return await this.gw(`/transaction/${hash}/process-status`); + } catch (e) { + if (e instanceof ClipError) throw e; + return null; + } + } + + async tokens(address: string): Promise { + const r = await this.getApi(`/accounts/${address}/tokens?size=100&fields=identifier,name,ticker,decimals,balance,assets,isVerified,type`); + return Array.isArray(r) ? r : []; + } + + async token(identifier: string): Promise { + return this.getApi(`/tokens/${encodeURIComponent(identifier)}?fields=identifier,name,ticker,decimals,assets,isVerified`).catch(() => null); + } + + /** Staking provider's identity (e.g. "truststaking"), or null. */ + async providerName(address: string): Promise { + const p = await this.getApi<{ identity?: string }>(`/providers/${address}?fields=identity`).catch(() => null); + return p?.identity && /^[\w.-]{1,64}$/.test(p.identity) ? p.identity : null; + } +} + +/** + * Gateway / node rejection messages → plain words. Sources: mx-chain-go process/errors.go and + * mx-chain-proxy-go (e.g. "insufficient funds", "lowerNonceInTx", "invalid chain ID", "insufficient gas limit", + * "too many transactions", guardian errors). + */ +export function plainMultiversXError(message: string): string { + const m = message.toLowerCase(); + if (/insufficient (funds|balance)/.test(m)) return "You don't have enough EGLD for this and its network fee. Nothing was sent."; + if (/lower ?nonce|nonce too low|lowernonceintx/.test(m)) return "This transaction was already used. Nothing new was sent. Try again from the start."; + if (/higher ?nonce|nonce too high|veryhighnonceintx/.test(m)) return "Your earlier transactions haven't gone through yet. Wait a moment, then try again."; + if (/chain ?id/.test(m)) return "This transaction is for a different MultiversX network. Nothing was sent."; + if (/gas ?limit|not enough gas|insufficient gas/.test(m)) return "This transaction doesn't have enough gas to run. Nothing was sent."; + if (/gas ?price/.test(m)) return "The network fee price is too low for MultiversX right now. Nothing was sent."; + if (/guard/.test(m)) return "This account has a guardian, and the guardian's co-signature is missing. Nothing was sent."; + if (/too many transactions|tx pool|pool is full|cache is full/.test(m)) return "MultiversX is busy right now. Nothing was sent. Try again in a minute."; + if (/signature/.test(m)) return "The network didn't accept the signature. Nothing was sent."; + return "The MultiversX network rejected this transaction. Nothing was sent."; +} diff --git a/packages/chains-multiversx/src/describe.ts b/packages/chains-multiversx/src/describe.ts new file mode 100644 index 00000000..b954d47c --- /dev/null +++ b/packages/chains-multiversx/src/describe.ts @@ -0,0 +1,223 @@ +import { type AssetRef, type BalanceChange, type Msg, type Warning, msg, say, titled } from "@clip-wallet/core"; +import type { MultiversXClient, NetworkConfig } from "./api.js"; +import { EGLD_DECIMALS, EGLD_MULTI_ID, type TokenInfo, egldAsset, esdtAsset, isEsdtIdentifier, specFor, tickerOf } from "./networks.js"; +import { type Call, type Tx, parseCall } from "./tx.js"; +import { bigFromBytes, decodeAddress, encodeAddress, formatUnits, isContractKey, isSystemContractKey, short, textOf } from "./util.js"; + +/** + * Plain-language description of one MultiversX transaction. Built-in functions are parsed from the data field + * ("ESDTTransfer@@", …) as mx-chain-vm-common-go's builtInFunctions define them and + * sdk-core's TokenTransfersDataBuilder writes them: + * https://docs.multiversx.com/tokens/fungible-tokens#transfers and …/tokens/nft-tokens#transfers. + */ +export interface Described { + title: string; + titleMsg?: Msg; + lines: { label: string; value: string; labelMsg?: Msg }[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + blind: boolean; + /** Fee in base units (EGLD) if all the gas is used; refunds can make it lower. */ + fee: bigint; +} + +export interface DescribeContext { + networkId: string; + me: string; + host: string; + config: NetworkConfig; + client: MultiversXClient; + token(identifier: string): Promise; +} + +/** System smart contracts that aren't staking providers (mx-chain-go vm/address.go): staking, ESDT, governance, delegation manager, … */ +const SYSTEM_NOT_PROVIDER = new Set([ + "000000000000000000010000000000000000000000000000000000000001ffff", + "000000000000000000010000000000000000000000000000000000000002ffff", + "000000000000000000010000000000000000000000000000000000000003ffff", + "000000000000000000010000000000000000000000000000000000000004ffff", + "000000000000000000010000000000000000000000000000000000000005ffff", + "000000000000000000010000000000000000000000000000000000000006ffff", + "000000000000000000010000000000000000000000000000000000000007ffff", + "000000000000000000010000000000000000000000000000000000000008ffff", + "000000000000000000010000000000000000000000000000000000000009ffff", + "00000000000000000001000000000000000000000000000000000000000affff", + "00000000000000000001000000000000000000000000000000000000000bffff", +]); + +const DELEGATION_FNS = new Set(["delegate", "unDelegate", "claimRewards", "withdraw", "reDelegateRewards"]); + +/** Moving the data costs minGasLimit + gasPerDataByte × bytes at the full price; gas beyond that at price × modifier (TransactionComputer.computeTransactionFee). */ +export function maxFee(tx: Tx, c: NetworkConfig): bigint { + const move = c.minGasLimit + BigInt(tx.data.length) * c.gasPerDataByte; + if (tx.gasLimit <= move) return tx.gasLimit * tx.gasPrice; + const scaled = BigInt(Math.round(c.gasPriceModifier * 1_000_000)); + return move * tx.gasPrice + ((tx.gasLimit - move) * tx.gasPrice * scaled) / 1_000_000n; +} + +const keyHex = (k: Uint8Array) => Array.from(k, (b) => b.toString(16).padStart(2, "0")).join(""); + +interface Moved { + asset: AssetRef; + amount: bigint; + /** NFT / SFT nonce (0 for fungible tokens and EGLD). */ + nonce: bigint; + collection?: string; +} + +export async function describeTransaction(tx: Tx, d: DescribeContext): Promise { + const egld = egldAsset(d.networkId); + const fee = maxFee(tx, d.config); + const receiverKey = decodeAddress(tx.receiver)!; + const toContract = isContractKey(receiverKey); + const call = parseCall(tx.data); + const out: Described = { title: "", lines: [], balanceChanges: [], warnings: [], blind: false, fee }; + const toSelf = tx.receiver === d.me; + + const tokenAsset = async (identifier: string): Promise => { + const info = (await d.token(identifier)) ?? { identifier }; + return esdtAsset(d.networkId, info); + }; + const fmt = (m: Moved) => (m.nonce > 0n ? `${m.amount} × ${m.collection} #${m.nonce}` : `${formatUnits(m.amount, m.asset.decimals)} ${m.asset.symbol}`); + const out_ = (m: Moved) => { + if (m.amount > 0n && m.nonce === 0n) out.balanceChanges.push({ asset: m.asset, delta: (-m.amount).toString() }); + }; + const egldMoved = (amount: bigint): Moved => ({ asset: egld, amount, nonce: 0n }); + + /** A token payment: the identifier must look like an ESDT and the asset must not be a look-alike. */ + const payment = async (id: Uint8Array, nonce: bigint, amount: bigint): Promise => { + const identifier = new TextDecoder().decode(id); + if (identifier === EGLD_MULTI_ID && nonce === 0n) return egldMoved(amount); + const collection = nonce > 0n ? identifier : undefined; + if (!isEsdtIdentifier(identifier)) return null; + const asset = await tokenAsset(identifier); + if (asset.spam && /^(USDC|EGLD|USD)/i.test(asset.symbol)) { + out.warnings.push({ level: "danger", code: "known-scam", message: `${asset.symbol} here isn't the real ${asset.symbol}: it's a look-alike token (${identifier}).` }); + } + return { asset, amount, nonce, ...(collection ? { collection: tickerOf(collection) } : {}) }; + }; + + const describeTransfers = async (moved: Moved[], dest: string, rest: Call | null) => { + const amounts = moved.map(fmt).join(" + "); + const destKey = decodeAddress(dest)!; + if (rest) { + contractCall(rest.fn, dest, amounts); + } else if (moved.length === 1 && moved[0]!.nonce > 0n) { + const m = moved[0]!; + const values = { id: m.nonce.toString(), collection: m.collection!, to: short(dest) }; + Object.assign(out, m.amount === 1n ? titled(msg("bg.req.sendNftTo", values)) : titled(msg("bg.req.sendNftsTo", { count: m.amount.toString(), ...values }))); + } else { + Object.assign(out, titled(msg(isContractKey(destKey) ? "bg.req.sendToContract" : "bg.req.sendTo", isContractKey(destKey) ? { amount: amounts, contract: short(dest) } : { amount: amounts, to: short(dest) }))); + } + out.lines.push({ label: "To", value: dest }); + for (const m of moved) { + if (m.asset.address) out.lines.push({ label: "Token", value: m.nonce > 0n ? `${m.asset.address} #${m.nonce}` : m.asset.address }); + out_(m); + } + }; + + const contractCall = (fn: string, contract: string, sends: string | null) => { + Object.assign(out, titled(msg("bg.req.approveFnOnContract", { fn, contract: short(contract) }))); + out.lines.push({ label: "Contract", value: contract }, { label: "Function", value: fn }); + if (sends) out.lines.push({ label: "Also sends", value: sends }); + out.warnings.push({ level: "caution", code: "unknown-call", message: "Clip Wallet can name this call but can't fully read it, so some of its effects may not be shown." }); + }; + + if (!tx.data.length) { + // Plain EGLD transfer. + await describeTransfers([egldMoved(tx.value)], tx.receiver, null); + } else if (call?.fn === "ESDTTransfer" && call.args.length >= 2 && tx.value === 0n) { + const m = await payment(call.args[0]!, 0n, bigFromBytes(call.args[1]!)); + if (m) await describeTransfers([m], tx.receiver, rest(call, 2)); + else out.blind = true; + } else if (call?.fn === "ESDTNFTTransfer" && call.args.length >= 4 && toSelf && tx.value === 0n && call.args[3]!.length === 32) { + const m = await payment(call.args[0]!, bigFromBytes(call.args[1]!), bigFromBytes(call.args[2]!)); + if (m) await describeTransfers([m], encodeAddress(call.args[3]!), rest(call, 4)); + else out.blind = true; + } else if (call?.fn === "MultiESDTNFTTransfer" && call.args.length >= 2 && toSelf && tx.value === 0n && call.args[0]!.length === 32) { + const count = Number(bigFromBytes(call.args[1]!)); + const moved: Moved[] = []; + if (count >= 1 && count <= 100 && call.args.length >= 2 + 3 * count) { + for (let i = 0; i < count; i++) { + const [id, nonce, amount] = call.args.slice(2 + 3 * i, 5 + 3 * i); + const m = await payment(id!, bigFromBytes(nonce!), bigFromBytes(amount!)); + if (!m) break; + moved.push(m); + } + } + if (moved.length === count && count > 0) await describeTransfers(moved, encodeAddress(call.args[0]!), rest(call, 2 + 3 * count)); + else out.blind = true; + } else if (call && DELEGATION_FNS.has(call.fn) && isSystemContractKey(receiverKey) && !SYSTEM_NOT_PROVIDER.has(keyHex(receiverKey))) { + const validator = (await d.client.providerName(tx.receiver).catch(() => null)) ?? short(tx.receiver); + out.lines.push({ label: "Validator", value: validator === short(tx.receiver) ? tx.receiver : `${validator} (${tx.receiver})` }); + const egldText = (v: bigint) => `${formatUnits(v, EGLD_DECIMALS)} EGLD`; + if (call.fn === "delegate" && call.args.length === 0 && tx.value > 0n) { + Object.assign(out, titled(msg("bg.req.stakeWith", { amount: egldText(tx.value), validator }))); + } else if (call.fn === "unDelegate" && call.args.length === 1 && tx.value === 0n) { + Object.assign(out, titled(msg("bg.req.unstakeFrom", { amount: egldText(bigFromBytes(call.args[0]!)), validator }))); + } else if (call.fn === "claimRewards" && call.args.length === 0 && tx.value === 0n) { + Object.assign(out, titled(msg("bg.multiversx.claimRewardsFrom", { validator }))); + } else if (call.fn === "withdraw" && call.args.length === 0 && tx.value === 0n) { + Object.assign(out, titled(msg("bg.multiversx.withdrawFrom", { validator }))); + } else if (call.fn === "reDelegateRewards" && call.args.length === 0 && tx.value === 0n) { + Object.assign(out, titled(msg("bg.multiversx.restakeRewardsWith", { validator }))); + } else { + out.lines.length = 0; + contractCall(call.fn, tx.receiver, tx.value > 0n ? egldText(tx.value) : null); + } + if (tx.value > 0n) out.balanceChanges.push({ asset: egld, delta: (-tx.value).toString() }); + } else if (call && toSelf && call.fn === "SetGuardian" && call.args.length >= 1 && call.args[0]!.length === 32) { + const guardian = encodeAddress(call.args[0]!); + Object.assign(out, titled(msg("bg.multiversx.setGuardianTitle", { guardian: short(guardian) }))); + out.lines.push({ label: say("bg.multiversx.labelGuardian"), value: guardian }); + const w = msg("bg.multiversx.setGuardianWarn", { guardian: short(guardian) }); + out.warnings.push({ level: "danger", code: "account-takeover", message: w.fallback, msg: w }); + } else if (call && toSelf && call.fn === "GuardAccount" && call.args.length === 0) { + Object.assign(out, titled(msg("bg.multiversx.guardAccountTitle"))); + const w = msg("bg.multiversx.guardAccountWarn"); + out.warnings.push({ level: "danger", code: "account-takeover", message: w.fallback, msg: w }); + } else if (call && toSelf && call.fn === "UnGuardAccount" && call.args.length === 0) { + Object.assign(out, titled(msg("bg.multiversx.unguardAccountTitle"))); + } else if (call && toContract && call.fn === "ChangeOwnerAddress" && call.args.length === 1 && call.args[0]!.length === 32) { + const owner = encodeAddress(call.args[0]!); + Object.assign(out, titled(msg("bg.multiversx.changeOwnerTitle", { contract: short(tx.receiver), owner: short(owner) }))); + out.lines.push({ label: "Contract", value: tx.receiver }, { label: "Owner", value: owner }); + const w = msg("bg.multiversx.changeOwnerWarn", { contract: short(tx.receiver), owner: short(owner) }); + out.warnings.push({ level: "danger", code: "account-takeover", message: w.fallback, msg: w }); + } else if (call && toContract) { + contractCall(call.fn, tx.receiver, tx.value > 0n ? `${formatUnits(tx.value, EGLD_DECIMALS)} EGLD` : null); + if (tx.value > 0n) out.balanceChanges.push({ asset: egld, delta: (-tx.value).toString() }); + } else if (!toContract && !toSelf && textOf(tx.data) !== null) { + // EGLD to a person with a note (exchanges use the data field as a memo). + await describeTransfers([egldMoved(tx.value)], tx.receiver, null); + out.lines.push({ label: "Note", value: textOf(tx.data)! }); + } else { + out.blind = true; + } + + if (out.blind) { + out.title = say("bg.req.unreadableFrom", { host: d.host }); + delete out.titleMsg; + out.lines = [{ label: "To", value: tx.receiver }]; + out.balanceChanges = tx.value > 0n ? [{ asset: egld, delta: (-tx.value).toString() }] : []; + out.warnings = [{ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }]; + } + if (tx.relayer) { + out.lines.push({ label: "Fee paid by", value: tx.relayer }); + out.warnings.push({ level: "info", code: "network-matters", message: "Another account pays the network fee for this transaction." }); + } + if (tx.guardian) out.lines.push({ label: say("bg.multiversx.labelGuardian"), value: tx.guardian }); + const spec = specFor(d.networkId); + if (spec && tx.chainID !== spec.chainId) { + out.warnings.push({ level: "danger", code: "network-matters", message: "This transaction is for a different MultiversX network. Nothing was sent." }); + } + return out; +} + +/** The smart contract call after a token payment's own arguments ("…@@"), or null. */ +function rest(call: Call, used: number): Call | null { + if (call.args.length <= used) return null; + const fn = textOf(call.args[used]!); + if (!fn || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(fn)) return { fn: "unknown", args: call.args.slice(used + 1) }; + return { fn, args: call.args.slice(used + 1) }; +} diff --git a/packages/chains-multiversx/src/index.ts b/packages/chains-multiversx/src/index.ts new file mode 100644 index 00000000..bac1ee32 --- /dev/null +++ b/packages/chains-multiversx/src/index.ts @@ -0,0 +1,10 @@ +export { GatewayError, MultiversXClient, type NetworkConfig, plainMultiversXError } from "./api.js"; +export { type Described, type DescribeContext, describeTransaction, maxFee } from "./describe.js"; +export * from "./module.js"; +export * from "./networks.js"; +export { type Call, OPTION_GUARDED, OPTION_HASH_SIGN, type PlainTransaction, type Tx, bytesToSign, parseCall, parseTransaction, plainOf, serializeForSigning, signedPlain } from "./tx.js"; + +import { createMultiversXModule } from "./module.js"; + +/** Default instance. */ +export const multiversxModule = createMultiversXModule(); diff --git a/packages/chains-multiversx/src/module.ts b/packages/chains-multiversx/src/module.ts new file mode 100644 index 00000000..736c0ac4 --- /dev/null +++ b/packages/chains-multiversx/src/module.ts @@ -0,0 +1,421 @@ +import { + type AssetRef, + type BalanceChange, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + type Warning, + WALLET_ORIGIN, + msg, + say, + titled, +} from "@clip-wallet/core"; +import { ed25519 } from "@noble/curves/ed25519.js"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { GatewayError, MultiversXClient, type NetworkConfig, plainMultiversXError } from "./api.js"; +import { type Described, describeTransaction } from "./describe.js"; +import { EGLD_DECIMALS, MULTIVERSX_NETS, type TokenInfo, egldAsset, esdtAsset, isEsdtIdentifier, netOf, specFor } from "./networks.js"; +import { type Tx, bytesToSign, parseTransaction, plainOf, signedPlain } from "./tx.js"; +import { concat, decodeAddress, encodeAddress, formatUnits, hex, hostOf, numberArg, randomId, sleep, textOf, utf8 } from "./util.js"; + +/** + * Request methods: the MultiversX WalletConnect methods (mx-sdk-js-wallet-connect-provider `OPERATION`): + * `mvx_signTransaction` `{ transaction }` → `{ signature }`, `mvx_signTransactions` `{ transactions }` → + * `{ signatures: [{ signature }] }`, `mvx_signMessage` `{ message, address }` → `{ signature }`. Results also carry + * the signed plain transactions. `mvx_signAndSendTransactions` is Clip Wallet's own (same params as + * mvx_signTransactions; the wallet also sends them): buildTransfer uses it. + */ +export const MULTIVERSX_METHODS = { + signTransaction: "mvx_signTransaction", + signTransactions: "mvx_signTransactions", + signMessage: "mvx_signMessage", + signAndSendTransactions: "mvx_signAndSendTransactions", +} as const; + +/** sdk-core MESSAGE_PREFIX (core/constants.ts). */ +export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n"; + +/** Extra gas for an ESDTTransfer (sdk-core TransactionsFactoryConfig.gasLimitESDTTransfer + ADDITIONAL_GAS_FOR_ESDT_TRANSFER). */ +export const ESDT_TRANSFER_GAS = 200_000n + 100_000n; + +const MAX_TXS = 20; + +export interface MultiversXModuleOptions { + /** Dry-run single transactions in decode() through the gateway's /transaction/simulate (default true). */ + simulate?: boolean; + /** process-status polls after sending (default 10) and the wait between them (default 1500 ms). */ + confirmAttempts?: number; + confirmPollMs?: number; + sleep?: (ms: number) => Promise; +} + +export type Normalized = + | { kind: "txs"; txs: Tx[]; single: boolean; send: boolean } + | { kind: "message"; data: Uint8Array }; + +/** + * sdk-core MessageComputer.computeBytesForSigning: keccak256("\x17Elrond Signed Message:\n" ‖ decimal byte length ‖ + * message). The ed25519 signature covers these 32 bytes. + */ +export function messageHash(message: Uint8Array): Uint8Array { + return keccak_256(concat(utf8(MESSAGE_PREFIX), utf8(String(message.length)), message)); +} + +/** Checks a request against the connected account and network and parses it. */ +export function normalize(request: DappRequest, ctx: ChainContext): Normalized { + const spec = specFor(request.networkId); + if (!spec || request.networkId !== ctx.network.id) { + throw new ClipError("This app is asking for a different MultiversX network than the one it's connected to.", "multiversx/network-mismatch"); + } + const me = ctx.account.address; + const p = (request.params ?? {}) as Record; + if (typeof p !== "object") throw new ClipError("This request from the app is malformed, so we stopped it.", "multiversx/bad-params"); + if (typeof p.address === "string" && p.address && p.address !== me) { + throw new ClipError("This request is for a different account than the one you connected.", "multiversx/wrong-account"); + } + const checkTx = (raw: unknown): Tx => { + const tx = parseTransaction(raw); + if (tx.sender !== me) throw new ClipError("This request is for a different account than the one you connected.", "multiversx/wrong-account"); + if (tx.chainID !== spec.chainId) { + throw new ClipError("This app is asking for a different MultiversX network than the one it's connected to.", "multiversx/network-mismatch"); + } + return tx; + }; + switch (request.method) { + case MULTIVERSX_METHODS.signTransaction: + return { kind: "txs", txs: [checkTx(p.transaction)], single: true, send: false }; + case MULTIVERSX_METHODS.signTransactions: + case MULTIVERSX_METHODS.signAndSendTransactions: { + const list = p.transactions; + if (!Array.isArray(list) || list.length === 0 || list.length > MAX_TXS) throw new ClipError("This transaction can't be read.", "multiversx/bad-params"); + const txs = list.map(checkTx); + // Wallet-sent batches go out in nonce order, one after another. + if (request.method === MULTIVERSX_METHODS.signAndSendTransactions && request.origin !== WALLET_ORIGIN) { + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "multiversx/unsupported-method"); + } + return { kind: "txs", txs, single: false, send: request.method === MULTIVERSX_METHODS.signAndSendTransactions }; + } + case MULTIVERSX_METHODS.signMessage: { + if (typeof p.message !== "string") throw new ClipError("This request from the app is malformed, so we stopped it.", "multiversx/bad-params"); + return { kind: "message", data: utf8(p.message) }; + } + default: + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "multiversx/unsupported-method"); + } +} + +/** + * The site a MultiversX native-auth login token is for, or null. Token (sdk-native-auth-client `initialize`): + * base64url(origin) "." blockHash "." ttl "." base64url(extraInfo JSON); sdk-native-auth-server verifies the signature + * over the message `address + token` (MessageComputer). + */ +export function nativeAuthOrigin(token: string): string | null { + const parts = token.split("."); + if (parts.length !== 4 || !/^[0-9a-f]{64}$/i.test(parts[1]!) || !/^\d{1,10}$/.test(parts[2]!)) return null; + try { + const p = parts[0]!; + const b = atob(p.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((p.length + 3) % 4)); + const origin = new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(b, (c) => c.charCodeAt(0))); + return /^https?:\/\/[^\s/]+$/.test(origin) ? origin : null; + } catch { + return null; + } +} + +function payloadBytes(n: Normalized): Uint8Array[] { + return n.kind === "message" ? [messageHash(n.data)] : n.txs.map(bytesToSign); +} + +export interface MultiversXModule extends ChainModule { + normalize: typeof normalize; +} + +export function createMultiversXModule(options: MultiversXModuleOptions = {}): MultiversXModule { + const configs = new Map>(); + const tokenCache = new Map>(); + const wait = options.sleep ?? sleep; + const attempts = options.confirmAttempts ?? 10; + const pollMs = options.confirmPollMs ?? 1500; + + function clientFor(ctx: ChainContext): MultiversXClient { + const n = netOf(ctx.network.id); + const spec = n ? MULTIVERSX_NETS[n] : null; + const gateway = ctx.network.rpcUrls[0] ?? spec?.gateway; + const api = ctx.network.indexerUrl ?? spec?.api; + if (!gateway || !api) throw new ClipError("No connection is set up for this network.", "multiversx/no-rpc"); + return new MultiversXClient(gateway.replace(/\/$/, ""), api.replace(/\/$/, ""), ctx.fetch); + } + + function configFor(ctx: ChainContext, client: MultiversXClient): Promise { + let c = configs.get(ctx.network.id); + if (!c) { + c = client.networkConfig(); + configs.set(ctx.network.id, c); + c.catch(() => configs.delete(ctx.network.id)); + } + return c; + } + + function tokenInfo(ctx: ChainContext, client: MultiversXClient, identifier: string): Promise { + const key = `${ctx.network.id}:${identifier}`; + let t = tokenCache.get(key); + if (!t) { + t = client.token(identifier); + tokenCache.set(key, t); + } + return t; + } + + async function describeAll(req: DappRequest, ctx: ChainContext, txs: Tx[]): Promise { + const client = clientFor(ctx); + const config = await configFor(ctx, client); + const d = { networkId: ctx.network.id, me: ctx.account.address, host: hostOf(req.origin), config, client, token: (id: string) => tokenInfo(ctx, client, id) }; + const out: Described[] = []; + for (const tx of txs) out.push(await describeTransaction(tx, d)); + return out; + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const base = { requestId: req.id, networkId: req.networkId }; + const host = hostOf(req.origin); + if (n.kind === "message") { + const text = textOf(n.data); + const me = ctx.account.address; + const site = text?.startsWith(me) ? nativeAuthOrigin(text.slice(me.length)) : null; + if (site) { + // A native-auth login: address + token, the token naming the site it signs in to. + const domain = hostOf(site); + const warnings: Warning[] = []; + if (domain !== host) { + const w = msg("bg.warn.signInPhishing", { domain, host }); + warnings.push({ level: "danger", code: "domain-mismatch", message: w.fallback, msg: w }); + } + return { ...base, ...titled(msg("bg.req.signIn", { domain })), lines: [{ label: "Website", value: site }, { label: "Message", value: text! }], balanceChanges: [], simulated: false, blind: false, warnings }; + } + const warnings: Warning[] = text === null ? [{ level: "danger", code: "blind-signing", message: "This message isn't readable text. Only sign it if you trust the app." }] : []; + return { ...base, ...titled(msg("bg.req.signMessage", { host })), lines: [{ label: "Message", value: text ?? hex(n.data) }], balanceChanges: [], simulated: false, blind: text === null, warnings }; + } + const ds = await describeAll(req, ctx, n.txs); + const egld = egldAsset(ctx.network.id); + let simulated = false; + const simWarnings: Warning[] = []; + if ((options.simulate ?? true) && n.txs.length === 1 && !ds[0]!.blind) { + const sim = await clientFor(ctx).simulate(plainOf(n.txs[0]!)).catch(() => null); + if (sim?.ok) simulated = true; + else if (sim) { + simWarnings.push({ + level: "caution", + code: "simulation-failed", + message: /insufficient|not enough/i.test(sim.reason ?? "") + ? "A test run says you don't have enough EGLD for this and its network fee." + : "A test run of this transaction failed. It would probably fail and still cost the network fee.", + }); + } + } + const sponsored = n.txs.every((t) => !!t.relayer); + const fee = ds.reduce((a, d, i) => a + (n.txs[i]!.relayer ? 0n : d.fee), 0n); + const lines = ds.length === 1 ? [...ds[0]!.lines] : ds.map((d, i) => ({ label: say("bg.label.transactionN", { n: i + 1 }), value: d.title })); + lines.push({ label: "Network fee", value: `up to ${formatUnits(fee, EGLD_DECIMALS)} EGLD` }); + if (!n.send) lines.push({ label: "Sent by", value: `${host} (it gets the signed transaction)` }); + const head = ds.length === 1 ? { title: ds[0]!.title, ...(ds[0]!.titleMsg ? { titleMsg: ds[0]!.titleMsg } : {}) } : titled(msg("bg.req.approveCount", { count: ds.length })); + return { + ...base, + ...head, + lines, + balanceChanges: merge(ds.flatMap((d) => d.balanceChanges)), + fee: { asset: egld, amount: fee.toString(), ...(sponsored ? { sponsored: true } : {}) }, + blind: ds.some((d) => d.blind), + simulated, + warnings: [...ds.flatMap((d) => d.warnings), ...simWarnings], + }; + } + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + return payloadBytes(normalize(req, ctx)).map((bytes) => ({ accountId: ctx.account.id, scheme: "ed25519", bytes, approvalId })); + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const payloads = payloadBytes(n); + if (signatures.length !== payloads.length) throw new ClipError("Some signatures are missing. Nothing was sent.", "multiversx/bad-signature"); + const pub = decodeAddress(ctx.account.address); + if (!pub) throw new ClipError("That address isn't valid. Check it and try again.", "multiversx/bad-address"); + payloads.forEach((bytes, i) => { + const s = signatures[i]!; + if (s.scheme !== "ed25519" || s.bytes.length !== 64 || !ed25519.verify(s.bytes, bytes, pub)) { + throw new ClipError("The signature didn't match. Nothing was sent.", "multiversx/bad-signature"); + } + }); + if (n.kind === "message") return { signature: hex(signatures[0]!.bytes), address: ctx.account.address }; + const signed = n.txs.map((tx, i) => signedPlain(tx, signatures[i]!.bytes)); + if (n.single) return { signature: signed[0]!.signature, transaction: signed[0] }; + if (!n.send) return { signatures: signed.map((t) => ({ signature: t.signature })), transactions: signed }; + + const client = clientFor(ctx); + const hashes: string[] = []; + for (const tx of signed) { + let hash: string; + try { + hash = await client.send(tx); + } catch (e) { + if (e instanceof ClipError) throw e; + if (e instanceof GatewayError) throw new ClipError(plainMultiversXError(e.message), "multiversx/send-failed", e); + throw new ClipError("The MultiversX network rejected this transaction. Nothing was sent.", "multiversx/send-failed", e); + } + hashes.push(hash); + await confirm(client, hash); + } + return { txHash: hashes[0]!, txHashes: hashes, status: "success" }; + } + + /** Polls process-status. "fail"/"invalid" become plain errors; still pending after the last poll = accepted. */ + async function confirm(client: MultiversXClient, hash: string): Promise { + for (let k = 0; k < attempts; k++) { + await wait(pollMs); + const s = await client.processStatus(hash).catch(() => null); + if (!s || s.status === "pending" || s.status === "received") continue; + if (s.status === "success") return; + if (s.status === "fail" || s.status === "invalid") { + const reason = reasonText(s.reason); + throw new ClipError( + reason && /insufficient funds|not enough/i.test(reason) ? "The transaction failed: there wasn't enough to cover it. Only the network fee was spent." : "The transaction failed on MultiversX. Only the network fee was spent.", + "multiversx/transaction-failed", + s, + ); + } + } + } + + async function getBalances(ctx: ChainContext): Promise { + const client = clientFor(ctx); + const acct = await client.account(ctx.account.address); + const out: TokenBalance[] = [{ asset: egldAsset(ctx.network.id), amount: /^\d+$/.test(acct.balance) ? acct.balance : "0" }]; + const tokens = await client.tokens(ctx.account.address).catch(() => []); + for (const t of tokens) { + if (t.type && t.type !== "FungibleESDT") continue; + if (!t.identifier || !isEsdtIdentifier(t.identifier) || !/^\d+$/.test(t.balance ?? "")) continue; + out.push({ asset: esdtAsset(ctx.network.id, t), amount: t.balance! }); + } + return out; + } + + async function getNfts(): Promise { + // NFTs/SFTs are listed by the API (/accounts/{a}/nfts) but not shown yet. + return []; + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + const me = ctx.account.address; + const to = p.to.trim(); + if (!decodeAddress(to)) throw new ClipError("That address isn't valid. Check it and try again.", "multiversx/bad-address"); + if (to === me) throw new ClipError("That's your own address.", "multiversx/self-transfer"); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "multiversx/bad-amount"); + const amount = BigInt(p.amount); + const spec = specFor(ctx.network.id); + if (!spec) throw new ClipError("That network isn't available in this wallet.", "multiversx/unknown-network"); + const client = clientFor(ctx); + const config = await configFor(ctx, client); + if (config.chainId !== spec.chainId) throw new ClipError("This app is asking for a different MultiversX network than the one it's connected to.", "multiversx/network-mismatch"); + const [acct, guarded] = await Promise.all([client.account(me), client.isGuarded(me)]); + if (guarded) throw new ClipError(msg("bg.multiversx.guardedAccount"), "multiversx/guarded"); + const balance = /^\d+$/.test(acct.balance) ? BigInt(acct.balance) : 0n; + + let data = new Uint8Array(); + let value = amount; + let receiver = to; + let gasLimit = config.minGasLimit; + if (p.asset.address) { + const id = p.asset.address; + if (!isEsdtIdentifier(id)) throw new ClipError("That token couldn't be found.", "multiversx/unknown-token"); + const held = await client.esdtBalance(me, id); + if (held < amount) throw new ClipError(msg("bg.err.notEnough", { symbol: p.asset.symbol }), "multiversx/insufficient-token"); + data = utf8(`ESDTTransfer@${hex(utf8(id))}@${numberArg(amount)}`); + value = 0n; + receiver = to; + gasLimit = config.minGasLimit + config.gasPerDataByte * BigInt(data.length) + ESDT_TRANSFER_GAS; + } + const fee = gasLimit * config.minGasPrice; + if (balance < value + fee) { + throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: "EGLD" }), "multiversx/insufficient-funds"); + } + const tx: Tx = { + nonce: BigInt(acct.nonce), + value, + receiver, + sender: me, + senderUsername: new Uint8Array(), + receiverUsername: new Uint8Array(), + gasPrice: config.minGasPrice, + gasLimit, + data, + chainID: spec.chainId, + version: 2, + options: 0, + guardian: "", + relayer: "", + guardianSignature: "", + relayerSignature: "", + }; + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "multiversx", + networkId: ctx.network.id, + method: MULTIVERSX_METHODS.signAndSendTransactions, + params: { transactions: [plainOf(tx)] }, + }; + } + + return { + family: "multiversx", + curve: "ed25519", + /** xPortal / MultiversX DeFi Wallet / sdk-core Mnemonic.deriveKey(i): SLIP-10 ed25519, all hardened. */ + derivationPath: (index: number) => `m/44'/508'/0'/0'/${index}'`, + addressFromPublicKey(publicKey: Uint8Array, _network: Network): string { + if (publicKey.length !== 32) throw new Error("ed25519 public key must be 32 bytes"); + return encodeAddress(publicKey); + }, + /** "erd1…" bech32 with a valid checksum and a 32-byte key (accounts and contracts). */ + isAddress: (value: string) => decodeAddress(value.trim()) !== null, + /** The same address works on every MultiversX network. */ + networksForAddress: (value: string, candidates: Network[]) => (decodeAddress(value.trim()) ? candidates.filter((c) => c.family === "multiversx") : []), + getBalances, + getNfts, + decode, + prepare, + finalize, + buildTransfer, + normalize, + }; +} + +function merge(changes: BalanceChange[]): BalanceChange[] { + const by = new Map(); + for (const c of changes) { + const k = `${c.asset.key}:${c.asset.address ?? ""}`; + const prev = by.get(k); + by.set(k, prev ? { asset: prev.asset, delta: (BigInt(prev.delta) + BigInt(c.delta)).toString() } : c); + } + return [...by.values()].filter((c) => c.delta !== "0"); +} + +/** process-status reasons look like "@04@@…": the message as text, or the raw reason. */ +function reasonText(reason: string | undefined): string | null { + if (!reason) return null; + const parts = reason.split("@"); + const hexMsg = parts[2]; + if (hexMsg && /^([0-9a-f]{2})+$/i.test(hexMsg)) { + const bytes = Uint8Array.from(hexMsg.match(/../g)!.map((x) => parseInt(x, 16))); + return textOf(bytes) ?? reason; + } + return reason; +} diff --git a/packages/chains-multiversx/src/networks.ts b/packages/chains-multiversx/src/networks.ts new file mode 100644 index 00000000..c8883646 --- /dev/null +++ b/packages/chains-multiversx/src/networks.ts @@ -0,0 +1,153 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * MultiversX networks. Ids are the WalletConnect / CAIP-2 chain ids of the `mvx` namespace: "mvx:" + the chain's + * own chain ID ("1" mainnet, "D" devnet, "T" testnet), the `chainID` field every transaction carries. + * Gateway (proxy) and API endpoints are MultiversX's public ones, no key; each `GET /network/config` answered with + * erd_chain_id 1 / D / T, min gas price 1,000,000,000, min gas limit 50,000, 1,500 gas per data byte (2026-10-06). + */ +export type MultiversXNet = "devnet" | "testnet" | "mainnet"; + +export interface MultiversXNetSpec { + caip2: NetworkId; + /** The transaction `chainID`. */ + chainId: string; + /** Proxy (gateway): accounts, nonces, network config, /transaction/send, process-status. */ + gateway: string; + /** Indexer API: token lists with metadata, staking providers. */ + api: string; + explorer: string; + /** USDC (bridged from Ethereum as an ESDT; the issuer's own MultiversX USDC doesn't exist). */ + usdc: string; +} + +export const MULTIVERSX_NETS: Record = { + devnet: { + caip2: "mvx:D", + chainId: "D", + gateway: "https://devnet-gateway.multiversx.com", + api: "https://devnet-api.multiversx.com", + explorer: "https://devnet-explorer.multiversx.com", + // devnet-api /tokens?search=USDC: the one with registered assets ("bridged as an ESDT token"), 6 decimals. + usdc: "USDC-350c4e", + }, + testnet: { + caip2: "mvx:T", + chainId: "T", + gateway: "https://testnet-gateway.multiversx.com", + api: "https://testnet-api.multiversx.com", + explorer: "https://testnet-explorer.multiversx.com", + usdc: "", + }, + mainnet: { + caip2: "mvx:1", + chainId: "1", + gateway: "https://gateway.multiversx.com", + api: "https://api.multiversx.com", + explorer: "https://explorer.multiversx.com", + // api.multiversx.com /tokens/USDC-c76f1f: "WrappedUSDC", ticker USDC, 6 decimals. + usdc: "USDC-c76f1f", + }, +}; + +/** EGLD: 18 decimals. */ +export const EGLD_DECIMALS = 18; + +/** The token identifier MultiESDTNFTTransfer uses for EGLD inside a multi-transfer (sdk-core EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER). */ +export const EGLD_MULTI_ID = "EGLD-000000"; + +export function egldAsset(networkId: NetworkId): AssetRef { + return { key: "egld", symbol: "EGLD", name: "MultiversX eGold", decimals: EGLD_DECIMALS, networkId }; +} + +function network(net: MultiversXNet): Network { + const c = MULTIVERSX_NETS[net]; + return { + id: c.caip2, + family: "multiversx", + name: net === "mainnet" ? "MultiversX" : net === "devnet" ? "MultiversX Devnet" : "MultiversX Testnet", + nativeAsset: egldAsset(c.caip2), + testnet: net !== "mainnet", + rpcUrls: [c.gateway], + explorerUrl: c.explorer, + indexerUrl: c.api, + }; +} + +export const MULTIVERSX_DEVNET = network("devnet"); +export const MULTIVERSX_TESTNET = network("testnet"); +export const MULTIVERSX_MAINNET = network("mainnet"); +/** Devnet first: it's where MultiversX's faucet and dapps' test deployments are. */ +export const MULTIVERSX_NETWORKS: Network[] = [MULTIVERSX_DEVNET, MULTIVERSX_TESTNET, MULTIVERSX_MAINNET]; + +/** Accepts "mvx:D", "D", or a network name. */ +export function netOf(id: string): MultiversXNet | null { + for (const [name, c] of Object.entries(MULTIVERSX_NETS) as [MultiversXNet, MultiversXNetSpec][]) { + if (id === c.caip2 || id === c.chainId || id === name) return name; + } + return null; +} + +export function specFor(networkId: NetworkId): MultiversXNetSpec | null { + const n = netOf(networkId); + return n ? MULTIVERSX_NETS[n] : null; +} + +/** Ticker part of an ESDT identifier ("USDC-c76f1f" → "USDC"). */ +export function tickerOf(identifier: string): string { + const i = identifier.indexOf("-"); + return i > 0 ? identifier.slice(0, i) : identifier; +} + +/** ESDT identifiers: TICKER (3-10 upper-case letters/digits) + "-" + 6 lower-case hex characters. */ +export function isEsdtIdentifier(value: string): boolean { + return /^[A-Z0-9]{3,10}-[0-9a-f]{6}$/.test(value); +} + +export interface TokenInfo { + identifier: string; + ticker?: string; + name?: string; + decimals?: number; + /** Registered in the MultiversX assets repository (logo, description). */ + assets?: { status?: string; pngUrl?: string; svgUrl?: string } | null; + isVerified?: boolean; +} + +/** + * AssetRef for an ESDT. USDC (the known identifier) → key "usdc.mvx", bridged (it is Ethereum USDC moved over a + * bridge, so it never merges with Circle's native USDC). Others: "esdt:". A token is marked spam when + * it isn't registered in the MultiversX assets repository, or when it calls itself USDC / EGLD without being the + * real one. + */ +export function esdtAsset(networkId: NetworkId, info: TokenInfo): AssetRef { + const spec = specFor(networkId); + const isUsdc = !!spec?.usdc && info.identifier === spec.usdc; + const ticker = info.ticker || tickerOf(info.identifier); + const asset: AssetRef = { + key: isUsdc ? "usdc.mvx" : `esdt:${info.identifier}`, + symbol: isUsdc ? "USDC" : ticker, + name: isUsdc ? "USD Coin (bridged from Ethereum)" : info.name || ticker, + decimals: isUsdc ? 6 : (info.decimals ?? 0), + networkId, + address: info.identifier, + }; + if (isUsdc) { + asset.bridged = true; + return asset; + } + const logo = info.assets?.svgUrl ?? info.assets?.pngUrl; + if (logo) asset.logoUrl = logo; + const registered = !!info.assets && info.assets.status !== "inactive"; + if (!registered || looksLikeKnown(ticker) || looksLikeKnown(info.name ?? "")) asset.spam = true; + return asset; +} + +function looksLikeKnown(text: string): boolean { + const t = text.toUpperCase().replace(/[^A-Z0-9]/g, ""); + return t === "EGLD" || t.startsWith("USDC") || t === "USD" || t === "U5DC"; +} + +export function explorerTxUrl(net: Network, hash: string): string { + return `${net.explorerUrl}/transactions/${hash}`; +} diff --git a/packages/chains-multiversx/src/tx.ts b/packages/chains-multiversx/src/tx.ts new file mode 100644 index 00000000..73b768fb --- /dev/null +++ b/packages/chains-multiversx/src/tx.ts @@ -0,0 +1,210 @@ +import { ClipError } from "@clip-wallet/core"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { b64decode, b64encode, decodeAddress, fromHex, hex, utf8 } from "./util.js"; + +/** + * MultiversX transactions as sdk-core's `Transaction.toPlainObject()` / `newFromPlainObject()` spell them (the shape + * WalletConnect, the extension and web-wallet providers carry): numbers as JSON numbers, `value` as a decimal + * string, `data` and usernames base64, addresses bech32, signatures hex. + * https://github.com/multiversx/mx-sdk-js-core/blob/main/src/core/transactionComputer.ts (toPlainObject) + */ +export interface PlainTransaction { + nonce: number | string; + value: string; + receiver: string; + sender: string; + senderUsername?: string; + receiverUsername?: string; + gasPrice: number | string; + gasLimit: number | string; + data?: string; + chainID: string; + version: number; + options?: number; + guardian?: string; + relayer?: string; + signature?: string; + guardianSignature?: string; + relayerSignature?: string; +} + +/** A checked transaction. */ +export interface Tx { + nonce: bigint; + value: bigint; + receiver: string; + sender: string; + senderUsername: Uint8Array; + receiverUsername: Uint8Array; + gasPrice: bigint; + gasLimit: bigint; + data: Uint8Array; + chainID: string; + version: number; + options: number; + guardian: string; + relayer: string; + guardianSignature: string; + relayerSignature: string; +} + +/** options bit 0: the signature covers keccak256 of the serialized transaction (Ledger), bit 1: guarded transaction. */ +export const OPTION_HASH_SIGN = 0b01; +export const OPTION_GUARDED = 0b10; + +const MAX_U64 = (1n << 64n) - 1n; + +function bad(what: string): ClipError { + return new ClipError(`This transaction can't be read (${what}).`, "multiversx/bad-transaction"); +} + +function u64(v: unknown, what: string): bigint { + const s = typeof v === "number" ? (Number.isSafeInteger(v) ? String(v) : "") : typeof v === "string" ? v : typeof v === "bigint" ? v.toString() : ""; + if (!/^\d+$/.test(s)) throw bad(what); + const n = BigInt(s); + if (n > MAX_U64) throw bad(what); + return n; +} + +function addr(v: unknown, what: string, optional = false): string { + if (optional && (v === undefined || v === null || v === "")) return ""; + if (typeof v !== "string" || !decodeAddress(v)) throw bad(what); + return v; +} + +function b64(v: unknown, what: string): Uint8Array { + if (v === undefined || v === null || v === "") return new Uint8Array(); + if (typeof v !== "string") throw bad(what); + try { + return b64decode(v); + } catch { + throw bad(what); + } +} + +function sigHex(v: unknown, what: string): string { + if (v === undefined || v === null || v === "") return ""; + if (typeof v !== "string" || !/^[0-9a-f]{128}$/i.test(v)) throw bad(what); + return v.toLowerCase(); +} + +/** Checks a plain transaction from an app (or the wallet) and parses it. */ +export function parseTransaction(input: unknown): Tx { + if (!input || typeof input !== "object") throw bad("not an object"); + const p = input as Record; + const value = typeof p.value === "string" || typeof p.value === "number" ? String(p.value) : ""; + if (!/^\d+$/.test(value)) throw bad("value"); + const chainID = p.chainID ?? p.chainId; + if (typeof chainID !== "string" || !chainID) throw bad("chain"); + const version = p.version ?? 1; + if (typeof version !== "number" || !Number.isInteger(version) || version < 1 || version > 255) throw bad("version"); + const options = p.options ?? 0; + if (typeof options !== "number" || !Number.isInteger(options) || options < 0 || options > 255) throw bad("options"); + if (options && version < 2) throw bad("options need version 2"); + const tx: Tx = { + nonce: u64(p.nonce, "nonce"), + value: BigInt(value), + receiver: addr(p.receiver, "receiver"), + sender: addr(p.sender, "sender"), + senderUsername: b64(p.senderUsername, "sender username"), + receiverUsername: b64(p.receiverUsername, "receiver username"), + gasPrice: u64(p.gasPrice, "gas price"), + gasLimit: u64(p.gasLimit, "gas limit"), + data: b64(p.data, "data"), + chainID, + version, + options, + guardian: addr(p.guardian, "guardian", true), + relayer: addr(p.relayer, "relayer", true), + guardianSignature: sigHex(p.guardianSignature, "guardian signature"), + relayerSignature: sigHex(p.relayerSignature, "relayer signature"), + }; + if (tx.gasPrice === 0n || tx.gasLimit === 0n) throw bad("gas"); + return tx; +} + +/** Same field order and omissions as sdk-core TransactionComputer.toPlainObject (JSON.stringify drops `undefined`). */ +function fields(tx: Tx, signature?: string): [string, string][] { + const out: [string, string][] = [ + ["nonce", tx.nonce.toString()], + ["value", JSON.stringify(tx.value.toString())], + ["receiver", JSON.stringify(tx.receiver)], + ["sender", JSON.stringify(tx.sender)], + ]; + if (tx.senderUsername.length) out.push(["senderUsername", JSON.stringify(b64encode(tx.senderUsername))]); + if (tx.receiverUsername.length) out.push(["receiverUsername", JSON.stringify(b64encode(tx.receiverUsername))]); + out.push(["gasPrice", tx.gasPrice.toString()], ["gasLimit", tx.gasLimit.toString()]); + if (tx.data.length) out.push(["data", JSON.stringify(b64encode(tx.data))]); + if (signature) out.push(["signature", JSON.stringify(signature)]); + out.push(["chainID", JSON.stringify(tx.chainID)], ["version", String(tx.version)]); + if (tx.options) out.push(["options", String(tx.options)]); + if (tx.guardian) out.push(["guardian", JSON.stringify(tx.guardian)]); + if (tx.relayer) out.push(["relayer", JSON.stringify(tx.relayer)]); + return out; +} + +const json = (f: [string, string][]) => `{${f.map(([k, v]) => `${JSON.stringify(k)}:${v}`).join(",")}}`; + +/** TransactionComputer.computeBytesForSigning: UTF-8 of JSON.stringify(toPlainObject(tx)). */ +export function serializeForSigning(tx: Tx): Uint8Array { + return utf8(json(fields(tx))); +} + +/** + * What the account's ed25519 signature covers (TransactionComputer.computeBytesForVerifying): the serialized + * transaction, or its keccak256 when options bit 0 (hash signing) is set. + */ +export function bytesToSign(tx: Tx): Uint8Array { + const raw = serializeForSigning(tx); + return tx.options & OPTION_HASH_SIGN ? keccak_256(raw) : raw; +} + +/** The signed transaction as the gateway's POST /transaction/send takes it (and dapps get back). */ +export function signedPlain(tx: Tx, signature: Uint8Array): PlainTransaction { + const out: PlainTransaction = { + nonce: Number(tx.nonce), + value: tx.value.toString(), + receiver: tx.receiver, + sender: tx.sender, + gasPrice: Number(tx.gasPrice), + gasLimit: Number(tx.gasLimit), + chainID: tx.chainID, + version: tx.version, + signature: hex(signature), + }; + if (tx.senderUsername.length) out.senderUsername = b64encode(tx.senderUsername); + if (tx.receiverUsername.length) out.receiverUsername = b64encode(tx.receiverUsername); + if (tx.data.length) out.data = b64encode(tx.data); + if (tx.options) out.options = tx.options; + if (tx.guardian) out.guardian = tx.guardian; + if (tx.guardianSignature) out.guardianSignature = tx.guardianSignature; + if (tx.relayer) out.relayer = tx.relayer; + if (tx.relayerSignature) out.relayerSignature = tx.relayerSignature; + return out; +} + +/** The plain (unsigned) form of a wallet-built transaction. */ +export function plainOf(tx: Tx): PlainTransaction { + const { signature: _s, ...rest } = signedPlain(tx, new Uint8Array(64)); + return rest; +} + +/** A smart contract call / built-in function in the data field: "name@hexArg@hexArg…". */ +export interface Call { + fn: string; + args: Uint8Array[]; +} + +/** Parses "fn@aa@bb" (ASCII function name, even-length hex arguments; an empty argument is zero bytes), or null. */ +export function parseCall(data: Uint8Array): Call | null { + if (!data.length || data.some((b) => b < 0x20 || b > 0x7e)) return null; + const parts = new TextDecoder().decode(data).split("@"); + const fn = parts[0]!; + if (!/^[A-Za-z_][A-Za-z0-9_]{0,254}$/.test(fn)) return null; + const args: Uint8Array[] = []; + for (const a of parts.slice(1)) { + if (a.length % 2 || /[^0-9a-f]/i.test(a)) return null; + args.push(fromHex(a)); + } + return { fn, args }; +} diff --git a/packages/chains-multiversx/src/util.ts b/packages/chains-multiversx/src/util.ts new file mode 100644 index 00000000..2ebae08c --- /dev/null +++ b/packages/chains-multiversx/src/util.ts @@ -0,0 +1,131 @@ +import { bech32 } from "@scure/base"; + +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.replace(/^0x/i, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export function b64encode(bytes: Uint8Array): string { + let s = ""; + for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + return btoa(s); +} + +/** Strict standard base64 (what sdk-core's toPlainObject writes for `data` and usernames). */ +export function b64decode(text: string): Uint8Array { + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(text) || text.length % 4 === 1) throw new Error("bad base64"); + const s = atob(text); + const out = new Uint8Array(s.length); + for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); + return out; +} + +export const utf8 = (s: string) => new TextEncoder().encode(s); + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +/** The 32-byte public key behind an "erd1…" address (bech32 checksum checked), or null. */ +export function decodeAddress(value: string): Uint8Array | null { + try { + const { prefix, words } = bech32.decode(value as `${string}1${string}`); + if (prefix !== "erd") return null; + const bytes = bech32.fromWords(words); + return bytes.length === 32 ? Uint8Array.from(bytes) : null; + } catch { + return null; + } +} + +export function encodeAddress(publicKey: Uint8Array): string { + if (publicKey.length !== 32) throw new Error("MultiversX addresses are 32 bytes"); + return bech32.encode("erd", bech32.toWords(publicKey)); +} + +/** Smart contracts (user-deployed "erd1qqqqqqqqqqqqqpgq…" and system ones) start with 8 zero bytes. */ +export function isContractKey(key: Uint8Array): boolean { + return key.length === 32 && key.subarray(0, 8).every((b) => b === 0); +} + +/** System smart contracts (staking, ESDT, governance, delegation manager, staking providers): 00×9 ‖ 01 ‖ … */ +export function isSystemContractKey(key: Uint8Array): boolean { + return isContractKey(key) && key[8] === 0 && key[9] === 1; +} + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +export function short(address: string): string { + return address.length > 14 ? `${address.slice(0, 7)}…${address.slice(-4)}` : address; +} + +export function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +/** Big-endian unsigned bytes → bigint (empty = 0). */ +export function bigFromBytes(b: Uint8Array): bigint { + let n = 0n; + for (const x of b) n = (n << 8n) | BigInt(x); + return n; +} + +/** sdk-core's argument encoding of an unsigned number: minimal big-endian hex, even length ("" for 0 is never used: "00"). */ +export function numberArg(n: bigint): string { + if (n < 0n) throw new Error("negative"); + const h = n.toString(16); + return h.length % 2 ? `0${h}` : h; +} + +/** Printable text (no control characters except line breaks/tabs), or null. */ +export function textOf(bytes: Uint8Array): string | null { + if (!bytes.length) return null; + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + return /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s) ? null : s; + } catch { + return null; + } +} + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/packages/chains-multiversx/test/helpers.ts b/packages/chains-multiversx/test/helpers.ts new file mode 100644 index 00000000..486f4b1f --- /dev/null +++ b/packages/chains-multiversx/test/helpers.ts @@ -0,0 +1,79 @@ +import type { Account, ChainContext, Network, SignablePayload, Signature } from "@clip-wallet/core"; +import { ed25519 } from "@noble/curves/ed25519.js"; +import { MULTIVERSX_DEVNET } from "../src/index.js"; +import { fromHex, hex } from "../src/util.js"; +import { FIX } from "./signatures.js"; + +/** Vault stand-in: answers with a precomputed signature that verifies for the payload (verification only). */ +export function fixtureSigner(publicKeyHex: string, signatures: readonly string[]) { + const pub = fromHex(publicKeyHex); + const sigs = signatures.map(fromHex); + return { + sign(p: SignablePayload): Signature { + const sig = sigs.find((s) => ed25519.verify(s, p.bytes, pub)); + if (!sig) throw new Error("no fixture signature for this payload"); + return { scheme: "ed25519", bytes: sig, publicKey: hex(pub) }; + }, + }; +} + +export const signer = fixtureSigner(FIX.publicKey, [FIX.egldSig, FIX.usdcSig, FIX.messageSig]); + +export function makeAccount(): Account { + return { id: "multiversx:0", family: "multiversx", index: 0, curve: "ed25519", derivationPath: "m/44'/508'/0'/0'/0'", publicKey: FIX.publicKey, address: FIX.me }; +} + +export const GATEWAY = MULTIVERSX_DEVNET.rpcUrls[0]!; +export const API = MULTIVERSX_DEVNET.indexerUrl!; + +export interface Reply { + status: number; + body: unknown; +} +export const reply = (status: number, body: unknown): Reply => ({ status, body }); +const isReply = (v: unknown): v is Reply => !!v && typeof v === "object" && "status" in v && "body" in v && Object.keys(v).length === 2; +type Body = unknown | ((url: string, init?: RequestInit) => unknown); + +/** URL-routed mock fetch (first match wins). Handlers may return a Reply for a non-200 status. */ +export function mockFetch(routes: [RegExp, Body][]) { + const calls: { url: string; method: string; body?: string }[] = []; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + calls.push({ url, method: init?.method ?? "GET", body: init?.body as string | undefined }); + for (const [re, body] of routes) { + if (!re.test(url)) continue; + let v = typeof body === "function" ? (body as (u: string, i?: RequestInit) => unknown)(url, init) : body; + if (!isReply(v)) v = reply(200, v); + const r = v as Reply; + return new Response(JSON.stringify(r.body), { status: r.status }); + } + return new Response(JSON.stringify({ statusCode: 404, message: `no mock for ${url}` }), { status: 404 }); + }) as typeof fetch; + return { fetch: f, calls }; +} + +/** Gateway envelope. */ +export const gw = (data: unknown) => ({ data, error: "", code: "successful" }); + +/** Devnet /network/config as captured with curl (trimmed). */ +export const NETWORK_CONFIG = gw({ + config: { erd_chain_id: "D", erd_min_gas_price: 1000000000, erd_min_gas_limit: 50000, erd_gas_per_data_byte: 1500, erd_gas_price_modifier: "0.01", erd_min_transaction_version: 1 }, +}); + +export function baseRoutes(extra: [RegExp, Body][] = []): [RegExp, Body][] { + return [ + ...extra, + [/\/network\/config$/, NETWORK_CONFIG], + [new RegExp(`/address/${FIX.me}/guardian-data$`), gw({ guardianData: { guarded: false } })], + [new RegExp(`/address/${FIX.me}/esdt/USDC-350c4e$`), gw({ tokenData: { balance: "10000000", tokenIdentifier: "USDC-350c4e", properties: "" } })], + [new RegExp(`/address/${FIX.me}$`), gw({ account: { address: FIX.me, nonce: 7, balance: "5000000000000000000", username: "" } })], + [/\/tokens\/USDC-350c4e\?/, { identifier: "USDC-350c4e", name: "USDC", ticker: "USDC", decimals: 6, assets: { status: "active" } }], + [/\/tokens\/WEGLD-a28c59\?/, { identifier: "WEGLD-a28c59", name: "WrappedEGLD", ticker: "WEGLD", decimals: 18, assets: { status: "active" } }], + [/\/tokens\/USDC-fa4e01\?/, { identifier: "USDC-fa4e01", name: "USDC", ticker: "USDC", decimals: 6 }], + [/\/providers\/erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqq80llllsrepk69\?/, { identity: "castlestake" }], + ]; +} + +export function ctxFor(fetchImpl: typeof fetch, network: Network = MULTIVERSX_DEVNET): ChainContext { + return { network, account: makeAccount(), fetch: fetchImpl }; +} diff --git a/packages/chains-multiversx/test/multiversx.test.ts b/packages/chains-multiversx/test/multiversx.test.ts new file mode 100644 index 00000000..8ce3cbca --- /dev/null +++ b/packages/chains-multiversx/test/multiversx.test.ts @@ -0,0 +1,358 @@ +import { describe, expect, it } from "vitest"; +import { ClipError, type DappRequest, WALLET_ORIGIN } from "@clip-wallet/core"; +import { Address, Message, MessageComputer, Transaction, TransactionComputer, UserPublicKey } from "@multiversx/sdk-core"; +import { + MULTIVERSX_DEVNET, + MULTIVERSX_MAINNET, + MULTIVERSX_METHODS, + MULTIVERSX_NETWORKS, + MULTIVERSX_TESTNET, + type PlainTransaction, + bytesToSign, + createMultiversXModule, + esdtAsset, + messageHash, + parseCall, + parseTransaction, + plainMultiversXError, + serializeForSigning, +} from "../src/index.js"; +import { b64encode, encodeAddress, fromHex, hex, utf8 } from "../src/util.js"; +import { API, GATEWAY, baseRoutes, ctxFor, gw, makeAccount, mockFetch, reply, signer } from "./helpers.js"; +import { FIX } from "./signatures.js"; + +const module = createMultiversXModule({ confirmPollMs: 0, confirmAttempts: 3, sleep: async () => {} }); +const PROVIDER = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqq80llllsrepk69"; +const CONTRACT = "erd1qqqqqqqqqqqqqpgqd77fnev2sthnczp2lnfx0y5jdycynjfhzzgq6p3rax"; + +function tx(over: Partial = {}): PlainTransaction { + return { nonce: 7, value: "0", receiver: FIX.bob, sender: FIX.me, gasPrice: 1000000000, gasLimit: 500000, chainID: "D", version: 2, ...over }; +} +const data = (s: string) => b64encode(utf8(s)); +const hexOf = (s: string) => hex(utf8(s)); + +function req(method: string, params: unknown, over: Partial = {}): DappRequest { + return { id: "r1", origin: "https://app.example", via: "walletconnect", family: "multiversx", networkId: "mvx:D", method, params, ...over }; +} +const one = (t: PlainTransaction) => req(MULTIVERSX_METHODS.signTransaction, { transaction: t }); + +describe("networks and addresses", () => { + it("devnet, testnet and mainnet use the mvx namespace with the chain's own chain ID", () => { + expect(MULTIVERSX_NETWORKS.map((n) => n.id)).toEqual(["mvx:D", "mvx:T", "mvx:1"]); + expect(MULTIVERSX_DEVNET.testnet && MULTIVERSX_TESTNET.testnet && !MULTIVERSX_MAINNET.testnet).toBe(true); + expect(MULTIVERSX_MAINNET.rpcUrls[0]).toBe("https://gateway.multiversx.com"); + expect(MULTIVERSX_MAINNET.nativeAsset).toMatchObject({ key: "egld", symbol: "EGLD", decimals: 18 }); + }); + + it("derives the abandon-about address like sdk-core, and checks bech32 checksums", () => { + expect(module.derivationPath(0)).toBe("m/44'/508'/0'/0'/0'"); + expect(module.derivationPath(3)).toBe("m/44'/508'/0'/0'/3'"); + expect(module.curve).toBe("ed25519"); + const addr = module.addressFromPublicKey(fromHex(FIX.publicKey), MULTIVERSX_DEVNET); + expect(addr).toBe(FIX.me); + expect(new Address(fromHex(FIX.publicKey)).toBech32()).toBe(FIX.me); + expect(module.isAddress(FIX.me)).toBe(true); + expect(module.isAddress(FIX.me.slice(0, -1) + (FIX.me.endsWith("g") ? "h" : "g"))).toBe(false); + expect(module.isAddress("cosmos1sqhjrtmsn5yjk6w85099p8v0ly0g8z9pxeqe5dvu5rlf2n7vq3vq")).toBe(false); + expect(module.networksForAddress(FIX.me, MULTIVERSX_NETWORKS)).toHaveLength(3); + }); + + it("USDC is the known ESDT (bridged, own key); look-alikes and unregistered tokens are spam", () => { + expect(esdtAsset("mvx:1", { identifier: "USDC-c76f1f", decimals: 6 })).toMatchObject({ key: "usdc.mvx", symbol: "USDC", bridged: true, decimals: 6 }); + expect(esdtAsset("mvx:D", { identifier: "USDC-350c4e", decimals: 6 })).toMatchObject({ key: "usdc.mvx", bridged: true }); + expect(esdtAsset("mvx:1", { identifier: "USDC-350c4e", ticker: "USDC", decimals: 6, assets: { status: "active" } }).spam).toBe(true); + expect(esdtAsset("mvx:1", { identifier: "WEGLD-bd4d79", ticker: "WEGLD", decimals: 18, assets: { status: "active" } }).key).toBe("esdt:WEGLD-bd4d79"); + expect(esdtAsset("mvx:1", { identifier: "WEGLD-bd4d79", ticker: "WEGLD", decimals: 18, assets: { status: "active" } }).spam).toBeUndefined(); + expect(esdtAsset("mvx:1", { identifier: "FREE-123456", ticker: "FREE", decimals: 18 }).spam).toBe(true); + }); +}); + +describe("serialization (cross-checked with @multiversx/sdk-core)", () => { + const computer = new TransactionComputer(); + const cases: [string, PlainTransaction][] = [ + ["EGLD transfer", tx({ value: "1500000000000000000", gasLimit: 50000 })], + ["ESDT transfer", tx({ data: data(`ESDTTransfer@${hexOf("USDC-350c4e")}@2625a0`), gasLimit: 413000 })], + ["usernames, guardian, relayer, hash signing", tx({ senderUsername: data("alice"), receiverUsername: data("bob"), options: 3, guardian: CONTRACT, relayer: PROVIDER, data: data("hello") })], + ["version 1, note", tx({ version: 1, data: data("for the coffee ☕") })], + ]; + for (const [name, plain] of cases) { + it(name, () => { + const t = parseTransaction(plain); + const sdk = Transaction.newFromPlainObject(plain as never); + expect(hex(serializeForSigning(t))).toBe(hex(computer.computeBytesForSigning(sdk))); + expect(hex(bytesToSign(t))).toBe(hex(computer.computeBytesForVerifying(sdk))); + }); + } + + it("the wallet's own transfers serialize exactly as sdk-core would", async () => { + const f = mockFetch(baseRoutes()); + const r = await module.buildTransfer({ asset: MULTIVERSX_DEVNET.nativeAsset, to: FIX.bob, amount: "1500000000000000000" }, ctxFor(f.fetch)); + const plain = (r.params as { transactions: PlainTransaction[] }).transactions[0]!; + const [p] = await module.prepare(r, ctxFor(f.fetch), "ap"); + expect(hex(p!.bytes)).toBe(hex(computer.computeBytesForSigning(Transaction.newFromPlainObject(plain as never)))); + }); + + it("message hash matches MessageComputer, and the fixture signature verifies with sdk-core", async () => { + const m = new Message({ data: utf8(FIX.message) }); + expect(hex(messageHash(utf8(FIX.message)))).toBe(hex(new MessageComputer().computeBytesForSigning(m))); + expect(await new UserPublicKey(fromHex(FIX.publicKey)).verify(messageHash(utf8(FIX.message)), fromHex(FIX.messageSig))).toBe(true); + }); + + it("refuses malformed fields", () => { + expect(() => parseTransaction(tx({ value: "-1" }))).toThrow(ClipError); + expect(() => parseTransaction(tx({ receiver: "erd1nope" }))).toThrow(ClipError); + expect(() => parseTransaction(tx({ version: 1, options: 1 }))).toThrow(ClipError); + expect(() => parseTransaction(tx({ data: "not base64!" }))).toThrow(ClipError); + expect(parseCall(utf8("ESDTTransfer@0a@zz"))).toBeNull(); + expect(parseCall(utf8("delegate"))).toEqual({ fn: "delegate", args: [] }); + }); +}); + +describe("decode", () => { + const f = mockFetch(baseRoutes()); + const ctx = ctxFor(f.fetch); + + it("an EGLD transfer", async () => { + const d = await module.decode(one(tx({ value: "1500000000000000000", gasLimit: 50000 })), ctx); + expect(d.title).toBe(`Send 1.5 EGLD to ${FIX.bob.slice(0, 7)}…${FIX.bob.slice(-4)}`); + expect(d.titleMsg?.id).toBe("bg.req.sendTo"); + expect(d.blind).toBe(false); + expect(d.balanceChanges).toEqual([{ asset: MULTIVERSX_DEVNET.nativeAsset, delta: "-1500000000000000000" }]); + expect(d.fee).toMatchObject({ amount: "50000000000000" }); + expect(d.lines).toContainEqual({ label: "Network fee", value: "up to 0.00005 EGLD" }); + expect(d.lines).toContainEqual({ label: "Sent by", value: "app.example (it gets the signed transaction)" }); + }); + + it("an ESDTTransfer of USDC", async () => { + const d = await module.decode(one(tx({ data: data(`ESDTTransfer@${hexOf("USDC-350c4e")}@2625a0`), gasLimit: 413000 })), ctx); + expect(d.title).toMatch(/^Send 2\.5 USDC to erd1sxm/); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "usdc.mvx", address: "USDC-350c4e" }), delta: "-2500000" }]); + // fee: 113,000 gas to move the data at full price + 300,000 × 1% (gas price modifier) + expect(d.fee?.amount).toBe(String(113000n * 1000000000n + 300000n * 10000000n)); + }); + + it("a look-alike USDC is flagged", async () => { + const d = await module.decode(one(tx({ data: data(`ESDTTransfer@${hexOf("USDC-fa4e01")}@2625a0`) })), ctx); + expect(d.warnings.some((w) => w.code === "known-scam" && w.level === "danger")).toBe(true); + }); + + it("a MultiESDTNFTTransfer of EGLD + WEGLD + an NFT", async () => { + const dest = hex(fromHex("80" + "00".repeat(31))); + const d = await module.decode( + one(tx({ receiver: FIX.me, data: data(`MultiESDTNFTTransfer@${dest}@03@${hexOf("EGLD-000000")}@@0de0b6b3a7640000@${hexOf("WEGLD-a28c59")}@@0de0b6b3a7640000@${hexOf("NICENFT-abcdef")}@05@01`) })), + ctx, + ); + expect(d.blind).toBe(false); + expect(d.title).toContain("1 EGLD + 1 WEGLD + 1 × NICENFT #5"); + expect(d.lines).toContainEqual({ label: "To", value: encodeAddress(fromHex(dest)) }); + expect(d.balanceChanges.map((c) => c.asset.symbol).sort()).toEqual(["EGLD", "WEGLD"]); + }); + + it("delegation: stake, unstake, claim, withdraw, restake in plain words", async () => { + const shortP = `${PROVIDER.slice(0, 7)}…${PROVIDER.slice(-4)}`; + expect(shortP).toBeTruthy(); + const stake = await module.decode(one(tx({ receiver: PROVIDER, value: "1000000000000000000", data: data("delegate"), gasLimit: 12000000 })), ctx); + expect(stake.title).toBe("Stake 1 EGLD with castlestake"); + expect(stake.titleMsg?.id).toBe("bg.req.stakeWith"); + expect(stake.balanceChanges[0]?.delta).toBe("-1000000000000000000"); + const un = await module.decode(one(tx({ receiver: PROVIDER, data: data("unDelegate@0de0b6b3a7640000"), gasLimit: 12000000 })), ctx); + expect(un.title).toBe("Unstake 1 EGLD from castlestake"); + expect((await module.decode(one(tx({ receiver: PROVIDER, data: data("claimRewards"), gasLimit: 6000000 })), ctx)).title).toBe("Claim your staking rewards from castlestake"); + expect((await module.decode(one(tx({ receiver: PROVIDER, data: data("withdraw"), gasLimit: 12000000 })), ctx)).title).toBe("Withdraw your unstaked EGLD from castlestake"); + expect((await module.decode(one(tx({ receiver: PROVIDER, data: data("reDelegateRewards"), gasLimit: 12000000 })), ctx)).title).toBe("Restake your rewards with castlestake"); + }); + + it("SetGuardian, GuardAccount and ChangeOwnerAddress are dangers", async () => { + const g = await module.decode(one(tx({ receiver: FIX.me, data: data(`SetGuardian@${hex(fromHex("11".repeat(32)))}@${hexOf("ServiceID")}`) })), ctx); + expect(g.titleMsg?.id).toBe("bg.multiversx.setGuardianTitle"); + expect(g.warnings).toContainEqual(expect.objectContaining({ level: "danger", code: "account-takeover" })); + const ga = await module.decode(one(tx({ receiver: FIX.me, data: data("GuardAccount") })), ctx); + expect(ga.warnings[0]).toMatchObject({ level: "danger", code: "account-takeover" }); + const o = await module.decode(one(tx({ receiver: CONTRACT, data: data(`ChangeOwnerAddress@${hex(fromHex("22".repeat(32)))}`) })), ctx); + expect(o.title).toMatch(/^Give contract erd1qqq…3rax to /); + expect(o.warnings[0]).toMatchObject({ level: "danger", code: "account-takeover" }); + }); + + it("another smart contract call is named with a caution", async () => { + const d = await module.decode(one(tx({ receiver: CONTRACT, value: "100000000000000000", data: data("swapTokensFixedInput@0a@0b") })), ctx); + expect(d.title).toBe("Approve swapTokensFixedInput on contract erd1qqq…3rax"); + expect(d.blind).toBe(false); + expect(d.warnings).toContainEqual(expect.objectContaining({ level: "caution", code: "unknown-call" })); + expect(d.balanceChanges[0]?.delta).toBe("-100000000000000000"); + }); + + it("a note to a person is shown, not blind", async () => { + const d = await module.decode(one(tx({ value: "1", data: data("invoice 42") })), ctx); + expect(d.blind).toBe(false); + expect(d.lines).toContainEqual({ label: "Note", value: "invoice 42" }); + }); + + it("undecodable data to a contract is blind", async () => { + const d = await module.decode(one(tx({ receiver: CONTRACT, data: b64encode(Uint8Array.of(0, 1, 2, 255)) })), ctx); + expect(d.blind).toBe(true); + expect(d.warnings[0]).toMatchObject({ level: "danger", code: "blind-signing" }); + }); + + it("several transactions → one approval", async () => { + const d = await module.decode(req(MULTIVERSX_METHODS.signTransactions, { transactions: [tx({ value: "1" }), tx({ nonce: 8, value: "2" })] }), ctx); + expect(d.title).toBe("Approve 2 transactions"); + expect(d.balanceChanges[0]?.delta).toBe("-3"); + }); + + it("a relayed transaction's fee is paid by the relayer", async () => { + const d = await module.decode(one(tx({ value: "1", relayer: PROVIDER, version: 2 })), ctx); + expect(d.fee).toMatchObject({ amount: "0", sponsored: true }); + expect(d.lines).toContainEqual({ label: "Fee paid by", value: PROVIDER }); + }); + + it("a dry run through the gateway: success marks it simulated, a failure warns in plain words", async () => { + const sims: string[] = []; + const ok = mockFetch(baseRoutes([[/\/transaction\/simulate\?checkSignature=false$/, (_u: string, init?: RequestInit) => (sims.push(String(init?.body)), gw({ result: { status: "success", hash: "aa" } }))]])); + const d = await module.decode(one(tx({ value: "1", gasLimit: 50000 })), ctxFor(ok.fetch)); + expect(d.simulated).toBe(true); + expect(JSON.parse(sims[0]!)).toMatchObject({ sender: FIX.me, signature: "00".repeat(64) }); + const failing = mockFetch( + baseRoutes([[/\/transaction\/simulate/, gw({ result: { receiverShard: { status: "success" }, senderShard: { status: "fail", failReason: "insufficient balance for fees, has: 0, wanted: 50000000000000" } } })]]), + ); + const f2 = await module.decode(one(tx({ value: "1", gasLimit: 50000 })), ctxFor(failing.fetch)); + expect(f2.simulated).toBe(false); + expect(f2.warnings).toContainEqual({ level: "caution", code: "simulation-failed", message: "A test run says you don't have enough EGLD for this and its network fee." }); + }); + + it("a message", async () => { + const d = await module.decode(req(MULTIVERSX_METHODS.signMessage, { message: FIX.message, address: FIX.me }), ctx); + expect(d.title).toBe("Sign a message for app.example"); + expect(d.lines).toEqual([{ label: "Message", value: FIX.message }]); + }); + + it("a native-auth login is a sign-in to the token's site; another site's token is a danger", async () => { + const b64url = (t: string) => btoa(t).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); + const token = (origin: string) => `${b64url(origin)}.${"ab".repeat(32)}.86400.${b64url("{}")}`; + const mine = await module.decode(req(MULTIVERSX_METHODS.signMessage, { message: FIX.me + token("https://app.example"), address: FIX.me }), ctx); + expect(mine.title).toBe("Sign in to app.example"); + expect(mine.titleMsg?.id).toBe("bg.req.signIn"); + expect(mine.warnings).toEqual([]); + expect(mine.lines[0]).toEqual({ label: "Website", value: "https://app.example" }); + const other = await module.decode(req(MULTIVERSX_METHODS.signMessage, { message: FIX.me + token("https://xexchange.com"), address: FIX.me }), ctx); + expect(other.title).toBe("Sign in to xexchange.com"); + expect(other.warnings[0]).toMatchObject({ level: "danger", code: "domain-mismatch", msg: { id: "bg.warn.signInPhishing" } }); + }); + + it("refuses the wrong network, account or method", async () => { + await expect(module.decode(one(tx({ chainID: "1" })), ctx)).rejects.toMatchObject({ code: "multiversx/network-mismatch" }); + await expect(module.decode(one(tx()), ctxFor(f.fetch, MULTIVERSX_MAINNET))).rejects.toMatchObject({ code: "multiversx/network-mismatch" }); + await expect(module.decode(one(tx({ sender: FIX.bob })), ctx)).rejects.toMatchObject({ code: "multiversx/wrong-account" }); + await expect(module.decode(req(MULTIVERSX_METHODS.signMessage, { message: "x", address: FIX.bob }), ctx)).rejects.toMatchObject({ code: "multiversx/wrong-account" }); + await expect(module.decode(req("mvx_cancelAction", {}), ctx)).rejects.toMatchObject({ code: "multiversx/unsupported-method" }); + // Only the wallet itself may ask it to send. + await expect(module.decode(req(MULTIVERSX_METHODS.signAndSendTransactions, { transactions: [tx()] }), ctx)).rejects.toMatchObject({ code: "multiversx/unsupported-method" }); + }); +}); + +describe("send (buildTransfer → decode → prepare → finalize)", () => { + function sendRoutes(status: string, sent: { body?: string }[]) { + return baseRoutes([ + [/\/transaction\/send$/, (_u: string, init?: RequestInit) => (sent.push({ body: String(init?.body) }), gw({ txHash: "ab".repeat(32) }))], + [/\/process-status$/, gw({ status, reason: status === "fail" ? `@04@${hexOf("insufficient funds")}` : "" })], + ]); + } + + it("EGLD: builds, decodes, signs, verifies and sends", async () => { + const sent: { body?: string }[] = []; + const f = mockFetch(sendRoutes("success", sent)); + const ctx = ctxFor(f.fetch); + const r = await module.buildTransfer({ asset: MULTIVERSX_DEVNET.nativeAsset, to: FIX.bob, amount: "1500000000000000000" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, method: MULTIVERSX_METHODS.signAndSendTransactions, networkId: "mvx:D" }); + const d = await module.decode(r, ctx); + expect(d.title).toMatch(/^Send 1\.5 EGLD to/); + expect(d.lines.some((l) => l.label === "Sent by")).toBe(false); + const payloads = await module.prepare(r, ctx, "ap1"); + const out = await module.finalize(r, payloads.map((p) => signer.sign(p)), ctx); + expect(out).toEqual({ txHash: "ab".repeat(32), txHashes: ["ab".repeat(32)], status: "success" }); + expect(JSON.parse(sent[0]!.body!)).toMatchObject({ nonce: 7, value: "1500000000000000000", signature: FIX.egldSig, chainID: "D", version: 2, gasLimit: 50000 }); + }); + + it("USDC: ESDTTransfer with sdk-core's gas", async () => { + const sent: { body?: string }[] = []; + const f = mockFetch(sendRoutes("success", sent)); + const ctx = ctxFor(f.fetch); + const usdc = esdtAsset("mvx:D", { identifier: "USDC-350c4e", decimals: 6 }); + const r = await module.buildTransfer({ asset: usdc, to: FIX.bob, amount: "2500000" }, ctx); + const payloads = await module.prepare(r, ctx, "ap2"); + await module.finalize(r, payloads.map((p) => signer.sign(p)), ctx); + const body = JSON.parse(sent[0]!.body!) as PlainTransaction; + expect(body).toMatchObject({ value: "0", gasLimit: 413000, signature: FIX.usdcSig }); + expect(new TextDecoder().decode(Uint8Array.from(atob(body.data!), (c) => c.charCodeAt(0)))).toBe(`ESDTTransfer@${hexOf("USDC-350c4e")}@2625a0`); + }); + + it("a bad signature is refused before anything is sent", async () => { + const sent: { body?: string }[] = []; + const f = mockFetch(sendRoutes("success", sent)); + const ctx = ctxFor(f.fetch); + const r = await module.buildTransfer({ asset: MULTIVERSX_DEVNET.nativeAsset, to: FIX.bob, amount: "1500000000000000000" }, ctx); + const good = signer.sign((await module.prepare(r, ctx, "ap3"))[0]!); + const bad = { ...good, bytes: Uint8Array.from(good.bytes, (b, i) => (i === 0 ? b ^ 1 : b)) }; + await expect(module.finalize(r, [bad], ctx)).rejects.toMatchObject({ code: "multiversx/bad-signature" }); + await expect(module.finalize(r, [], ctx)).rejects.toMatchObject({ code: "multiversx/bad-signature" }); + expect(sent).toHaveLength(0); + expect(f.calls.some((c) => c.method === "POST")).toBe(false); + }); + + it("a failed transaction and a gateway rejection come back in plain words", async () => { + const f = mockFetch(sendRoutes("fail", [])); + const ctx = ctxFor(f.fetch); + const r = await module.buildTransfer({ asset: MULTIVERSX_DEVNET.nativeAsset, to: FIX.bob, amount: "1500000000000000000" }, ctx); + const sigs = (await module.prepare(r, ctx, "ap4")).map((p) => signer.sign(p)); + await expect(module.finalize(r, sigs, ctx)).rejects.toMatchObject({ code: "multiversx/transaction-failed", userMessage: "The transaction failed: there wasn't enough to cover it. Only the network fee was spent." }); + const g = mockFetch(baseRoutes([[/\/transaction\/send$/, reply(400, { data: null, error: "transaction generation failed: lowerNonceInTx: true, veryHighNonceInTx: false", code: "bad_request" })]])); + await expect(module.finalize(r, sigs, ctxFor(g.fetch))).rejects.toMatchObject({ code: "multiversx/send-failed", userMessage: plainMultiversXError("lowerNonceInTx: true") }); + }); + + it("refuses sends the network would reject", async () => { + const f = mockFetch(baseRoutes()); + const ctx = ctxFor(f.fetch); + const egld = MULTIVERSX_DEVNET.nativeAsset; + await expect(module.buildTransfer({ asset: egld, to: FIX.me, amount: "1" }, ctx)).rejects.toMatchObject({ code: "multiversx/self-transfer" }); + await expect(module.buildTransfer({ asset: egld, to: "erd1xyz", amount: "1" }, ctx)).rejects.toMatchObject({ code: "multiversx/bad-address" }); + await expect(module.buildTransfer({ asset: egld, to: FIX.bob, amount: "0" }, ctx)).rejects.toMatchObject({ code: "multiversx/bad-amount" }); + await expect(module.buildTransfer({ asset: egld, to: FIX.bob, amount: "5000000000000000000" }, ctx)).rejects.toMatchObject({ code: "multiversx/insufficient-funds" }); + const usdc = esdtAsset("mvx:D", { identifier: "USDC-350c4e", decimals: 6 }); + await expect(module.buildTransfer({ asset: usdc, to: FIX.bob, amount: "10000001" }, ctx)).rejects.toMatchObject({ code: "multiversx/insufficient-token" }); + const guarded = mockFetch(baseRoutes([[new RegExp(`/address/${FIX.me}/guardian-data$`), gw({ guardianData: { guarded: true } })]])); + await expect(module.buildTransfer({ asset: egld, to: FIX.bob, amount: "1" }, ctxFor(guarded.fetch))).rejects.toMatchObject({ code: "multiversx/guarded" }); + }); + + it("signs messages and returns the hex signature", async () => { + const f = mockFetch(baseRoutes()); + const ctx = ctxFor(f.fetch); + const r = req(MULTIVERSX_METHODS.signMessage, { message: FIX.message, address: FIX.me }); + const payloads = await module.prepare(r, ctx, "ap5"); + expect(payloads[0]!.bytes).toHaveLength(32); + expect(await module.finalize(r, payloads.map((p) => signer.sign(p)), ctx)).toEqual({ signature: FIX.messageSig, address: FIX.me }); + }); +}); + +describe("balances", () => { + it("EGLD from the gateway, fungible ESDTs from the API with spam marked", async () => { + const f = mockFetch( + baseRoutes([ + [ + new RegExp(`${API}/accounts/${FIX.me}/tokens`), + [ + { identifier: "USDC-350c4e", ticker: "USDC", decimals: 6, balance: "2500000", type: "FungibleESDT", assets: { status: "active" } }, + { identifier: "USDC-fa4e01", ticker: "USDC", name: "USDC", decimals: 6, balance: "999", type: "FungibleESDT" }, + { identifier: "MEX-a659d0", ticker: "MEX", decimals: 18, balance: "7", type: "MetaESDT" }, + ], + ], + ]), + ); + const b = await module.getBalances(ctxFor(f.fetch)); + expect(f.calls[0]!.url.startsWith(GATEWAY) || f.calls[0]!.url.startsWith(API)).toBe(true); + expect(b.map((x) => [x.asset.key, x.amount, !!x.asset.spam])).toEqual([ + ["egld", "5000000000000000000", false], + ["usdc.mvx", "2500000", false], + ["esdt:USDC-fa4e01", "999", true], + ]); + expect(makeAccount().address).toBe(FIX.me); + }); +}); diff --git a/packages/chains-multiversx/test/signatures.ts b/packages/chains-multiversx/test/signatures.ts new file mode 100644 index 00000000..648b4242 --- /dev/null +++ b/packages/chains-multiversx/test/signatures.ts @@ -0,0 +1,21 @@ +/** + * Signature fixtures, computed once OFFLINE (outside the repo) from the public BIP-39 test vector ("abandon" ×11 + + * "about", empty passphrase), SLIP-10 ed25519 at m/44'/508'/0'/0'/0' (derivationPath(0)), with the vault's own + * derive + sign code. The address equals sdk-core `Mnemonic.fromString(…).deriveKey(0)` (packages/vault + * test/families87.test.ts checks that). Only the public key, addresses, payloads and signatures live here. + * + * - egld: buildTransfer of 1.5 EGLD me → bob on devnet, nonce 7, gas 50,000 × 1 gwei (the signed bytes are + * sdk-core computeBytesForSigning of that transaction) + * - usdc: buildTransfer of 2.5 USDC (USDC-350c4e) me → bob, "ESDTTransfer@…@2625a0", gas 413,000 + * - message: mvx_signMessage "Sign in to app.example" (signature over the 32-byte MessageComputer hash) + * bob is bech32("erd", sha256("bob")): nobody holds its key. + */ +export const FIX = { + publicKey: "802f21af709d092b69c7a3ca509d8ff91e8388a136419a359ca0fe954fcc0458", + me: "erd1sqhjrtmsn5yjk6w85099p8v0ly0g8z9pxeqe5dvu5rlf2n7vq3vqytny9g", + bob: "erd1sxmr0k8u6trd5c6eu6trzyapzux7090ykujmsng7pdx0m8k93n5s48kwzk", + egldSig: "254d766ce178bf8683ef3d591a522878657f2a7d483543cafe4f1139988a242a7d9aaf052d7be1f0c41603cbbbb3de33857d97f335b172572e04029516f5f70e", + usdcSig: "d214629a26b044b6460502069692a495c08b89ed40efa9053475a01eab161af66987569814c5267d8563e643045f54ddf2f3d1bfedfdc00bb2cbc7a1e2269604", + message: "Sign in to app.example", + messageSig: "e113110bdab522e665b3ec0c6b164aa3ba65bd569ce2c92c6636167273d2493929655a9ca39505b5b8130e248ba0b6dd0df85b8058928b2876893a5eacee9306", +} as const; diff --git a/packages/chains-multiversx/tsconfig.json b/packages/chains-multiversx/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-multiversx/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-stacks/LICENSE b/packages/chains-stacks/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-stacks/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-stacks/NOTICE b/packages/chains-stacks/NOTICE new file mode 100644 index 00000000..a45ce809 --- /dev/null +++ b/packages/chains-stacks/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-stacks +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-stacks/README.md b/packages/chains-stacks/README.md new file mode 100644 index 00000000..ce2a83a1 --- /dev/null +++ b/packages/chains-stacks/README.md @@ -0,0 +1,115 @@ +# @clip-wallet/chains-stacks + +Stacks `ChainModule` for Clip Wallet. It builds and decodes Stacks transactions in plain words and never touches +keys. `prepare()` returns the 32-byte digest the account's key signs; `finalize()` checks the vault's signature with +`secp256k1.verify`, works out the recovery id from the account's key, puts the signature into the transaction and +broadcasts when the method asks for it. + +Everything is hand-written from SIP-005 with `@noble/hashes` (SHA-512/256, SHA-256, RIPEMD-160) and `@noble/curves` +(verification only): c32check addresses, Clarity values, post-conditions, payloads, the transaction wire format and +the signing hashes. `@stacks/transactions` and `@stacks/encryption` 7.6 are dev dependencies only: the tests check +our bytes, txids and digests against them, and check our signed transactions with stacks.js `verifyOrigin()`. + +## Networks + +| NetworkId (CAIP-2) | chain id | tx version | API (`rpcUrls[0]`) | explorer | +|---|---|---|---|---| +| `stacks:2147483648` | `0x80000000` | `0x80` | https://api.testnet.hiro.so | https://explorer.hiro.so/?chain=testnet | +| `stacks:1` | `0x00000001` | `0x00` | https://api.hiro.so | https://explorer.hiro.so | + +- The ids come from the ChainAgnostic `stacks` namespace (`stacks/caip2.md`, test cases `stacks:1` and + `stacks:2147483648`), which is the `network_id` of `GET /v2/info`. Both were checked against Hiro (2026-10). +- Hiro's API is used without a key: `/v2/accounts`, `/extended/v1/address/{a}/nonces` (falls back to the node's + account nonce), `/extended/v1/address/{a}/balances`, `/metadata/v1/ft/{contract}`, `POST /v2/fees/transaction` + (falls back to `/v2/fees/transfer` × length), `POST /v2/transactions` and `/extended/v1/tx/{id}`. +- Testnet faucet: `POST https://api.testnet.hiro.so/extended/v1/faucets/stx?address=ST…` (no key, no captcha; + rate-limited per address/IP). It sends 500 testnet STX. + +Assets: + +- STX is `stx` (6 decimals). +- Curated SIP-010 tokens (`STACKS_NETS[n].tokens`): sBTC (`sbtc`, mainnet `SM3VDXK3…JFQ4.sbtc-token::sbtc-token`, + docs.stacks.co "Deployed Mainnet Contracts"; no official testnet deployment, only mocks), Circle's USDCx through + xReserve (`usdcx`, mainnet `SP120SBR…2CNE.usdcx::usdcx-token`, testnet `ST1PQHQK…PGZGM.usdcx::usdcx-token`, + docs.stacks.co/learn/bridging/usdcx/contracts) and Allbridge's aeUSDC (`aeusdc`, mainnet). All three are + `bridged: true` with their own key: none is the issuer's native token. +- Other tokens are `sip10:::` with metadata from Hiro. One named like a curated token, STX or a + dollar (`USDC`, `sBTC`, `STX`, …) is `spam`. +- `stxBalance(ctx)` gives total, locked (stacking) and spendable µSTX. `getNfts` returns `[]`. + +## Accounts and addresses + +- `derivationPath(i)` = `m/44'/5757'/0'/0/i` (Leather, Xverse, `@stacks/wallet-sdk` account i), secp256k1. +- The address is P2PKH (`hash160` of the compressed key) spelled per network: `SP…` on mainnet, `ST…` on testnet. + `Account.address` is the mainnet form; `addressFromPublicKey(key, network)` and `receiveAddress(ctx)` give the + network's spelling. Fixtures: "abandon … about" → `SPC5KHM4…SH54J` / `STC5KHM4…330BQ`, as stacks.js derives them. +- `isAddress` accepts standard (`SP/ST/SM/SN`) and contract (`SP….name`) principals, checksum verified. + `networksForAddress` maps SP/SM to mainnet and ST/SN to testnet. + +## Methods (`STACKS_METHODS`) + +The SIP-030 names (https://github.com/stacksgov/sips/blob/main/sips/sip-030/sip-030-wallet-interface.md), which +`@stacks/connect` v8 `request()` sends. Clarity values and post-conditions arrive as SIP-005 hex (what Connect's +`serializeParams` sends) or as the SIP-030 JSON form. + +| method | params | result | +|---|---|---| +| `stx_transferStx` | `{ recipient, amount, memo?, fee?, nonce?, sponsored?, broadcast? = true, network?, address? }` | `{ txid, transaction }` | +| `stx_transferSip10Ft` | `{ recipient, asset: "SP….contract::asset", amount, … }` | `{ txid, transaction }` (adds "you send exactly `amount`" when the app gives no post-conditions) | +| `stx_transferSip9Nft` | `{ recipient, asset, assetId, … }` | `{ txid, transaction }` (adds "you send `assetId`") | +| `stx_callContract` | `{ contract: "SP….name", functionName, functionArgs?, postConditions?, postConditionMode?, … }` | `{ txid, transaction }` | +| `stx_deployContract` | `{ name, clarityCode, clarityVersion?, … }` | `{ txid, transaction }` | +| `stx_signTransaction` | `{ transaction: hex, broadcast? = false }` | `{ transaction, txid }` | +| `stx_signMessage` | `{ message: string }` | `{ signature: RSV hex, publicKey }` | +| `stx_signStructuredMessage` | `{ domain, message }` (SIP-018) | `{ signature: RSV hex, publicKey }` | + +Transactions built from an app's params get their nonce (`possible_next_nonce`) and fee at decode time, and are kept +per request id so `prepare` and `finalize` sign exactly what the approval showed. A sponsored transaction is never +broadcast by the wallet (the sponsor signs and sends it). `buildTransfer` makes a complete unsigned transaction and +sends it as `stx_signTransaction` with `broadcast: true` (STX: a token transfer; SIP-010 tokens: `transfer` with an +exact post-condition). + +Refusals: another network (`network` param, request network, transaction version/chain id or an address's version) +→ `stacks/network-mismatch`; an `address` that isn't this account, or a transaction whose origin isn't this +account's P2PKH key → `stacks/wrong-account`; multisig origins → `stacks/unsupported-multisig`; a transfer to +yourself (nodes refuse it) → `stacks/self-transfer`. A transaction that can't be parsed (bad hex, coinbase or +tenure-change payloads, unknown post-condition types) is `blind: true` and can't be prepared. + +### What gets signed (`prepare`) + +- **Transactions (origin, single-sig P2PKH):** the initial sighash is the txid (SHA-512/256) of the transaction with + the origin's nonce, fee and signature cleared and, when sponsored, the sponsor condition replaced by an empty + P2PKH one. The digest is `SHA-512/256(sighash ‖ 0x04 ‖ fee(8) ‖ nonce(8))`. The origin always uses the standard + flag 0x04, sponsored or not (stacks.js `signNextOrigin` / `sigHashPreSign`). The signature goes into the + spending condition as **recovery id ‖ r ‖ s** (65 bytes, "VRS") with key encoding 0x00 (compressed). +- **Messages:** `SHA-256("\x17Stacks Signed Message:\n" ‖ CompactSize(len) ‖ utf8(message))` (@stacks/encryption + `hashMessage`). **SIP-018:** `SHA-256("SIP018" ‖ SHA-256(domain) ‖ SHA-256(message))`; the domain must be a tuple + of `name`, `version` (string-ascii) and `chain-id` (uint), and a `chain-id` that isn't this network's is a danger + warning. Results are **r ‖ s ‖ recovery id** ("RSV") hex, what `verifyMessageSignatureRsv` checks. + +### Decoding + +- STX transfers: "Send 1.5 STX to ST1…ABCD", amount, memo, fee, nonce, the balance change. +- Contract calls: "Use swap on contract amm", every argument as readable Clarity (`u100`, `'SP…`, `(some …)`, + `{ a: u1 }`), and every post-condition as a line ("You send exactly 2.5 USDCx", "pool sends at least 1 USDCx", + "You keep nft u7"). Your exact / at-most post-conditions become the balance changes. +- SIP-010 `transfer` from you is recognised and titled "Send 2.5 USDCx to …". +- `postConditionMode: "allow"` → danger: the contract may move any of your assets. "originator" (SIP-040) → info. + Deny mode without a post-condition on you → "None of your assets can leave your account … apart from the network fee." +- Contract deploys: the name, Clarity version and code, with a caution. +- Sponsored: the fee line says who pays. + +## 1Mask (`@clip-wallet/1mask`) + +`inpage/stacks.ts` puts a SIP-030 provider on `window.clipwallet.stacks` (`request(method, params)` resolving a +JSON-RPC 2.0 response, `listen("stx_accountChange", …)`) and registers it with WBIP-004: it pushes +`{ id: "clipwallet.stacks", name, icon, methods }` (the wallet's own identity) onto `window.wbip_providers`, which +`@stacks/connect` v8 lists in its picker and resolves by path (`getProviderFromId`). It never sets +`window.StacksProvider`, `LeatherProvider` or `XverseProviders`. `background/stacks.ts` (`createStacksDispatcher`) +allowlists the methods, checks permissions and the signer, re-spells the account for the site's network (SP → ST) +and hands signing methods to the module unchanged. + +## Not supported (yet) + +Multisig origins, sponsoring someone else's transaction, `stx_getAccounts` (it hands out a Gaia app key, which is a +private key), `stx_updateProfile`, NFTs in `getNfts`, stacking. diff --git a/packages/chains-stacks/package.json b/packages/chains-stacks/package.json new file mode 100644 index 00000000..1d864916 --- /dev/null +++ b/packages/chains-stacks/package.json @@ -0,0 +1,79 @@ +{ + "name": "@clip-wallet/chains-stacks", + "version": "0.1.0", + "description": "Stacks ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "stacks", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-stacks#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-stacks" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0" + }, + "devDependencies": { + "@stacks/encryption": "7.6.0", + "@stacks/transactions": "7.6.0" + } +} diff --git a/packages/chains-stacks/src/api.ts b/packages/chains-stacks/src/api.ts new file mode 100644 index 00000000..be708606 --- /dev/null +++ b/packages/chains-stacks/src/api.ts @@ -0,0 +1,192 @@ +import { ClipError, msg } from "@clip-wallet/core"; +import { hex } from "./util.js"; + +/** + * Hiro's public Stacks API (no key): the node RPC (/v2/*) and the indexer (/extended/*). + * https://docs.hiro.so/stacks/api — every call here was checked with curl against api.hiro.so and + * api.testnet.hiro.so (2026-10). Errors become plain-words ClipErrors with a "stacks/" code. + */ +export class HiroError extends Error { + constructor( + message: string, + readonly status: number, + readonly body: unknown, + ) { + super(message); + } +} + +export interface AccountInfo { + /** µSTX, hex-encoded u128 in /v2/accounts. */ + balance: bigint; + locked: bigint; + nonce: bigint; +} + +export interface FtBalance { + balance: string; +} + +export interface BalancesV1 { + stx: { balance: string; locked: string; estimated_balance?: string; pending_balance_inbound?: string; pending_balance_outbound?: string }; + fungible_tokens?: Record; +} + +export interface TxStatus { + tx_status: "pending" | "success" | "abort_by_response" | "abort_by_post_condition" | "dropped_replace_by_fee" | "dropped_replace_across_fork" | "dropped_too_expensive" | "dropped_stale_garbage_collect" | string; + tx_result?: { repr?: string }; +} + +export interface FtMetadata { + name?: string; + symbol?: string; + decimals?: number; +} + +const bigOf = (v: unknown): bigint => { + if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v); + if (typeof v === "string" && /^(0x[0-9a-f]+|\d+)$/i.test(v)) return BigInt(v); + return 0n; +}; + +export class Hiro { + constructor( + readonly base: string, + readonly fetchImpl: typeof fetch, + ) {} + + async #req(path: string, init?: RequestInit): Promise { + let res: Response; + try { + res = await this.fetchImpl(`${this.base.replace(/\/$/, "")}${path}`, init); + } catch (e) { + throw new ClipError(msg("bg.err.couldntReach", { what: "Stacks" }), "stacks/offline", e); + } + const text = await res.text(); + let body: unknown = text; + try { + body = JSON.parse(text); + } catch { + /* plain text */ + } + if (!res.ok) throw new HiroError(typeof body === "object" && body && "error" in body ? String((body as { error: unknown }).error) : text.slice(0, 200), res.status, body); + return body as T; + } + + get(path: string): Promise { + return this.#req(path); + } + + async account(address: string): Promise { + const r = await this.#req<{ balance?: string; locked?: string; nonce?: number }>(`/v2/accounts/${address}?proof=0`); + return { balance: bigOf(r.balance), locked: bigOf(r.locked), nonce: bigOf(r.nonce) }; + } + + /** Next nonce: the indexer's `possible_next_nonce` (counts the mempool), else the node's account nonce. */ + async nextNonce(address: string): Promise { + try { + const r = await this.#req<{ possible_next_nonce?: number }>(`/extended/v1/address/${address}/nonces`); + if (typeof r.possible_next_nonce === "number") return BigInt(r.possible_next_nonce); + } catch { + /* fall back to the node */ + } + return (await this.account(address)).nonce; + } + + balances(address: string): Promise { + return this.#req(`/extended/v1/address/${address}/balances`); + } + + async ftMetadata(contract: string): Promise { + try { + return await this.#req(`/metadata/v1/ft/${contract}`); + } catch { + return null; + } + } + + /** + * Fee for a payload: POST /v2/fees/transaction (the middle of the three estimates), else the per-byte rate of + * GET /v2/fees/transfer × the transaction length. Never below 1 µSTX per byte. + */ + async fee(payload: Uint8Array, txLength: number): Promise { + const floor = BigInt(Math.max(txLength, 180)); + try { + const r = await this.#req<{ estimations?: { fee?: number }[] }>("/v2/fees/transaction", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ transaction_payload: hex(payload), estimated_len: txLength }), + }); + const e = r.estimations ?? []; + const mid = e[1]?.fee ?? e[0]?.fee; + if (typeof mid === "number" && Number.isFinite(mid) && mid > 0) { + const fee = BigInt(Math.ceil(mid)); + return fee > floor ? fee : floor; + } + } catch { + /* fall back */ + } + try { + const rate = await this.#req("/v2/fees/transfer"); + if (typeof rate === "number" && rate > 0) { + const fee = BigInt(Math.ceil(rate * txLength)); + return fee > floor ? fee : floor; + } + } catch { + /* fall back */ + } + return floor; + } + + /** POST /v2/transactions (raw bytes) → txid (hex, no 0x). */ + async broadcast(raw: Uint8Array): Promise { + const r = await this.#req("/v2/transactions", { + method: "POST", + headers: { "content-type": "application/octet-stream" }, + body: raw as unknown as BodyInit, + }); + if (typeof r === "string" && /^(0x)?[0-9a-f]{64}$/i.test(r.trim())) return r.trim().replace(/^0x/i, ""); + throw new HiroError("unexpected broadcast reply", 200, r); + } + + tx(txid: string): Promise { + return this.#req(`/extended/v1/tx/0x${txid.replace(/^0x/i, "")}`); + } +} + +/** + * The node's rejection reasons (stacks-core `MemPoolRejection`, the `reason` field of a 400 from /v2/transactions) + * in plain words. Never shows the raw error. + */ +export function plainStacksError(e: unknown): ClipError { + if (e instanceof ClipError) return e; + const body = e instanceof HiroError && e.body && typeof e.body === "object" ? (e.body as { reason?: string; error?: string }) : {}; + const reason = body.reason ?? ""; + switch (reason) { + case "NotEnoughFunds": + case "FeeTooLow": + return reason === "FeeTooLow" + ? new ClipError("The network fee was too low, so the network didn't take it. Nothing was sent. Try again.", "stacks/fee-too-low", e) + : new ClipError(msg("bg.err.notEnoughForFee", { symbol: "STX" }), "stacks/insufficient-funds", e); + case "BadNonce": + case "ConflictingNonceInMempool": + return new ClipError("Another transaction from this account is still waiting. Wait for it to finish, then try again.", "stacks/nonce-busy", e); + case "BadAddressVersionByte": + case "BadTransactionVersion": + return new ClipError("This transaction is for a different Stacks network, so it wasn't sent.", "stacks/network-mismatch", e); + case "NoSuchContract": + case "NoSuchPublicFunction": + case "BadFunctionArgument": + case "ContractAlreadyExists": + return new ClipError("The app's contract call doesn't match the contract on the network. Nothing was sent.", "stacks/bad-contract-call", e); + case "TransferRecipientIsSender": + return new ClipError("That's your own address.", "stacks/self-transfer", e); + case "TooMuchChaining": + return new ClipError("This account has too many transactions waiting. Wait for some to finish, then try again.", "stacks/too-many-pending", e); + case "SignatureValidation": + return new ClipError("The network didn't accept the signature. Nothing was sent.", "stacks/bad-signature", e); + default: + if (e instanceof HiroError && e.status >= 500) return new ClipError(msg("bg.err.isBusy", { what: "Stacks" }), "stacks/busy", e); + return new ClipError("The network rejected this. Nothing was sent.", "stacks/rejected", e); + } +} diff --git a/packages/chains-stacks/src/c32.ts b/packages/chains-stacks/src/c32.ts new file mode 100644 index 00000000..4d6d8d67 --- /dev/null +++ b/packages/chains-stacks/src/c32.ts @@ -0,0 +1,113 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { bytesEqual, concat, hash160 } from "./util.js"; + +/** + * c32check addresses (https://github.com/stacks-network/c32check, SIP-005 "Address"): "S" + c32(version) + + * c32(hash160 ‖ SHA-256(SHA-256(version ‖ hash160))[0..4]). Crockford base32 alphabet, big-endian, leading zero + * bytes kept as "0". Same encoding as the vault's `stacksAddress` (packages/vault/src/encodings87.ts). + */ +const C32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + +/** Single-signature (P2PKH) versions: 22 "SP" mainnet, 26 "ST" testnet. Multisig: 20 "SM", 21 "SN". */ +export const VERSION = { mainnetSingle: 22, mainnetMulti: 20, testnetSingle: 26, testnetMulti: 21 } as const; +const MAINNET_VERSIONS = new Set([VERSION.mainnetSingle, VERSION.mainnetMulti]); +const TESTNET_VERSIONS = new Set([VERSION.testnetSingle, VERSION.testnetMulti]); + +export function c32encode(data: Uint8Array): string { + let n = 0n; + for (const b of data) n = (n << 8n) | BigInt(b); + let out = ""; + while (n > 0n) { + out = C32[Number(n & 31n)]! + out; + n >>= 5n; + } + let zeros = 0; + while (zeros < data.length && data[zeros] === 0) zeros++; + return "0".repeat(zeros) + out; +} + +/** Inverse of c32encode for a known byte length (Crockford's O/I/L aliases accepted, as c32check does). */ +export function c32decode(text: string, length: number): Uint8Array { + const s = text.toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1"); + let n = 0n; + for (const ch of s) { + const v = C32.indexOf(ch); + if (v < 0) throw new Error("not c32"); + n = (n << 5n) | BigInt(v); + } + const out = new Uint8Array(length); + for (let i = length - 1; i >= 0; i--) { + out[i] = Number(n & 0xffn); + n >>= 8n; + } + if (n !== 0n) throw new Error("c32 value too large"); + // leading "0" characters must match leading zero bytes + let zeros = 0; + while (zeros < s.length && s[zeros] === "0") zeros++; + let zb = 0; + while (zb < out.length && out[zb] === 0) zb++; + if (zeros > zb) throw new Error("bad c32 padding"); + return out; +} + +const checksum = (version: number, h: Uint8Array) => sha256(sha256(concat(new Uint8Array([version]), h))).subarray(0, 4); + +export function c32address(version: number, h: Uint8Array): string { + if (h.length !== 20 || version < 0 || version >= 32) throw new Error("bad c32 address input"); + return "S" + C32[version] + c32encode(concat(h, checksum(version, h))); +} + +/** Parses a standard principal ("SP…"/"ST…"/"SM…"/"SN…"), checksum verified; null when it isn't one. */ +export function parseAddress(value: string): { version: number; hash160: Uint8Array } | null { + const v = value.trim(); + // Short when the hash starts with zero bytes (each is a leading "0"): "ST000000000000000000002AMW42H". + if (!/^S[0-9A-HJKMNP-TV-Z][0-9A-HJKMNP-TV-Z]{2,41}$/i.test(v)) return null; + const version = C32.indexOf(v[1]!.toUpperCase()); + try { + const body = c32decode(v.slice(2), 24); + const h = body.subarray(0, 20); + if (!bytesEqual(body.subarray(20), checksum(version, h))) return null; + if (c32address(version, h) !== v.toUpperCase()) return null; // canonical spelling only + return { version, hash160: h.slice() }; + } catch { + return null; + } +} + +export type StacksNet = "mainnet" | "testnet"; + +export function netOfVersion(version: number): StacksNet | null { + return MAINNET_VERSIONS.has(version) ? "mainnet" : TESTNET_VERSIONS.has(version) ? "testnet" : null; +} + +export function singleSigVersion(net: StacksNet): number { + return net === "mainnet" ? VERSION.mainnetSingle : VERSION.testnetSingle; +} + +/** P2PKH address of a compressed secp256k1 key on `net`. */ +export function addressOfKey(compressedKey: Uint8Array, net: StacksNet): string { + if (compressedKey.length !== 33) throw new Error("Stacks needs a compressed secp256k1 public key"); + return c32address(singleSigVersion(net), hash160(compressedKey)); +} + +/** Contract names (SIP-005 / clarity `ContractName`): 1–40 chars, letter first, then letters, digits, "-" or "_". */ +export const CONTRACT_NAME = /^[a-zA-Z]([a-zA-Z0-9]|[-_])*$/; + +/** Clarity names (functions, assets, tuple keys; clarity `ClarityName`), at most 128 characters. */ +export const CLARITY_NAME = /^([a-zA-Z]([a-zA-Z0-9]|[-_!?+<>=/*])*|[-+=/*]|[<>]=?)$/; + +/** "SP….name" → parts; null unless the address is valid and the name well formed. */ +export function parseContractId(value: string): { address: string; version: number; hash160: Uint8Array; name: string } | null { + const [addr, name, ...rest] = value.trim().split("."); + if (!addr || !name || rest.length || name.length > 128 || !CONTRACT_NAME.test(name)) return null; + const a = parseAddress(addr); + return a ? { address: addr.toUpperCase(), ...a, name } : null; +} + +/** "SP….contract::asset-name" (SIP-030 `asset`). */ +export function parseAssetId(value: string): { contract: string; address: string; version: number; hash160: Uint8Array; contractName: string; assetName: string } | null { + const [contract, assetName, ...rest] = value.trim().split("::"); + if (!contract || !assetName || rest.length || assetName.length > 128 || !CLARITY_NAME.test(assetName)) return null; + const c = parseContractId(contract); + return c ? { contract: `${c.address}.${c.name}`, address: c.address, version: c.version, hash160: c.hash160, contractName: c.name, assetName } : null; +} diff --git a/packages/chains-stacks/src/clarity.ts b/packages/chains-stacks/src/clarity.ts new file mode 100644 index 00000000..b965caa4 --- /dev/null +++ b/packages/chains-stacks/src/clarity.ts @@ -0,0 +1,340 @@ +import { CLARITY_NAME, CONTRACT_NAME, c32address, parseAddress, parseContractId } from "./c32.js"; +import { Reader, concat, fromHex, hex, isHex, readUintBE, textOf, uintBE, utf8 } from "./util.js"; + +/** + * Clarity values: SIP-005 wire format ("Clarity value representation") and the SIP-030 JSON form + * (https://github.com/stacksgov/sips/blob/main/sips/sip-030/sip-030-wallet-interface.md#clarity-values). The type + * tags match @stacks/transactions `ClarityType` names, so a dapp's `Cl.*` objects (stringified) parse too. + */ +export type CV = + | { type: "int"; value: bigint } + | { type: "uint"; value: bigint } + | { type: "buffer"; value: Uint8Array } + | { type: "true" } + | { type: "false" } + | { type: "address"; value: string } + | { type: "contract"; value: string } + | { type: "ok"; value: CV } + | { type: "err"; value: CV } + | { type: "none" } + | { type: "some"; value: CV } + | { type: "list"; value: CV[] } + | { type: "tuple"; value: Record } + | { type: "ascii"; value: string } + | { type: "utf8"; value: string }; + +const TAG = { + int: 0x00, + uint: 0x01, + buffer: 0x02, + true: 0x03, + false: 0x04, + address: 0x05, + contract: 0x06, + ok: 0x07, + err: 0x08, + none: 0x09, + some: 0x0a, + list: 0x0b, + tuple: 0x0c, + ascii: 0x0d, + utf8: 0x0e, +} as const; + +/** Clarity's own nesting limit (MAX_TYPE_DEPTH 32); deeper is refused instead of recursing without bound. */ +const MAX_DEPTH = 32; +const MAX_VALUE_SIZE = 1024 * 1024; + +const I128_MIN = -(1n << 127n); +const I128_MAX = (1n << 127n) - 1n; +const U128_MAX = (1n << 128n) - 1n; + +/** Standard principal bytes: version ‖ hash160. */ +export function principalBytes(address: string): Uint8Array { + const a = parseAddress(address); + if (!a) throw new Error("bad principal"); + return concat(new Uint8Array([a.version]), a.hash160); +} + +function lp1(s: string): Uint8Array { + const b = utf8(s); + if (b.length > 128) throw new Error("name too long"); + return concat(new Uint8Array([b.length]), b); +} + +function u32(n: number): Uint8Array { + return uintBE(BigInt(n), 4); +} + +export function serializeCV(v: CV): Uint8Array { + switch (v.type) { + case "int": { + if (v.value < I128_MIN || v.value > I128_MAX) throw new Error("int out of range"); + return concat(new Uint8Array([TAG.int]), uintBE(BigInt.asUintN(128, v.value), 16)); + } + case "uint": + if (v.value < 0n || v.value > U128_MAX) throw new Error("uint out of range"); + return concat(new Uint8Array([TAG.uint]), uintBE(v.value, 16)); + case "buffer": + return concat(new Uint8Array([TAG.buffer]), u32(v.value.length), v.value); + case "true": + case "false": + case "none": + return new Uint8Array([TAG[v.type]]); + case "address": + return concat(new Uint8Array([TAG.address]), principalBytes(v.value)); + case "contract": { + const c = parseContractId(v.value); + if (!c) throw new Error("bad contract principal"); + return concat(new Uint8Array([TAG.contract, c.version]), c.hash160, lp1(c.name)); + } + case "ok": + case "err": + case "some": + return concat(new Uint8Array([TAG[v.type]]), serializeCV(v.value)); + case "list": + return concat(new Uint8Array([TAG.list]), u32(v.value.length), ...v.value.map(serializeCV)); + case "tuple": { + // Keys sorted (lexicographic byte order), as the Clarity VM does. + const keys = Object.keys(v.value).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + return concat(new Uint8Array([TAG.tuple]), u32(keys.length), ...keys.flatMap((k) => [lp1(k), serializeCV(v.value[k]!)])); + } + case "ascii": { + const b = utf8(v.value); + if ([...b].some((c) => c > 0x7e || (c < 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d))) throw new Error("not ascii"); + return concat(new Uint8Array([TAG.ascii]), u32(b.length), b); + } + case "utf8": { + const b = utf8(v.value); + return concat(new Uint8Array([TAG.utf8]), u32(b.length), b); + } + } +} + +export function readCV(r: Reader, depth = 0): CV { + if (depth > MAX_DEPTH) throw new Error("value nested too deeply"); + const tag = r.u8(); + switch (tag) { + case TAG.int: + return { type: "int", value: BigInt.asIntN(128, readUintBE(r.read(16))) }; + case TAG.uint: + return { type: "uint", value: readUintBE(r.read(16)) }; + case TAG.buffer: { + const n = r.u32(); + if (n > MAX_VALUE_SIZE) throw new Error("buffer too large"); + return { type: "buffer", value: r.read(n).slice() }; + } + case TAG.true: + return { type: "true" }; + case TAG.false: + return { type: "false" }; + case TAG.address: { + const version = r.u8(); + return { type: "address", value: c32address(version, r.read(20)) }; + } + case TAG.contract: { + const version = r.u8(); + const h = r.read(20); + const name = new TextDecoder().decode(r.read(r.u8())); + if (!CONTRACT_NAME.test(name)) throw new Error("bad contract name"); + return { type: "contract", value: `${c32address(version, h)}.${name}` }; + } + case TAG.ok: + case TAG.err: + case TAG.some: + return { type: tag === TAG.ok ? "ok" : tag === TAG.err ? "err" : "some", value: readCV(r, depth + 1) }; + case TAG.none: + return { type: "none" }; + case TAG.list: { + const n = r.u32(); + if (n > MAX_VALUE_SIZE) throw new Error("list too long"); + const value: CV[] = []; + for (let i = 0; i < n; i++) value.push(readCV(r, depth + 1)); + return { type: "list", value }; + } + case TAG.tuple: { + const n = r.u32(); + if (n > MAX_VALUE_SIZE) throw new Error("tuple too large"); + const value: Record = Object.create(null); + for (let i = 0; i < n; i++) { + const key = new TextDecoder().decode(r.read(r.u8())); + if (!CLARITY_NAME.test(key)) throw new Error("bad tuple key"); + value[key] = readCV(r, depth + 1); + } + return { type: "tuple", value }; + } + case TAG.ascii: + case TAG.utf8: { + const n = r.u32(); + if (n > MAX_VALUE_SIZE) throw new Error("string too large"); + const b = r.read(n); + const s = textOf(b); + if (s === null && n > 0) throw new Error("bad string"); + return { type: tag === TAG.ascii ? "ascii" : "utf8", value: s ?? "" }; + } + default: + throw new Error(`unknown clarity type ${tag}`); + } +} + +export function deserializeCV(bytes: Uint8Array): CV { + const r = new Reader(bytes); + const v = readCV(r); + if (!r.done) throw new Error("trailing bytes"); + return v; +} + +/* ------------------------------------------------------------------ SIP-030 JSON → CV */ + +/** stacks.js v7 `ClarityType` names and older numeric tags, mapped onto SIP-030 names. */ +const ALIASES: Record = { + int: "int", + uint: "uint", + buffer: "buffer", + true: "true", + false: "false", + address: "address", + contract: "contract", + ok: "ok", + err: "err", + none: "none", + some: "some", + list: "list", + tuple: "tuple", + ascii: "ascii", + utf8: "utf8", + "0": "int", + "1": "uint", + "2": "buffer", + "3": "true", + "4": "false", + "5": "address", + "6": "contract", + "7": "ok", + "8": "err", + "9": "none", + "10": "some", + "11": "list", + "12": "tuple", + "13": "ascii", + "14": "utf8", +}; + +function big(v: unknown): bigint { + if (typeof v === "bigint") return v; + if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v); + if (typeof v === "string" && /^-?\d+$/.test(v.trim())) return BigInt(v.trim()); + throw new Error("bad integer"); +} + +/** + * A Clarity value from a dapp: hex (SIP-005 bytes) or the SIP-030 JSON object. Throws on anything else, so the + * request becomes unreadable instead of being guessed at. + */ +export function cvFrom(input: unknown, depth = 0): CV { + if (depth > MAX_DEPTH) throw new Error("value nested too deeply"); + if (typeof input === "string") { + if (!isHex(input)) throw new Error("not a clarity value"); + return deserializeCV(fromHex(input)); + } + if (!input || typeof input !== "object") throw new Error("not a clarity value"); + const o = input as { type?: unknown; value?: unknown; data?: unknown; list?: unknown; address?: unknown; contractName?: unknown }; + const type = ALIASES[String(o.type)]; + if (!type) throw new Error("unknown clarity type"); + switch (type) { + case "int": + case "uint": + return { type, value: big(o.value) }; + case "buffer": { + const v = o.value; + if (typeof v === "string") return { type, value: v === "" || v === "0x" ? new Uint8Array() : fromHex(v) }; + if (v instanceof Uint8Array) return { type, value: v }; + throw new Error("bad buffer"); + } + case "true": + case "false": + case "none": + return { type }; + case "address": + case "contract": { + const v = typeof o.value === "string" ? o.value : typeof o.address === "string" ? (typeof o.contractName === "string" ? `${o.address}.${o.contractName}` : o.address) : ""; + if (v.includes(".")) { + const c = parseContractId(v); + if (!c) throw new Error("bad contract principal"); + return { type: "contract", value: `${c.address}.${c.name}` }; + } + if (!parseAddress(v)) throw new Error("bad principal"); + return { type: "address", value: v.toUpperCase() }; + } + case "ok": + case "err": + case "some": + return { type, value: cvFrom(o.value, depth + 1) }; + case "list": { + const arr = Array.isArray(o.value) ? o.value : Array.isArray(o.list) ? o.list : null; + if (!arr) throw new Error("bad list"); + return { type, value: arr.map((x) => cvFrom(x, depth + 1)) }; + } + case "tuple": { + const rec = (o.value ?? o.data) as Record | undefined; + if (!rec || typeof rec !== "object" || Array.isArray(rec)) throw new Error("bad tuple"); + const value: Record = Object.create(null); + for (const [k, x] of Object.entries(rec)) { + if (!CLARITY_NAME.test(k)) throw new Error("bad tuple key"); + value[k] = cvFrom(x, depth + 1); + } + return { type, value }; + } + case "ascii": + case "utf8": + if (typeof o.value !== "string") throw new Error("bad string"); + return { type, value: o.value }; + } +} + +/* ------------------------------------------------------------------ display */ + +/** Readable Clarity-like text ("u100", "'SP…", "(some u5)", "{ amount: u1 }"), cut at `max` characters. */ +export function cvText(v: CV, max = 160): string { + const s = render(v, 0); + return s.length > max ? `${s.slice(0, max - 1)}…` : s; +} + +function render(v: CV, depth: number): string { + if (depth > 6) return "…"; + switch (v.type) { + case "int": + return v.value.toString(); + case "uint": + return `u${v.value}`; + case "buffer": { + const t = v.value.length && textOf(v.value); + return t ? `0x${hex(v.value)} ("${t}")` : `0x${hex(v.value)}`; + } + case "true": + case "false": + case "none": + return v.type; + case "address": + case "contract": + return `'${v.value}`; + case "ok": + case "err": + case "some": + return `(${v.type} ${render(v.value, depth + 1)})`; + case "list": + return `(list ${v.value.map((x) => render(x, depth + 1)).join(" ")})`; + case "tuple": + return `{ ${Object.entries(v.value) + .map(([k, x]) => `${k}: ${render(x, depth + 1)}`) + .join(", ")} }`; + case "ascii": + case "utf8": + return JSON.stringify(v.value); + } +} + +/** The principal text of an address/contract value, else null. */ +export function principalOf(v: CV | undefined): string | null { + return v && (v.type === "address" || v.type === "contract") ? v.value : null; +} diff --git a/packages/chains-stacks/src/describe.ts b/packages/chains-stacks/src/describe.ts new file mode 100644 index 00000000..844eca15 --- /dev/null +++ b/packages/chains-stacks/src/describe.ts @@ -0,0 +1,204 @@ +import { type AssetRef, type BalanceChange, type Msg, type Warning, msg } from "@clip-wallet/core"; +import { type CV, cvText, principalOf } from "./clarity.js"; +import { stxAsset } from "./networks.js"; +import { type PcPrincipal, type PostCondition, type StacksTx, AUTH_SPONSORED, PC_MODE, memoText } from "./tx.js"; +import { formatUnits, short } from "./util.js"; + +/** + * Plain-language description of a Stacks transaction for the approval screen. Post-conditions are the network's + * own guarantee of what may leave an account, so they are shown and turned into balance changes; "allow" mode + * (anything may move) is a danger warning. + */ + +export interface DescribeContext { + networkId: string; + /** This account's address on the request's network (SP… or ST…). */ + me: string; + host: string; + /** Token AssetRef for "SP….contract::asset" (curated, from metadata, or a bare fallback). */ + token(assetId: string): Promise; + /** Token AssetRef for a contract when only the contract is known (SIP-010 `transfer` without a post-condition). */ + tokenByContract(contract: string): Promise; +} + +export interface Line { + label: string; + value: string; + labelMsg?: Msg; + valueMsg?: Msg; +} + +export interface Described { + title: string; + titleMsg?: Msg; + lines: Line[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + blind: boolean; + fee: bigint; + sponsored: boolean; +} + +const STX_DECIMALS = 6; +export const fmtStx = (v: bigint) => `${formatUnits(v, STX_DECIMALS)} STX`; +export const fmtAsset = (v: bigint, a: AssetRef) => `${formatUnits(v, a.decimals)} ${a.symbol}`; + +const label = (m: Msg, value: string, valueMsg?: Msg): Line => ({ label: m.fallback, labelMsg: m, value, ...(valueMsg ? { valueMsg } : {}) }); +const valueLine = (labelText: string, m: Msg): Line => ({ label: labelText, value: m.fallback, valueMsg: m }); + +function isMe(p: PcPrincipal, me: string): boolean { + return p.kind === "origin" || (p.kind === "standard" && p.address === me); +} + +function whoOf(p: PcPrincipal): string { + return p.kind === "standard" ? short(p.address) : p.kind === "contract" ? p.contract.split(".")[1] ?? p.contract : "you"; +} + +const FT_YOU: Record[0]> = { + 0x01: "bg.stacks.pcYouSendExactly", + 0x02: "bg.stacks.pcYouSendMoreThan", + 0x03: "bg.stacks.pcYouSendAtLeast", + 0x04: "bg.stacks.pcYouSendLessThan", + 0x05: "bg.stacks.pcYouSendAtMost", +}; +const FT_OTHER: Record[0]> = { + 0x01: "bg.stacks.pcSendsExactly", + 0x02: "bg.stacks.pcSendsMoreThan", + 0x03: "bg.stacks.pcSendsAtLeast", + 0x04: "bg.stacks.pcSendsLessThan", + 0x05: "bg.stacks.pcSendsAtMost", +}; +const NFT_YOU: Record[0]> = { 0x10: "bg.stacks.pcYouSendNft", 0x11: "bg.stacks.pcYouKeepNft", 0x12: "bg.stacks.pcYouMaySendNft" }; +const NFT_OTHER: Record[0]> = { 0x10: "bg.stacks.pcSendsNft", 0x11: "bg.stacks.pcKeepsNft", 0x12: "bg.stacks.pcMaySendNft" }; + +/** One post-condition as a line, plus the balance change it guarantees for this account (exact or upper bound). */ +async function describePc(pc: PostCondition, ctx: DescribeContext): Promise<{ line: Line; change?: BalanceChange }> { + const pcLabel = msg("bg.stacks.postCondition"); + const mine = isMe(pc.principal, ctx.me); + if (pc.type === "stx" || pc.type === "ft") { + const asset = pc.type === "stx" ? stxAsset(ctx.networkId) : await ctx.token(`${pc.asset.contract}::${pc.asset.assetName}`); + const amount = fmtAsset(pc.amount, asset); + const m = mine ? msg(FT_YOU[pc.code]!, { amount }) : msg(FT_OTHER[pc.code]!, { who: whoOf(pc.principal), amount }); + // Exact (eq) or "at most" (lte / lt) limits on what leaves this account are what it can lose. + const bound = pc.code === 0x01 || pc.code === 0x05 ? pc.amount : pc.code === 0x04 && pc.amount > 0n ? pc.amount - 1n : null; + const change = mine && bound !== null && bound > 0n ? { asset, delta: (-bound).toString() } : undefined; + return { line: { ...label(pcLabel, m.fallback, m) }, ...(change ? { change } : {}) }; + } + if (pc.type === "nft") { + const item = `${pc.asset.assetName} ${cvText(pc.assetId, 40)}`; + const m = mine ? msg(NFT_YOU[pc.code]!, { item }) : msg(NFT_OTHER[pc.code]!, { item, who: whoOf(pc.principal) }); + return { line: label(pcLabel, m.fallback, m) }; + } + const m = msg("bg.stacks.pcStakingRule", { who: mine ? ctx.me : whoOf(pc.principal) }); + return { line: label(pcLabel, m.fallback, m) }; +} + +function mergeChanges(changes: BalanceChange[]): BalanceChange[] { + const by = new Map(); + for (const c of changes) { + const k = c.asset.address ?? c.asset.key; + const prev = by.get(k); + by.set(k, prev ? { asset: prev.asset, delta: (BigInt(prev.delta) + BigInt(c.delta)).toString() } : c); + } + return [...by.values()].filter((c) => c.delta !== "0"); +} + +/** SIP-010 `transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))`. */ +export function sip10Transfer(tx: StacksTx): { amount: bigint; sender: string; recipient: string; memo?: CV } | null { + const p = tx.payload; + if (p.type !== "contract-call" || p.functionName !== "transfer" || (p.args.length !== 3 && p.args.length !== 4)) return null; + const [amount, sender, recipient, memo] = p.args; + if (amount?.type !== "uint") return null; + const s = principalOf(sender); + const r = principalOf(recipient); + if (!s || !r) return null; + if (memo && memo.type !== "none" && memo.type !== "some") return null; + return { amount: amount.value, sender: s, recipient: r, ...(memo ? { memo } : {}) }; +} + +export async function describeTx(tx: StacksTx, ctx: DescribeContext): Promise { + const lines: Line[] = []; + const warnings: Warning[] = []; + const changes: BalanceChange[] = []; + const sponsored = tx.auth.type === AUTH_SPONSORED; + const fee = sponsored ? 0n : tx.auth.origin.fee; + const stx = stxAsset(ctx.networkId); + let title: Msg; + let blind = false; + const p = tx.payload; + + const pcs = await Promise.all(tx.postConditions.map((pc) => describePc(pc, ctx))); + + if (p.type === "token-transfer") { + const to = principalOf(p.recipient)!; + const amount = fmtStx(p.amount); + title = msg("bg.req.sendTo", { amount, to: short(to) }); + lines.push({ label: "To", value: to }, { label: "Amount", value: amount }); + const memo = memoText(p.memo); + if (memo) lines.push({ label: "Memo", value: memo }); + if (to !== ctx.me) changes.push({ asset: stx, delta: (-p.amount).toString() }); + } else if (p.type === "smart-contract") { + title = msg("bg.stacks.deployNamed", { name: p.name }); + lines.push({ label: "Contract", value: `${ctx.me}.${p.name}` }); + if (p.clarityVersion !== undefined) lines.push({ label: "Type", value: `Clarity ${p.clarityVersion}` }); + lines.push({ label: "Code", value: p.code.length > 400 ? `${p.code.slice(0, 399)}…` : p.code }); + warnings.push({ level: "caution", code: "unknown-call", message: "This creates a new smart contract. Clip Wallet can't check what its code does." }); + } else { + const contractName = p.contract.split(".")[1] ?? p.contract; + const t = sip10Transfer(tx); + if (t && t.sender === ctx.me) { + const pcAsset = tx.postConditions.find((pc) => pc.type === "ft" && pc.asset.contract === p.contract); + const asset = pcAsset && pcAsset.type === "ft" ? await ctx.token(`${pcAsset.asset.contract}::${pcAsset.asset.assetName}`) : await ctx.tokenByContract(p.contract); + if (asset) { + const amount = fmtAsset(t.amount, asset); + title = msg("bg.req.sendTo", { amount, to: short(t.recipient) }); + lines.push({ label: "To", value: t.recipient }, { label: "Amount", value: amount }, { label: "Contract", value: p.contract }); + if (t.memo?.type === "some") lines.push({ label: "Memo", value: cvText(t.memo.value) }); + if (t.recipient !== ctx.me && !tx.postConditions.some((pc) => pc.type === "ft" && isMe(pc.principal, ctx.me))) { + changes.push({ asset, delta: (-t.amount).toString() }); + } + } else { + title = msg("bg.req.useFnOnContract", { fn: p.functionName, contract: contractName }); + } + } else { + title = msg("bg.req.useFnOnContract", { fn: p.functionName, contract: contractName }); + } + if (!lines.length) { + lines.push({ label: "Contract", value: p.contract }, { label: "Function", value: p.functionName }); + p.args.forEach((a, i) => lines.push(label(msg("bg.label.argumentN", { n: i + 1 }), cvText(a)))); + if (!t || t.sender !== ctx.me) { + warnings.push({ level: "caution", code: "unknown-call", message: "Clip Wallet can name this call but can't fully read it, so some of its effects may not be shown." }); + } + } + } + + for (const d of pcs) { + lines.push(d.line); + if (d.change) changes.push(d.change); + } + + if (p.type !== "token-transfer") { + if (tx.postConditionMode === PC_MODE.allow) { + warnings.unshift({ level: "danger", code: "unlimited-approval", ...msgWarn(msg("bg.stacks.allowMode", { host: ctx.host })) }); + } else if (tx.postConditionMode === PC_MODE.originator) { + warnings.push({ level: "info", code: "unlimited-approval", ...msgWarn(msg("bg.stacks.originatorMode")) }); + } else if (!tx.postConditions.some((pc) => isMe(pc.principal, ctx.me))) { + lines.push(valueLine("What happens", msg("bg.stacks.nothingLeaves"))); + } + } + + if (sponsored) { + warnings.push({ level: "info", code: "network-matters", message: "Another account pays the network fee for this transaction." }); + lines.push(valueLine("Fee paid by", msg("bg.stacks.sponsorPays", { host: ctx.host }))); + } else { + lines.push({ label: "Network fee", value: fmtStx(fee) }); + if (fee > 10_000_000n) warnings.push({ level: "caution", code: "high-fee", ...msgWarn(msg("bg.stacks.highFee", { fee: fmtStx(fee) })) }); + } + lines.push({ label: "Nonce", value: tx.auth.origin.nonce.toString() }); + + return { title: title.fallback, titleMsg: title, lines, balanceChanges: mergeChanges(changes), warnings, blind, fee, sponsored }; +} + +function msgWarn(m: Msg): { message: string; msg: Msg } { + return { message: m.fallback, msg: m }; +} diff --git a/packages/chains-stacks/src/index.ts b/packages/chains-stacks/src/index.ts new file mode 100644 index 00000000..e617344c --- /dev/null +++ b/packages/chains-stacks/src/index.ts @@ -0,0 +1,37 @@ +export { STACKS_METHODS, type StacksModule, type StacksModuleOptions, type StacksOp, addressOn, buildParts, createStacksModule, normalize, postConditionFrom } from "./module.js"; +export { + STACKS_MAINNET, + STACKS_NETS, + STACKS_NETWORKS, + STACKS_TESTNET, + type StacksNetName, + type StacksNetSpec, + type StacksToken, + explorerTxUrl, + knownToken, + netOf, + stxAsset, + tokenAsset, +} from "./networks.js"; +export { CLARITY_NAME, CONTRACT_NAME, addressOfKey, c32address, c32decode, c32encode, parseAddress, parseAssetId, parseContractId } from "./c32.js"; +export { type CV, cvFrom, cvText, deserializeCV, serializeCV } from "./clarity.js"; +export { + type PostCondition, + type StacksTx, + deserializePostCondition, + deserializeTx, + initialSighash, + originPresignDigest, + serializePostCondition, + serializeTx, + txidOf, + withOriginSignature, +} from "./tx.js"; +export { MESSAGE_PREFIX, messageHash, structuredHash } from "./message.js"; +export { plainStacksError } from "./api.js"; +export { sip10Transfer } from "./describe.js"; + +import { createStacksModule } from "./module.js"; + +/** Default instance. Holds transactions built from a dapp's params between decode and finalize, in memory. */ +export const stacksModule = createStacksModule(); diff --git a/packages/chains-stacks/src/message.ts b/packages/chains-stacks/src/message.ts new file mode 100644 index 00000000..cdb5c8b0 --- /dev/null +++ b/packages/chains-stacks/src/message.ts @@ -0,0 +1,38 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { type CV, serializeCV } from "./clarity.js"; +import { concat, utf8 } from "./util.js"; + +/** + * Message signing as Leather and Xverse do it (SIP-030 `stx_signMessage` / `stx_signStructuredMessage`): + * - plain: SHA-256("\x17Stacks Signed Message:\n" ‖ varint(len) ‖ utf8(message)) (@stacks/encryption + * `hashMessage`/`encodeMessage`; 0x17 = 23 = the prefix's length; varint as Bitcoin's CompactSize). + * - structured (SIP-018): SHA-256("SIP018" ‖ SHA-256(domain) ‖ SHA-256(message)) over the SIP-005 serialization of + * each Clarity value (@stacks/transactions `encodeStructuredDataBytes`). The domain is a tuple with + * name (string-ascii), version (string-ascii) and chain-id (uint). + * The result is "RSV": r ‖ s ‖ recovery id, hex (what @stacks/encryption `verifyMessageSignatureRsv` checks). + */ +export const MESSAGE_PREFIX = "\x17Stacks Signed Message:\n"; +export const SIP018_PREFIX = utf8("SIP018"); + +export function compactSize(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + if (n <= 0xffff) return new Uint8Array([0xfd, n & 0xff, n >> 8]); + return new Uint8Array([0xfe, n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff]); +} + +export function messageHash(message: string | Uint8Array): Uint8Array { + const m = typeof message === "string" ? utf8(message) : message; + return sha256(concat(utf8(MESSAGE_PREFIX), compactSize(m.length), m)); +} + +export function isDomain(v: CV): boolean { + if (v.type !== "tuple") return false; + const { name, version } = v.value; + const chainId = v.value["chain-id"]; + return name?.type === "ascii" && version?.type === "ascii" && chainId?.type === "uint"; +} + +export function structuredHash(domain: CV, message: CV): Uint8Array { + if (!isDomain(domain)) throw new Error("bad SIP-018 domain"); + return sha256(concat(SIP018_PREFIX, sha256(serializeCV(domain)), sha256(serializeCV(message)))); +} diff --git a/packages/chains-stacks/src/module.ts b/packages/chains-stacks/src/module.ts new file mode 100644 index 00000000..a9ae783e --- /dev/null +++ b/packages/chains-stacks/src/module.ts @@ -0,0 +1,658 @@ +import { + type AssetRef, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + WALLET_ORIGIN, + msg, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { Hiro, HiroError, plainStacksError } from "./api.js"; +import { type StacksNet, addressOfKey, netOfVersion, parseAddress, parseAssetId, parseContractId } from "./c32.js"; +import { type CV, cvFrom, cvText } from "./clarity.js"; +import { type DescribeContext, describeTx } from "./describe.js"; +import { isDomain, messageHash, structuredHash } from "./message.js"; +import { STACKS_NETS, knownToken, netOf, stxAsset, tokenAsset } from "./networks.js"; +import { + FT_CODES, + NFT_CODES, + PC_MODE, + POX_CODES, + type Payload, + type PcPrincipal, + type PostCondition, + type StacksTx, + AUTH_SPONSORED, + HASH_MODE_P2PKH, + assetInfoOf, + deserializePostCondition, + deserializeTx, + memoBytes, + originPresignDigest, + recipientCV, + serializePayload, + serializeTx, + txidOf, + unsignedTx, + withOriginSignature, +} from "./tx.js"; +import { bytesEqual, fromHex, hash160, hex, hostOf, isHex, randomId, sleep, toBig, utf8 } from "./util.js"; + +/** + * Request methods: the SIP-030 names (https://github.com/stacksgov/sips/blob/main/sips/sip-030/sip-030-wallet-interface.md), + * which @stacks/connect v8 `request()` sends and WalletConnect's `stacks` namespace uses. Parameters as SIP-030 / + * @stacks/connect `MethodParams`; Clarity values and post-conditions as hex or the SIP-030 JSON form. + */ +export const STACKS_METHODS = { + transferStx: "stx_transferStx", + transferSip10Ft: "stx_transferSip10Ft", + transferSip9Nft: "stx_transferSip9Nft", + callContract: "stx_callContract", + deployContract: "stx_deployContract", + signTransaction: "stx_signTransaction", + signMessage: "stx_signMessage", + signStructuredMessage: "stx_signStructuredMessage", +} as const; + +const BUILD_METHODS = new Set([ + STACKS_METHODS.transferStx, + STACKS_METHODS.transferSip10Ft, + STACKS_METHODS.transferSip9Nft, + STACKS_METHODS.callContract, + STACKS_METHODS.deployContract, +]); + +export interface StacksModuleOptions { + /** Hiro API override (one URL, or per NetworkId). Default: the network's rpcUrls[0]. */ + apiUrl?: string | Record; + /** Token metadata lookups per getBalances call (default 25); the rest show with their contract name. */ + maxMetadataLookups?: number; +} + +export type StacksOp = + | { kind: "build"; method: string; p: Record; broadcast: boolean; sponsored: boolean } + | { kind: "tx"; tx: StacksTx; broadcast: boolean } + | { kind: "unreadable"; reason: unknown } + | { kind: "message"; text: string } + | { kind: "structured"; domain: CV; message: CV }; + +const obj = (p: unknown): Record => (p && typeof p === "object" && !Array.isArray(p) ? (p as Record) : {}); + +function bad(what: string, cause?: unknown): ClipError { + return new ClipError(`This request is missing its ${what}.`, "stacks/bad-params", cause); +} + +function unreadable(cause?: unknown): ClipError { + return new ClipError("This transaction can't be read.", "stacks/bad-transaction", cause); +} + +function netName(networkId: string): StacksNet { + const n = netOf(networkId); + if (!n) throw new ClipError("That network isn't available in this wallet.", "stacks/unknown-network"); + return n; +} + +/** The account's compressed key (Account.publicKey is hex, compressed for secp256k1). */ +function publicKeyOf(ctx: ChainContext): Uint8Array { + const raw = fromHex(ctx.account.publicKey); + return raw.length === 33 ? raw : secp256k1.Point.fromBytes(raw).toBytes(true); +} + +/** This account's address on the context's network (SP… mainnet, ST… testnet). */ +export function addressOn(ctx: ChainContext): string { + return addressOfKey(publicKeyOf(ctx), netName(ctx.network.id)); +} + +/** Checks method, network and account, and parses what can be parsed without the network. */ +export function normalize(request: DappRequest, ctx: ChainContext): StacksOp { + const n = netOf(request.networkId); + if (!n || request.networkId !== ctx.network.id) { + throw new ClipError("This app is asking for a different Stacks network than the one it's connected to.", "stacks/network-mismatch"); + } + const p = obj(request.params); + if (p.network !== undefined && p.network !== null && netOf(String(p.network)) !== n) { + throw new ClipError("This app is asking for a different Stacks network than the one it's connected to.", "stacks/network-mismatch"); + } + if (typeof p.address === "string" && p.address && p.address.trim().toUpperCase() !== addressOn(ctx)) { + throw new ClipError("This request is for a different account than the one you connected.", "stacks/wrong-account"); + } + switch (request.method) { + case STACKS_METHODS.signMessage: + if (typeof p.message !== "string") throw bad("message"); + return { kind: "message", text: p.message }; + case STACKS_METHODS.signStructuredMessage: { + let domain: CV; + let message: CV; + try { + domain = cvFrom(p.domain); + message = cvFrom(p.message); + } catch (e) { + throw new ClipError("This request from the app is malformed, so we stopped it.", "stacks/bad-params", e); + } + if (!isDomain(domain)) throw new ClipError("This request from the app is malformed, so we stopped it.", "stacks/bad-params"); + return { kind: "structured", domain, message }; + } + case STACKS_METHODS.signTransaction: { + const t = p.transaction ?? p.txHex ?? p.tx; + if (typeof t !== "string" || !t) throw bad("transaction"); + const broadcast = p.broadcast === true; + try { + if (!isHex(t)) throw new Error("not hex"); + return { kind: "tx", tx: deserializeTx(fromHex(t)), broadcast }; + } catch (reason) { + return { kind: "unreadable", reason }; + } + } + default: + if (BUILD_METHODS.has(request.method)) { + const sponsored = p.sponsored === true; + return { kind: "build", method: request.method, p, broadcast: !sponsored && p.broadcast !== false, sponsored }; + } + throw new ClipError("Clip Wallet doesn't support this Stacks request yet.", "stacks/unsupported-method"); + } +} + +/* ------------------------------------------------------------------ dapp params → transaction parts */ + +function principalForPc(address: unknown): PcPrincipal { + if (address === "origin") return { kind: "origin" }; + if (typeof address !== "string") throw new Error("bad post-condition address"); + if (address.includes(".")) { + const c = parseContractId(address); + if (!c) throw new Error("bad post-condition contract"); + return { kind: "contract", contract: `${c.address}.${c.name}` }; + } + const a = parseAddress(address); + if (!a) throw new Error("bad post-condition address"); + return { kind: "standard", address: address.trim().toUpperCase() }; +} + +function amountOf(v: unknown): bigint { + const b = toBig(v); + if (b === null || b < 0n || b >= 1n << 64n) throw new Error("bad amount"); + return b; +} + +/** A post-condition from hex (SIP-005) or the SIP-030 JSON form. */ +export function postConditionFrom(input: unknown): PostCondition { + if (typeof input === "string") { + if (!isHex(input)) throw new Error("bad post-condition"); + return deserializePostCondition(fromHex(input)); + } + const o = obj(input); + const principal = principalForPc(o.address); + const code = (table: Record) => { + const c = table[String(o.condition)]; + if (c === undefined) throw new Error("bad post-condition code"); + return c; + }; + const asset = () => { + const a = typeof o.asset === "string" ? assetInfoOf(o.asset) : null; + if (!a) throw new Error("bad post-condition asset"); + return a; + }; + switch (o.type) { + case "stx-postcondition": + return { type: "stx", principal, code: code(FT_CODES), amount: amountOf(o.amount) }; + case "ft-postcondition": + return { type: "ft", principal, asset: asset(), code: code(FT_CODES), amount: amountOf(o.amount) }; + case "nft-postcondition": + return { type: "nft", principal, asset: asset(), assetId: cvFrom(o.assetId), code: code(NFT_CODES) }; + case "staking-postcondition": + return { type: "staking", principal, code: code(FT_CODES), amount: amountOf(o.amount) }; + case "pox-postcondition": + return { type: "pox", principal, code: code(POX_CODES) }; + default: + throw new Error("unknown post-condition type"); + } +} + +function pcMode(v: unknown): number { + if (v === undefined || v === null || v === "deny" || v === PC_MODE.deny) return PC_MODE.deny; + if (v === "allow" || v === PC_MODE.allow) return PC_MODE.allow; + if (v === "originator" || v === PC_MODE.originator) return PC_MODE.originator; + throw new Error("bad post-condition mode"); +} + +/** Payload, post-conditions and mode for a build method's params (no network calls). */ +export function buildParts(method: string, p: Record, me: string): { payload: Payload; postConditions: PostCondition[]; mode: number } { + const recipient = (v: unknown) => { + if (typeof v !== "string") throw new Error("missing recipient"); + return recipientCV(v); + }; + const pcs = Array.isArray(p.postConditions) ? p.postConditions.map((x) => postConditionFrom(x)) : []; + switch (method) { + case STACKS_METHODS.transferStx: { + const amount = amountOf(p.amount); + if (amount === 0n) throw new Error("zero amount"); + const memo = typeof p.memo === "string" ? p.memo : ""; + if (utf8(memo).length > 34) throw new ClipError(msg("bg.stacks.memoTooLong"), "stacks/bad-memo"); + const to = recipient(p.recipient); + // Nodes refuse a transfer to its own sender (MemPoolRejection::TransferRecipientIsSender). + if (to.type === "address" && to.value === me) throw new ClipError("That's your own address.", "stacks/self-transfer"); + return { payload: { type: "token-transfer", recipient: to, amount, memo: memoBytes(memo) }, postConditions: [], mode: PC_MODE.deny }; + } + case STACKS_METHODS.transferSip10Ft: { + const asset = typeof p.asset === "string" ? parseAssetId(p.asset) : null; + if (!asset) throw new Error("bad asset"); + const amount = amountOf(p.amount); + const args: CV[] = [{ type: "uint", value: amount }, { type: "address", value: me }, recipient(p.recipient), { type: "none" }]; + const own: PostCondition = { type: "ft", principal: { kind: "standard", address: me }, asset: { contract: asset.contract, assetName: asset.assetName }, code: FT_CODES.eq, amount }; + return { payload: { type: "contract-call", contract: asset.contract, functionName: "transfer", args }, postConditions: pcs.length ? pcs : [own], mode: pcMode(p.postConditionMode) }; + } + case STACKS_METHODS.transferSip9Nft: { + const asset = typeof p.asset === "string" ? parseAssetId(p.asset) : null; + if (!asset) throw new Error("bad asset"); + const id = cvFrom(p.assetId); + const args: CV[] = [id, { type: "address", value: me }, recipient(p.recipient)]; + const own: PostCondition = { type: "nft", principal: { kind: "standard", address: me }, asset: { contract: asset.contract, assetName: asset.assetName }, assetId: id, code: NFT_CODES.sent }; + return { payload: { type: "contract-call", contract: asset.contract, functionName: "transfer", args }, postConditions: pcs.length ? pcs : [own], mode: pcMode(p.postConditionMode) }; + } + case STACKS_METHODS.callContract: { + const c = typeof p.contract === "string" ? parseContractId(p.contract) : null; + if (!c || typeof p.functionName !== "string" || !p.functionName) throw new Error("bad contract call"); + const args = Array.isArray(p.functionArgs) ? p.functionArgs.map((a) => cvFrom(a)) : []; + return { payload: { type: "contract-call", contract: `${c.address}.${c.name}`, functionName: p.functionName, args }, postConditions: pcs, mode: pcMode(p.postConditionMode) }; + } + case STACKS_METHODS.deployContract: { + if (typeof p.name !== "string" || typeof p.clarityCode !== "string" || !p.clarityCode) throw new Error("bad deploy"); + const v = p.clarityVersion === undefined || p.clarityVersion === null ? undefined : Number(p.clarityVersion); + if (v !== undefined && (!Number.isInteger(v) || v < 1 || v > 255)) throw new Error("bad clarity version"); + return { payload: { type: "smart-contract", name: p.name, code: p.clarityCode, ...(v !== undefined ? { clarityVersion: v } : {}) }, postConditions: pcs, mode: pcMode(p.postConditionMode) }; + } + } + throw new Error("not a build method"); +} + +/* ------------------------------------------------------------------ module */ + +export interface StacksModule extends ChainModule { + normalize: typeof normalize; + /** µSTX: total, locked by stacking, and what can be spent. */ + stxBalance(ctx: ChainContext): Promise<{ balance: string; locked: string; spendable: string }>; + /** Polls /extended/v1/tx until the transaction leaves the mempool (or `attempts` run out). */ + txStatus(ctx: ChainContext, txid: string, opts?: { attempts?: number; intervalMs?: number }): Promise<"pending" | "success" | "failed" | "dropped">; + receiveAddress(ctx: ChainContext): Promise; +} + +export function createStacksModule(options: StacksModuleOptions = {}): StacksModule { + /** Transactions built from a dapp's params (or a wallet send), per request id, so prepare and finalize sign exactly what decode showed. */ + const built = new Map(); + const metaCache = new Map(); + + function hiroFor(ctx: ChainContext): Hiro { + const o = options.apiUrl; + const url = (typeof o === "string" ? o : o?.[ctx.network.id]) ?? ctx.network.rpcUrls[0] ?? STACKS_NETS[netName(ctx.network.id)].api; + return new Hiro(url, ctx.fetch); + } + + async function token(ctx: ChainContext, assetId: string): Promise { + const key = `${ctx.network.id}|${assetId}`; + const hit = metaCache.get(key); + if (hit) return hit; + let asset: AssetRef; + if (knownToken(ctx.network.id, assetId)) asset = tokenAsset(ctx.network.id, assetId); + else { + const meta = await hiroFor(ctx).ftMetadata(assetId.split("::")[0]!); + asset = tokenAsset(ctx.network.id, assetId, meta); + } + metaCache.set(key, asset); + return asset; + } + + async function tokenByContract(ctx: ChainContext, contract: string): Promise { + const known = STACKS_NETS[netName(ctx.network.id)].tokens.find((t) => t.assetId.startsWith(`${contract}::`)); + if (known) return token(ctx, known.assetId); + const meta = await hiroFor(ctx).ftMetadata(contract); + if (!meta || typeof meta.decimals !== "number") return null; + return tokenAsset(ctx.network.id, `${contract}::${meta.symbol ?? "token"}`, meta); + } + + function describeCtx(ctx: ChainContext, origin: string): DescribeContext { + return { + networkId: ctx.network.id, + me: addressOn(ctx), + host: hostOf(origin), + token: (a) => token(ctx, a), + tokenByContract: (c) => tokenByContract(ctx, c), + }; + } + + /** Throws unless this account signs the transaction's origin on this network. */ + function checkTx(tx: StacksTx, ctx: ChainContext): void { + const n = netName(ctx.network.id); + if ((tx.version === 0x00) !== (n === "mainnet") || tx.chainId !== STACKS_NETS[n].chainId) { + throw new ClipError("This app is asking for a different Stacks network than the one it's connected to.", "stacks/network-mismatch"); + } + const o = tx.auth.origin; + const mine = hash160(publicKeyOf(ctx)); + if (o.kind !== "single") { + if (bytesEqual(o.signer, mine)) throw new ClipError(msg("bg.stacks.multisig"), "stacks/unsupported-multisig"); + throw new ClipError(msg("bg.stacks.notYourTx"), "stacks/wrong-account"); + } + if (o.hashMode !== HASH_MODE_P2PKH || !bytesEqual(o.signer, mine)) throw new ClipError(msg("bg.stacks.notYourTx"), "stacks/wrong-account"); + // Recipients and principals in the transaction must be of this network (mainnet SP/SM vs testnet ST/SN). + const p = tx.payload; + const versionOk = (addr: string) => netOfVersion(parseAddress(addr.split(".")[0]!)?.version ?? -1) === n; + if (p.type === "token-transfer" && p.recipient.type !== "none" && "value" in p.recipient && typeof p.recipient.value === "string" && !versionOk(p.recipient.value)) { + throw new ClipError("This transaction is for a different Stacks network, so it wasn't sent.", "stacks/network-mismatch"); + } + if (p.type === "contract-call" && !versionOk(p.contract)) { + throw new ClipError("This transaction is for a different Stacks network, so it wasn't sent.", "stacks/network-mismatch"); + } + } + + async function buildTx(op: Extract, ctx: ChainContext): Promise { + const me = addressOn(ctx); + let parts: ReturnType; + try { + parts = buildParts(op.method, op.p, me); + } catch (e) { + if (e instanceof ClipError) throw e; + throw new ClipError("This request from the app is malformed, so we stopped it.", "stacks/bad-params", e); + } + const n = netName(ctx.network.id); + const hiro = hiroFor(ctx); + const given = toBig(op.p.nonce); + const nonce = given !== null ? given : await hiro.nextNonce(me).catch((e: unknown) => Promise.reject(plainStacksError(e))); + const header = { mainnet: n === "mainnet", chainId: STACKS_NETS[n].chainId, signer: hash160(publicKeyOf(ctx)), nonce, fee: 0n, sponsored: op.sponsored }; + let tx = unsignedTx(header, parts.payload, parts.postConditions, parts.mode); + if (!op.sponsored) { + const given = toBig(op.p.fee); + const fee = given ?? (await hiro.fee(serializePayload(parts.payload), serializeTx(tx).length)); + tx = unsignedTx({ ...header, fee }, parts.payload, parts.postConditions, parts.mode); + } + return tx; + } + + async function txOf(req: DappRequest, op: StacksOp, ctx: ChainContext): Promise { + if (op.kind === "tx") { + checkTx(op.tx, ctx); + return op.tx; + } + if (op.kind === "unreadable") throw unreadable(op.reason); + if (op.kind !== "build") throw new Error("not a transaction"); + let tx = built.get(req.id); + if (!tx) { + tx = await buildTx(op, ctx); + built.set(req.id, tx); + } + checkTx(tx, ctx); + return tx; + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const op = normalize(req, ctx); + const host = hostOf(req.origin); + const base = { requestId: req.id, networkId: req.networkId, simulated: false }; + + if (op.kind === "message") { + const title = msg("bg.req.signMessage", { host }); + return { ...base, title: title.fallback, titleMsg: title, lines: [{ label: "Message", value: op.text }], balanceChanges: [], blind: false, warnings: [] }; + } + if (op.kind === "structured") { + const n = netName(ctx.network.id); + const t = op.domain.type === "tuple" ? op.domain.value : {}; + const name = t.name?.type === "ascii" ? t.name.value : ""; + const version = t.version?.type === "ascii" ? t.version.value : ""; + const chain = t["chain-id"]?.type === "uint" ? t["chain-id"].value : -1n; + const title = msg("bg.req.signData", { host }); + const warnings: DecodedRequest["warnings"] = []; + if (chain !== BigInt(STACKS_NETS[n].chainId)) { + warnings.push({ level: "danger", code: "network-matters", message: "This signature is for a different network than the one selected." }); + } + return { + ...base, + title: title.fallback, + titleMsg: title, + lines: [ + { label: "App", value: `${name} ${version}`.trim() }, + { label: "Data", value: cvText(op.message, 600) }, + ], + balanceChanges: [], + blind: false, + warnings, + }; + } + if (op.kind === "unreadable") { + const title = msg("bg.req.unreadableFrom", { host }); + return { ...base, title: title.fallback, titleMsg: title, lines: [], balanceChanges: [], blind: true, warnings: [{ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction." }] }; + } + + const tx = await txOf(req, op, ctx); + const d = await describeTx(tx, describeCtx(ctx, req.origin)); + const lines = [...d.lines]; + if (op.kind === "tx" && !op.broadcast) lines.push({ label: "Sent by", value: `${host} (it gets the signed transaction)` }); + if (op.kind === "build" && op.sponsored) lines.push({ label: "Sent by", value: `${host} (it gets the signed transaction)` }); + return { + ...base, + title: d.title, + ...(d.titleMsg ? { titleMsg: d.titleMsg } : {}), + lines, + balanceChanges: d.balanceChanges, + fee: d.sponsored ? { asset: stxAsset(ctx.network.id), amount: "0", sponsored: true } : { asset: stxAsset(ctx.network.id), amount: d.fee.toString() }, + blind: d.blind, + warnings: d.warnings, + }; + } + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + const op = normalize(req, ctx); + const one = (bytes: Uint8Array): SignablePayload[] => [{ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes, approvalId }]; + if (op.kind === "message") return one(messageHash(op.text)); + if (op.kind === "structured") return one(structuredHash(op.domain, op.message)); + const tx = await txOf(req, op, ctx); + return one(originPresignDigest(tx)); + } + + /** Verifies the vault's signature over `digest` and returns (r‖s low-S, recovery id) for this account's key. */ + function checkSignature(sig: Signature | undefined, digest: Uint8Array, ctx: ChainContext): { rs: Uint8Array; recovery: number } { + const pub = publicKeyOf(ctx); + if (!sig || sig.scheme !== "ecdsa-secp256k1" || sig.bytes.length !== 64) throw new ClipError("The signature didn't match. Nothing was sent.", "stacks/bad-signature"); + let s = secp256k1.Signature.fromBytes(sig.bytes, "compact"); + if (s.hasHighS()) s = new secp256k1.Signature(s.r, secp256k1.Point.CURVE().n - s.s); + const rs = s.toBytes("compact"); + if (!secp256k1.verify(rs, digest, pub, { prehash: false })) throw new ClipError("The signature didn't match. Nothing was sent.", "stacks/bad-signature"); + for (const rec of [0, 1]) { + try { + if (bytesEqual(s.addRecoveryBit(rec).recoverPublicKey(digest).toBytes(true), pub)) return { rs, recovery: rec }; + } catch { + /* next */ + } + } + throw new ClipError("The signature didn't match. Nothing was sent.", "stacks/bad-signature"); + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const op = normalize(req, ctx); + const pubHex = hex(publicKeyOf(ctx)); + if (op.kind === "message" || op.kind === "structured") { + const digest = op.kind === "message" ? messageHash(op.text) : structuredHash(op.domain, op.message); + const { rs, recovery } = checkSignature(signatures[0], digest, ctx); + return { signature: hex(rs) + recovery.toString(16).padStart(2, "0"), publicKey: pubHex }; + } + if (op.kind === "build" && !built.has(req.id)) { + throw new ClipError("This request wasn't prepared for signing (or was already sent). Nothing was sent. Try again.", "stacks/not-prepared"); + } + const tx = await txOf(req, op, ctx); + const digest = originPresignDigest(tx); + const { rs, recovery } = checkSignature(signatures[0], digest, ctx); + const vrs = new Uint8Array(65); + vrs[0] = recovery; + vrs.set(rs, 1); + const signed = withOriginSignature(tx, vrs); + const raw = serializeTx(signed); + const txid = hex(txidOf(raw)); + built.delete(req.id); + const broadcast = op.kind === "tx" ? op.broadcast : op.kind === "build" && op.broadcast; + if (!broadcast || signed.auth.type === AUTH_SPONSORED) return { transaction: hex(raw), txid }; + let sent: string; + try { + sent = await hiroFor(ctx).broadcast(raw); + } catch (e) { + throw plainStacksError(e); + } + return { txid: sent || txid, transaction: hex(raw) }; + } + + async function getBalances(ctx: ChainContext): Promise { + const me = addressOn(ctx); + const hiro = hiroFor(ctx); + let b; + try { + b = await hiro.balances(me); + } catch (e) { + if (e instanceof HiroError) { + // Fall back to the node for STX alone when the indexer is down. + const a = await hiro.account(me).catch(() => null); + if (a) return [{ asset: stxAsset(ctx.network.id), amount: a.balance.toString() }]; + } + throw e instanceof ClipError ? e : plainStacksError(e); + } + const out: TokenBalance[] = [{ asset: stxAsset(ctx.network.id), amount: String(toBig(b.stx?.balance) ?? 0n) }]; + let lookups = options.maxMetadataLookups ?? 25; + for (const [assetId, ft] of Object.entries(b.fungible_tokens ?? {})) { + const amount = toBig(ft.balance); + if (!amount || !parseAssetId(assetId)) continue; + let asset: AssetRef; + if (knownToken(ctx.network.id, assetId) || metaCache.has(`${ctx.network.id}|${assetId}`) || lookups-- > 0) asset = await token(ctx, assetId); + else asset = tokenAsset(ctx.network.id, assetId); + out.push({ asset, amount: amount.toString() }); + } + return out; + } + + async function stxBalance(ctx: ChainContext) { + const a = await hiroFor(ctx).account(addressOn(ctx)); + const spendable = a.balance > a.locked ? a.balance - a.locked : 0n; + return { balance: a.balance.toString(), locked: a.locked.toString(), spendable: spendable.toString() }; + } + + async function txStatus(ctx: ChainContext, txid: string, opts: { attempts?: number; intervalMs?: number } = {}) { + const hiro = hiroFor(ctx); + const attempts = opts.attempts ?? 10; + for (let i = 0; i < attempts; i++) { + const s = await hiro.tx(txid).catch(() => null); + const st = s?.tx_status; + if (st === "success") return "success" as const; + if (st && st.startsWith("abort")) return "failed" as const; + if (st && st.startsWith("dropped")) return "dropped" as const; + if (i < attempts - 1) await sleep(opts.intervalMs ?? 2000); + } + return "pending" as const; + } + + function checkRecipient(to: string, ctx: ChainContext): string { + const t = to.trim(); + const n = netName(ctx.network.id); + const contract = t.includes(".") ? parseContractId(t) : null; + const addr = contract ?? parseAddress(t); + if (!addr) throw new ClipError("That doesn't look like a Stacks address.", "stacks/bad-address"); + const v = netOfVersion(addr.version); + if (v !== n) { + throw new ClipError( + msg(n === "mainnet" ? "bg.stacks.testAddressOnMainnet" : "bg.stacks.mainAddressOnTestnet"), + "stacks/network-mismatch", + ); + } + const canonical = contract ? `${contract.address}.${contract.name}` : t.toUpperCase(); + if (canonical === addressOn(ctx)) throw new ClipError("That's your own address.", "stacks/self-transfer"); + return canonical; + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + const to = checkRecipient(p.to, ctx); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "stacks/bad-amount"); + const amount = BigInt(p.amount); + if (amount >= 1n << 64n) throw new ClipError("That amount isn't valid. Check it and try again.", "stacks/bad-amount"); + const me = addressOn(ctx); + const n = netName(ctx.network.id); + const hiro = hiroFor(ctx); + let account; + let nonce; + try { + [account, nonce] = await Promise.all([hiro.account(me), hiro.nextNonce(me)]); + } catch (e) { + throw plainStacksError(e); + } + const spendable = account.balance > account.locked ? account.balance - account.locked : 0n; + const header = { mainnet: n === "mainnet", chainId: STACKS_NETS[n].chainId, signer: hash160(publicKeyOf(ctx)), nonce, fee: 0n }; + + let payload: Payload; + let pcs: PostCondition[] = []; + if (!p.asset.address) { + payload = { type: "token-transfer", recipient: recipientCV(to), amount, memo: memoBytes("") }; + } else { + const a = parseAssetId(p.asset.address); + if (!a) throw new ClipError("That isn't a Stacks token.", "stacks/bad-asset"); + const held = toBig((await hiro.balances(me).catch((e) => Promise.reject(plainStacksError(e)))).fungible_tokens?.[p.asset.address]?.balance) ?? 0n; + if (held < amount) throw new ClipError(msg("bg.err.notEnough", { symbol: p.asset.symbol }), "stacks/insufficient-token"); + payload = { type: "contract-call", contract: a.contract, functionName: "transfer", args: [{ type: "uint", value: amount }, { type: "address", value: me }, recipientCV(to), { type: "none" }] }; + pcs = [{ type: "ft", principal: { kind: "standard", address: me }, asset: { contract: a.contract, assetName: a.assetName }, code: FT_CODES.eq, amount }]; + } + const draft = unsignedTx(header, payload, pcs); + const fee = await hiro.fee(serializePayload(payload), serializeTx(draft).length + 65); + const need = (p.asset.address ? 0n : amount) + fee; + if (need > spendable) { + throw new ClipError( + p.asset.address ? "You need a little STX to pay the network fee." : msg("bg.err.notEnoughForFee", { symbol: "STX" }), + "stacks/insufficient-funds", + ); + } + const tx = unsignedTx({ ...header, fee }, payload, pcs); + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "stacks", + networkId: ctx.network.id, + method: STACKS_METHODS.signTransaction, + params: { transaction: hex(serializeTx(tx)), broadcast: true }, + }; + } + + function addressFromPublicKey(publicKey: Uint8Array, network: Network): string { + const key = publicKey.length === 33 ? publicKey : secp256k1.Point.fromBytes(publicKey).toBytes(true); + return addressOfKey(key, netName(network.id)); + } + + function principalOk(value: string): ReturnType { + const v = value.trim(); + if (v.includes(".")) return parseContractId(v); + return parseAddress(v); + } + + return { + family: "stacks", + curve: "secp256k1", + /** Leather, Xverse and @stacks/wallet-sdk account i (BIP-44, coin 5757). */ + derivationPath: (index: number) => `m/44'/5757'/0'/0/${index}`, + addressFromPublicKey, + /** Standard (SP/ST/SM/SN) and contract principals, checksum verified. */ + isAddress: (value: string) => principalOk(value) !== null, + /** SP/SM → mainnet, ST/SN → testnet. */ + networksForAddress: (value: string, candidates: Network[]) => { + const a = principalOk(value); + const n = a ? netOfVersion(a.version) : null; + return n ? candidates.filter((c) => c.family === "stacks" && netOf(c.id) === n) : []; + }, + getBalances, + getNfts: async (): Promise => [], + decode, + prepare, + finalize, + buildTransfer, + /** The account's address is spelled per network: SP… on mainnet, ST… on testnet. */ + receiveAddress: async (ctx: ChainContext) => addressOn(ctx), + normalize, + stxBalance, + txStatus, + }; +} diff --git a/packages/chains-stacks/src/networks.ts b/packages/chains-stacks/src/networks.ts new file mode 100644 index 00000000..3fa2b5b1 --- /dev/null +++ b/packages/chains-stacks/src/networks.ts @@ -0,0 +1,123 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * Stacks networks. + * - CAIP-2 (ChainAgnostic namespaces, stacks/caip2.md): "stacks:" + the chain id in decimal, the `network_id` of + * GET /v2/info. Test cases there: mainnet `stacks:1`, testnet `stacks:2147483648` (0x80000000). Checked on + * api.hiro.so and api.testnet.hiro.so (2026-10). + * - Transaction version byte (SIP-005): 0x00 mainnet, 0x80 testnet. Address versions: SP (22) / ST (26). + * - Hiro's public API needs no key for these read and broadcast calls (rate-limited per IP). + */ +export type StacksNetName = "mainnet" | "testnet"; + +export interface StacksNetSpec { + caip2: NetworkId; + chainId: number; + api: string; + explorer: string; + /** Curated SIP-010 tokens (the rest of an account's tokens are shown as spam-checked "other"). */ + tokens: StacksToken[]; +} + +export interface StacksToken { + /** "SP….contract::asset-name" */ + assetId: string; + key: string; + symbol: string; + name: string; + decimals: number; + bridged?: boolean; +} + +export const STACKS_NETS: Record = { + mainnet: { + caip2: "stacks:1", + chainId: 0x00000001, + api: "https://api.hiro.so", + explorer: "https://explorer.hiro.so", + tokens: [ + // sBTC: docs.stacks.co/learn/sbtc/clarity-contracts ("Deployed Mainnet Contracts"), 8 decimals, 1:1 BTC. + { assetId: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token", key: "sbtc", symbol: "sBTC", name: "sBTC", decimals: 8, bridged: true }, + // USDCx (Circle xReserve): docs.stacks.co/learn/bridging/usdcx/contracts, mainnet token contract. + { assetId: "SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx::usdcx-token", key: "usdcx", symbol: "USDCx", name: "USDCx", decimals: 6, bridged: true }, + // aeUSDC: Allbridge's bridged USDC (Ethereum), the older bridged dollar on Stacks; its own key. + { assetId: "SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA4K.token-aeusdc::aeUSDC", key: "aeusdc", symbol: "aeUSDC", name: "Ethereum USDC via Allbridge", decimals: 6, bridged: true }, + ], + }, + testnet: { + caip2: "stacks:2147483648", + chainId: 0x80000000, + api: "https://api.testnet.hiro.so", + explorer: "https://explorer.hiro.so", + tokens: [ + // USDCx testnet token contract (same docs page). sBTC has no published testnet deployment (only mocks). + { assetId: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx::usdcx-token", key: "usdcx", symbol: "USDCx", name: "USDCx", decimals: 6, bridged: true }, + ], + }, +}; + +export function stxAsset(networkId: NetworkId): AssetRef { + return { key: "stx", symbol: "STX", name: "Stacks", decimals: 6, networkId }; +} + +function network(net: StacksNetName): Network { + const c = STACKS_NETS[net]; + return { + id: c.caip2, + family: "stacks", + name: net === "mainnet" ? "Stacks" : "Stacks Testnet", + nativeAsset: stxAsset(c.caip2), + testnet: net !== "mainnet", + rpcUrls: [c.api], + explorerUrl: net === "mainnet" ? c.explorer : `${c.explorer}/?chain=testnet`, + indexerUrl: c.api, + }; +} + +export const STACKS_MAINNET = network("mainnet"); +export const STACKS_TESTNET = network("testnet"); +export const STACKS_NETWORKS: Network[] = [STACKS_TESTNET, STACKS_MAINNET]; + +/** Accepts a CAIP-2 id, a numeric chain id, or the SIP-030 network names ("mainnet", "testnet"). */ +export function netOf(id: string | number | undefined): StacksNetName | null { + if (id === undefined) return null; + if (typeof id === "number") return id === 1 ? "mainnet" : id === 0x80000000 ? "testnet" : null; + const s = id.trim().toLowerCase(); + if (s === "mainnet" || s === "stacks:1" || s === "1") return "mainnet"; + if (s === "testnet" || s === "stacks:2147483648" || s === "2147483648") return "testnet"; + return null; +} + +export function specFor(networkId: NetworkId): StacksNetSpec | null { + const n = netOf(networkId); + return n ? STACKS_NETS[n] : null; +} + +export function explorerTxUrl(networkId: NetworkId, txid: string): string { + const n = netOf(networkId) ?? "testnet"; + const id = txid.startsWith("0x") ? txid : `0x${txid}`; + return `${STACKS_NETS[n].explorer}/txid/${id}${n === "testnet" ? "?chain=testnet" : ""}`; +} + +/** The curated token for "SP….contract::asset", if any. */ +export function knownToken(networkId: NetworkId, assetId: string): StacksToken | undefined { + return specFor(networkId)?.tokens.find((t) => t.assetId === assetId); +} + +/** AssetRef for a SIP-010 token. Unknown tokens named like a curated one (or like STX) are spam. */ +export function tokenAsset(networkId: NetworkId, assetId: string, meta?: { symbol?: string; name?: string; decimals?: number } | null): AssetRef { + const known = knownToken(networkId, assetId); + if (known) return { key: known.key, symbol: known.symbol, name: known.name, decimals: known.decimals, networkId, address: assetId, ...(known.bridged ? { bridged: true } : {}) }; + const symbol = (meta?.symbol || assetId.split("::")[1] || "token").slice(0, 32); + const name = (meta?.name || symbol).slice(0, 64); + const lookalike = /^(stx|wstx|sbtc|btc|usdc|usdcx|aeusdc|usdt|usdh)$/i.test(symbol.replace(/[^a-z]/gi, "")) || /\b(sbtc|usdc|stacks)\b/i.test(name); + return { + key: `sip10:${assetId}`, + symbol, + name, + decimals: Number.isInteger(meta?.decimals) && meta!.decimals! >= 0 && meta!.decimals! <= 38 ? meta!.decimals! : 0, + networkId, + address: assetId, + ...(lookalike ? { spam: true } : {}), + }; +} diff --git a/packages/chains-stacks/src/tx.ts b/packages/chains-stacks/src/tx.ts new file mode 100644 index 00000000..900cf928 --- /dev/null +++ b/packages/chains-stacks/src/tx.ts @@ -0,0 +1,420 @@ +import { sha512_256 } from "@noble/hashes/sha2.js"; +import { c32address, parseAddress, parseAssetId, parseContractId, CLARITY_NAME, CONTRACT_NAME } from "./c32.js"; +import { type CV, principalBytes, readCV, serializeCV } from "./clarity.js"; +import { Reader, concat, uintBE, utf8 } from "./util.js"; + +/** + * Stacks transactions, hand-written from SIP-005 ("Transaction Encoding", + * https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md) and cross-checked + * byte for byte against @stacks/transactions 7.6 in the tests (serializeTransaction, txid, sigHashPreSign). + * + * version(1: 0x00 mainnet, 0x80 testnet) ‖ chain id(4) ‖ authorization ‖ anchor mode(1) ‖ post-condition mode(1) + * ‖ post-conditions(u32 count + each) ‖ payload + */ + +export const AUTH_STANDARD = 0x04; +export const AUTH_SPONSORED = 0x05; +export const HASH_MODE_P2PKH = 0x00; +export const KEY_COMPRESSED = 0x00; +export const ANCHOR_ANY = 0x03; +export const PC_MODE = { allow: 0x01, deny: 0x02, originator: 0x03 } as const; +export const MEMO_BYTES = 34; + +export const FT_CODES = { eq: 0x01, gt: 0x02, gte: 0x03, lt: 0x04, lte: 0x05 } as const; +export const NFT_CODES = { sent: 0x10, "not-sent": 0x11, "maybe-sent": 0x12 } as const; +export const POX_CODES = { "will-not-perform": 0x30, "may-perform": 0x31, "will-perform": 0x32 } as const; + +export interface SingleSig { + hashMode: number; + signer: Uint8Array; + nonce: bigint; + fee: bigint; + keyEncoding: number; + /** 65 bytes, recovery id ‖ r ‖ s ("VRS", stacks.js `createMessageSignature`); all zero before signing. */ + signature: Uint8Array; +} + +export interface MultiSig { + hashMode: number; + signer: Uint8Array; + nonce: bigint; + fee: bigint; + fields: { type: number; data: Uint8Array }[]; + required: number; +} + +export type SpendingCondition = ({ kind: "single" } & SingleSig) | ({ kind: "multi" } & MultiSig); + +export interface Auth { + type: typeof AUTH_STANDARD | typeof AUTH_SPONSORED; + origin: SpendingCondition; + sponsor?: SpendingCondition; +} + +export type PcPrincipal = { kind: "origin" } | { kind: "standard"; address: string } | { kind: "contract"; contract: string }; + +export interface AssetInfo { + /** "SP….contract" */ + contract: string; + assetName: string; +} + +export type PostCondition = + | { type: "stx"; principal: PcPrincipal; code: number; amount: bigint } + | { type: "ft"; principal: PcPrincipal; asset: AssetInfo; code: number; amount: bigint } + | { type: "nft"; principal: PcPrincipal; asset: AssetInfo; assetId: CV; code: number } + /** SIP-045 (Epoch 4.0): staking (code + µSTX) and PoX (code only). */ + | { type: "staking"; principal: PcPrincipal; code: number; amount: bigint } + | { type: "pox"; principal: PcPrincipal; code: number }; + +export type Payload = + | { type: "token-transfer"; recipient: CV; amount: bigint; memo: Uint8Array } + | { type: "contract-call"; contract: string; functionName: string; args: CV[] } + | { type: "smart-contract"; name: string; code: string; clarityVersion?: number }; + +export interface StacksTx { + version: number; + chainId: number; + auth: Auth; + anchorMode: number; + postConditionMode: number; + postConditions: PostCondition[]; + payload: Payload; +} + +const EMPTY_SIG = () => new Uint8Array(65); + +/* ------------------------------------------------------------------ serialize */ + +function lp1(s: string): Uint8Array { + const b = utf8(s); + if (b.length > 128) throw new Error("name too long"); + return concat(new Uint8Array([b.length]), b); +} + +function serializeCondition(c: SpendingCondition): Uint8Array { + const head = concat(new Uint8Array([c.hashMode]), c.signer, uintBE(c.nonce, 8), uintBE(c.fee, 8)); + if (c.kind === "single") { + if (c.signature.length !== 65) throw new Error("bad signature length"); + return concat(head, new Uint8Array([c.keyEncoding]), c.signature); + } + return concat( + head, + uintBE(BigInt(c.fields.length), 4), + ...c.fields.map((f) => concat(new Uint8Array([f.type]), f.data)), + uintBE(BigInt(c.required), 2), + ); +} + +function serializeAuth(a: Auth): Uint8Array { + if (a.type === AUTH_SPONSORED) { + if (!a.sponsor) throw new Error("sponsored transaction without a sponsor condition"); + return concat(new Uint8Array([a.type]), serializeCondition(a.origin), serializeCondition(a.sponsor)); + } + return concat(new Uint8Array([a.type]), serializeCondition(a.origin)); +} + +function serializePcPrincipal(p: PcPrincipal): Uint8Array { + if (p.kind === "origin") return new Uint8Array([0x01]); + if (p.kind === "standard") return concat(new Uint8Array([0x02]), principalBytes(p.address)); + const c = parseContractId(p.contract); + if (!c) throw new Error("bad contract principal"); + return concat(new Uint8Array([0x03, c.version]), c.hash160, lp1(c.name)); +} + +function serializeAsset(a: AssetInfo): Uint8Array { + const c = parseContractId(a.contract); + if (!c) throw new Error("bad asset contract"); + return concat(new Uint8Array([c.version]), c.hash160, lp1(c.name), lp1(a.assetName)); +} + +export function serializePostCondition(pc: PostCondition): Uint8Array { + const typeByte = { stx: 0x00, ft: 0x01, nft: 0x02, staking: 0x03, pox: 0x04 }[pc.type]; + const parts = [new Uint8Array([typeByte]), serializePcPrincipal(pc.principal)]; + if (pc.type === "ft" || pc.type === "nft") parts.push(serializeAsset(pc.asset)); + if (pc.type === "nft") parts.push(serializeCV(pc.assetId)); + parts.push(new Uint8Array([pc.code])); + if (pc.type === "stx" || pc.type === "ft" || pc.type === "staking") parts.push(uintBE(pc.amount, 8)); + return concat(...parts); +} + +export function serializePayload(p: Payload): Uint8Array { + switch (p.type) { + case "token-transfer": { + if (p.memo.length !== MEMO_BYTES) throw new Error("memo must be 34 bytes"); + return concat(new Uint8Array([0x00]), serializeCV(p.recipient), uintBE(p.amount, 8), p.memo); + } + case "contract-call": { + const c = parseContractId(p.contract); + if (!c) throw new Error("bad contract"); + return concat( + new Uint8Array([0x02, c.version]), + c.hash160, + lp1(c.name), + lp1(p.functionName), + uintBE(BigInt(p.args.length), 4), + ...p.args.map(serializeCV), + ); + } + case "smart-contract": { + const code = utf8(p.code); + const body = concat(lp1(p.name), uintBE(BigInt(code.length), 4), code); + return p.clarityVersion === undefined ? concat(new Uint8Array([0x01]), body) : concat(new Uint8Array([0x06, p.clarityVersion]), body); + } + } +} + +export function serializeTx(tx: StacksTx): Uint8Array { + return concat( + new Uint8Array([tx.version]), + uintBE(BigInt(tx.chainId), 4), + serializeAuth(tx.auth), + new Uint8Array([tx.anchorMode, tx.postConditionMode]), + uintBE(BigInt(tx.postConditions.length), 4), + ...tx.postConditions.map(serializePostCondition), + serializePayload(tx.payload), + ); +} + +/** Transaction id: SHA-512/256 of the serialized transaction (hex, no 0x). */ +export function txidOf(tx: StacksTx | Uint8Array): Uint8Array { + return sha512_256(tx instanceof Uint8Array ? tx : serializeTx(tx)); +} + +/* ------------------------------------------------------------------ deserialize */ + +function readCondition(r: Reader): SpendingCondition { + const hashMode = r.u8(); + const signer = r.read(20).slice(); + const nonce = r.u64(); + const fee = r.u64(); + if (hashMode === 0x00 || hashMode === 0x02) { + const keyEncoding = r.u8(); + if (keyEncoding !== 0x00 && keyEncoding !== 0x01) throw new Error("bad key encoding"); + return { kind: "single", hashMode, signer, nonce, fee, keyEncoding, signature: r.read(65).slice() }; + } + if (hashMode === 0x01 || hashMode === 0x03 || hashMode === 0x05 || hashMode === 0x07) { + const n = r.u32(); + if (n > 64) throw new Error("too many multisig fields"); + const fields: { type: number; data: Uint8Array }[] = []; + for (let i = 0; i < n; i++) { + const type = r.u8(); + if (type > 0x03) throw new Error("bad auth field"); + fields.push({ type, data: r.read(type <= 0x01 ? 33 : 65).slice() }); + } + return { kind: "multi", hashMode, signer, nonce, fee, fields, required: r.u16() }; + } + throw new Error("unknown hash mode"); +} + +function readPcPrincipal(r: Reader): PcPrincipal { + const kind = r.u8(); + if (kind === 0x01) return { kind: "origin" }; + const version = r.u8(); + const address = c32address(version, r.read(20)); + if (kind === 0x02) return { kind: "standard", address }; + if (kind === 0x03) { + const name = new TextDecoder().decode(r.read(r.u8())); + if (!CONTRACT_NAME.test(name)) throw new Error("bad contract name"); + return { kind: "contract", contract: `${address}.${name}` }; + } + throw new Error("bad post-condition principal"); +} + +function readAsset(r: Reader): AssetInfo { + const version = r.u8(); + const address = c32address(version, r.read(20)); + const contractName = new TextDecoder().decode(r.read(r.u8())); + const assetName = new TextDecoder().decode(r.read(r.u8())); + if (!CONTRACT_NAME.test(contractName) || !CLARITY_NAME.test(assetName)) throw new Error("bad asset name"); + return { contract: `${address}.${contractName}`, assetName }; +} + +function readPostCondition(r: Reader): PostCondition { + const type = r.u8(); + const principal = readPcPrincipal(r); + switch (type) { + case 0x00: { + const code = r.u8(); + if (code < 0x01 || code > 0x05) throw new Error("bad condition code"); + return { type: "stx", principal, code, amount: r.u64() }; + } + case 0x01: { + const asset = readAsset(r); + const code = r.u8(); + if (code < 0x01 || code > 0x05) throw new Error("bad condition code"); + return { type: "ft", principal, asset, code, amount: r.u64() }; + } + case 0x02: { + const asset = readAsset(r); + const assetId = readCV(r); + const code = r.u8(); + if (code < 0x10 || code > 0x12) throw new Error("bad condition code"); + return { type: "nft", principal, asset, assetId, code }; + } + case 0x03: { + const code = r.u8(); + if (code < 0x01 || code > 0x05) throw new Error("bad condition code"); + return { type: "staking", principal, code, amount: r.u64() }; + } + case 0x04: { + const code = r.u8(); + if (code < 0x30 || code > 0x32) throw new Error("bad condition code"); + return { type: "pox", principal, code }; + } + default: + throw new Error("unknown post-condition type"); + } +} + +function readPayload(r: Reader): Payload { + const type = r.u8(); + switch (type) { + case 0x00: { + const recipient = readCV(r); + if (recipient.type !== "address" && recipient.type !== "contract") throw new Error("bad recipient"); + return { type: "token-transfer", recipient, amount: r.u64(), memo: r.read(MEMO_BYTES).slice() }; + } + case 0x02: { + const version = r.u8(); + const address = c32address(version, r.read(20)); + const name = new TextDecoder().decode(r.read(r.u8())); + const functionName = new TextDecoder().decode(r.read(r.u8())); + if (!CONTRACT_NAME.test(name) || !CLARITY_NAME.test(functionName)) throw new Error("bad contract call"); + const n = r.u32(); + if (n > 256) throw new Error("too many arguments"); + const args: CV[] = []; + for (let i = 0; i < n; i++) args.push(readCV(r)); + return { type: "contract-call", contract: `${address}.${name}`, functionName, args }; + } + case 0x01: + case 0x06: { + const clarityVersion = type === 0x06 ? r.u8() : undefined; + const name = new TextDecoder().decode(r.read(r.u8())); + if (!CONTRACT_NAME.test(name)) throw new Error("bad contract name"); + const code = new TextDecoder("utf-8", { fatal: true }).decode(r.read(r.u32())); + return clarityVersion === undefined ? { type: "smart-contract", name, code } : { type: "smart-contract", name, code, clarityVersion }; + } + default: + // Coinbase, tenure change, poison microblock: never a wallet's to sign. + throw new Error("unsupported payload"); + } +} + +export function deserializeTx(bytes: Uint8Array): StacksTx { + const r = new Reader(bytes); + const version = r.u8(); + if (version !== 0x00 && version !== 0x80) throw new Error("bad transaction version"); + const chainId = r.u32(); + const authType = r.u8(); + if (authType !== AUTH_STANDARD && authType !== AUTH_SPONSORED) throw new Error("bad auth type"); + const origin = readCondition(r); + const auth: Auth = authType === AUTH_SPONSORED ? { type: AUTH_SPONSORED, origin, sponsor: readCondition(r) } : { type: AUTH_STANDARD, origin }; + const anchorMode = r.u8(); + if (anchorMode < 0x01 || anchorMode > 0x03) throw new Error("bad anchor mode"); + const postConditionMode = r.u8(); + if (postConditionMode < 0x01 || postConditionMode > 0x03) throw new Error("bad post-condition mode"); + const n = r.u32(); + if (n > 1024) throw new Error("too many post-conditions"); + const postConditions: PostCondition[] = []; + for (let i = 0; i < n; i++) postConditions.push(readPostCondition(r)); + const payload = readPayload(r); + if (!r.done) throw new Error("trailing bytes"); + return { version, chainId, auth, anchorMode, postConditionMode, postConditions, payload }; +} + +/* ------------------------------------------------------------------ signing hashes */ + +function clearCondition(c: SpendingCondition): SpendingCondition { + return c.kind === "single" ? { ...c, nonce: 0n, fee: 0n, signature: EMPTY_SIG() } : { ...c, nonce: 0n, fee: 0n, fields: [] }; +} + +/** + * The initial sighash: the txid of the transaction with the origin condition cleared (nonce, fee and signature + * zeroed) and, when sponsored, the sponsor condition replaced by an empty P2PKH one (stacks.js + * `intoInitialSighashAuth`; stacks-core `TransactionAuth::into_initial_sighash_auth`). + */ +export function initialSighash(tx: StacksTx): Uint8Array { + const sponsor: SpendingCondition = { kind: "single", hashMode: HASH_MODE_P2PKH, signer: new Uint8Array(20), nonce: 0n, fee: 0n, keyEncoding: KEY_COMPRESSED, signature: EMPTY_SIG() }; + const auth: Auth = tx.auth.type === AUTH_SPONSORED ? { type: AUTH_SPONSORED, origin: clearCondition(tx.auth.origin), sponsor } : { type: AUTH_STANDARD, origin: clearCondition(tx.auth.origin) }; + return txidOf({ ...tx, auth }); +} + +/** + * The digest the origin's key signs: SHA-512/256(initial sighash ‖ auth flag ‖ fee(8) ‖ nonce(8)) (stacks.js + * `sigHashPreSign`). The origin always signs with the STANDARD flag (0x04), sponsored or not (`signNextOrigin`). + */ +export function originPresignDigest(tx: StacksTx): Uint8Array { + const o = tx.auth.origin; + return sha512_256(concat(initialSighash(tx), new Uint8Array([AUTH_STANDARD]), uintBE(o.fee, 8), uintBE(o.nonce, 8))); +} + +/** A copy of `tx` with the origin's single-sig signature set (VRS = recovery ‖ r ‖ s). */ +export function withOriginSignature(tx: StacksTx, vrs: Uint8Array): StacksTx { + if (tx.auth.origin.kind !== "single") throw new Error("not a single-signature origin"); + if (vrs.length !== 65) throw new Error("bad signature length"); + return { ...tx, auth: { ...tx.auth, origin: { ...tx.auth.origin, keyEncoding: KEY_COMPRESSED, signature: vrs } } }; +} + +/* ------------------------------------------------------------------ builders */ + +export function memoBytes(memo: string | undefined): Uint8Array { + const out = new Uint8Array(MEMO_BYTES); + if (!memo) return out; + const b = utf8(memo); + if (b.length > MEMO_BYTES) throw new Error("memo too long"); + out.set(b); + return out; +} + +export function memoText(memo: Uint8Array): string { + let end = memo.length; + while (end > 0 && memo[end - 1] === 0) end--; + if (!end) return ""; + try { + return new TextDecoder("utf-8", { fatal: true }).decode(memo.subarray(0, end)); + } catch { + return ""; + } +} + +export interface Header { + mainnet: boolean; + chainId: number; + signer: Uint8Array; + nonce: bigint; + fee: bigint; + sponsored?: boolean; +} + +export function unsignedTx(h: Header, payload: Payload, postConditions: PostCondition[] = [], postConditionMode: number = PC_MODE.deny): StacksTx { + const origin: SpendingCondition = { kind: "single", hashMode: HASH_MODE_P2PKH, signer: h.signer, nonce: h.nonce, fee: h.sponsored ? 0n : h.fee, keyEncoding: KEY_COMPRESSED, signature: EMPTY_SIG() }; + const auth: Auth = h.sponsored + ? { type: AUTH_SPONSORED, origin, sponsor: { kind: "single", hashMode: HASH_MODE_P2PKH, signer: new Uint8Array(20), nonce: 0n, fee: 0n, keyEncoding: KEY_COMPRESSED, signature: EMPTY_SIG() } } + : { type: AUTH_STANDARD, origin }; + return { version: h.mainnet ? 0x00 : 0x80, chainId: h.chainId, auth, anchorMode: ANCHOR_ANY, postConditionMode, postConditions, payload }; +} + +/** Recipient of an STX transfer: a standard or contract principal (c32), as a Clarity value. */ +export function recipientCV(to: string): CV { + if (to.includes(".")) { + const c = parseContractId(to); + if (!c) throw new Error("bad recipient"); + return { type: "contract", value: `${c.address}.${c.name}` }; + } + if (!parseAddress(to)) throw new Error("bad recipient"); + return { type: "address", value: to.trim().toUpperCase() }; +} + +export function assetInfoOf(assetId: string): AssetInfo | null { + const a = parseAssetId(assetId); + return a ? { contract: a.contract, assetName: a.assetName } : null; +} + +/** One post-condition from its SIP-005 bytes (what stacks.js `postConditionToHex` sends). */ +export function deserializePostCondition(bytes: Uint8Array): PostCondition { + const r = new Reader(bytes); + const pc = readPostCondition(r); + if (!r.done) throw new Error("trailing bytes"); + return pc; +} diff --git a/packages/chains-stacks/src/util.ts b/packages/chains-stacks/src/util.ts new file mode 100644 index 00000000..e0fef341 --- /dev/null +++ b/packages/chains-stacks/src/util.ts @@ -0,0 +1,133 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { ripemd160 } from "@noble/hashes/legacy.js"; + +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.trim().replace(/^0x/i, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export const isHex = (s: unknown): s is string => typeof s === "string" && /^(0x)?([0-9a-f]{2})*$/i.test(s.trim()) && s.trim().replace(/^0x/i, "").length > 0; + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s); + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +export const hash160 = (b: Uint8Array): Uint8Array => ripemd160(sha256(b)); + +/** Big-endian unsigned integer of `size` bytes. */ +export function uintBE(value: bigint, size: number): Uint8Array { + if (value < 0n || value >= 1n << BigInt(size * 8)) throw new Error("integer out of range"); + const out = new Uint8Array(size); + let v = value; + for (let i = size - 1; i >= 0; i--) { + out[i] = Number(v & 0xffn); + v >>= 8n; + } + return out; +} + +export function readUintBE(bytes: Uint8Array): bigint { + let v = 0n; + for (const b of bytes) v = (v << 8n) | BigInt(b); + return v; +} + +/** Sequential reader; every read past the end throws (the caller turns that into "can't read this"). */ +export class Reader { + at = 0; + constructor(readonly bytes: Uint8Array) {} + get done(): boolean { + return this.at >= this.bytes.length; + } + read(n: number): Uint8Array { + if (n < 0 || this.at + n > this.bytes.length) throw new Error("unexpected end of data"); + const out = this.bytes.subarray(this.at, this.at + n); + this.at += n; + return out; + } + u8(): number { + return this.read(1)[0]!; + } + u16(): number { + return Number(readUintBE(this.read(2))); + } + u32(): number { + return Number(readUintBE(this.read(4))); + } + u64(): bigint { + return readUintBE(this.read(8)); + } +} + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +export function short(address: string): string { + return address.length > 14 ? `${address.slice(0, 5)}…${address.slice(-4)}` : address; +} + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +export function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} + +/** Printable text, or null when the bytes aren't readable UTF-8. */ +export function textOf(bytes: Uint8Array): string | null { + if (!bytes.length) return ""; + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + return /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s) ? null : s; + } catch { + return null; + } +} + +/** A BigInt from what SIP-030 allows for integers (number, decimal string, bigint). */ +export function toBig(v: unknown): bigint | null { + if (typeof v === "bigint") return v; + if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v); + if (typeof v === "string" && /^\d+$/.test(v.trim())) return BigInt(v.trim()); + return null; +} + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/packages/chains-stacks/test/helpers.ts b/packages/chains-stacks/test/helpers.ts new file mode 100644 index 00000000..73a9e2ce --- /dev/null +++ b/packages/chains-stacks/test/helpers.ts @@ -0,0 +1,67 @@ +import type { Account, ChainContext, SignablePayload, Signature } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { STACKS_MAINNET, STACKS_TESTNET } from "../src/index.js"; +import { fromHex, hex } from "../src/util.js"; +import { FIX } from "./signatures.js"; + +/** Vault stand-in: answers with a precomputed signature that verifies for the payload (verification only). */ +export function fixtureSigner(publicKeyHex: string, sigs: Record) { + const pub = fromHex(publicKeyHex); + return { + sign(p: SignablePayload): Signature { + for (const v of Object.values(sigs)) { + const [rs, rec] = v.split(":"); + const bytes = fromHex(rs!); + if (secp256k1.verify(bytes, p.bytes, pub, { prehash: false })) return { scheme: "ecdsa-secp256k1", bytes, recovery: Number(rec), publicKey: publicKeyHex }; + } + throw new Error(`no fixture signature for digest ${hex(p.bytes)}`); + }, + }; +} + +export const signer = fixtureSigner(FIX.publicKey, FIX.sigs); + +export function makeAccount(publicKey = FIX.publicKey, address = FIX.mainnet): Account { + return { id: "stacks:0", family: "stacks", index: 0, curve: "secp256k1", derivationPath: "m/44'/5757'/0'/0/0", publicKey, address }; +} + +type Body = unknown | ((url: string, init?: RequestInit) => unknown); +export interface Reply { + status: number; + body: unknown; +} +export const reply = (status: number, body: unknown): Reply => ({ status, body }); +const isReply = (v: unknown): v is Reply => !!v && typeof v === "object" && "status" in v && "body" in v && Object.keys(v).length === 2; + +/** URL-routed mock fetch (first match wins). Handlers may return a Reply for a non-200 status. */ +export function mockFetch(routes: [RegExp, Body][]) { + const calls: { url: string; method: string; body?: unknown }[] = []; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + calls.push({ url, method: init?.method ?? "GET", body: init?.body }); + for (const [re, body] of routes) { + if (!re.test(url)) continue; + let v = typeof body === "function" ? (body as (u: string, i?: RequestInit) => unknown)(url, init) : body; + if (!isReply(v)) v = reply(200, v); + const r = v as Reply; + return new Response(typeof r.body === "string" ? r.body : JSON.stringify(r.body), { status: r.status }); + } + return new Response(JSON.stringify({ error: `no mock for ${url}` }), { status: 404 }); + }) as typeof fetch; + return { fetch: f, calls }; +} + +export function ctxFor(fetchImpl: typeof fetch, network = STACKS_TESTNET): ChainContext { + return { network, account: makeAccount(), fetch: fetchImpl }; +} +export { STACKS_MAINNET, STACKS_TESTNET }; + +/** Hiro replies trimmed from api.testnet.hiro.so (2026-10). */ +export const ACCOUNT = (balance: bigint, nonce = 7, locked = 0n) => ({ + balance: `0x${balance.toString(16).padStart(32, "0")}`, + locked: `0x${locked.toString(16).padStart(32, "0")}`, + unlock_height: 0, + nonce, +}); +export const NONCES = (next = 7) => ({ last_mempool_tx_nonce: null, last_executed_tx_nonce: next - 1, possible_next_nonce: next, detected_missing_nonces: [], detected_mempool_nonces: [] }); +export const FEES = { estimated_cost_scalar: 6, estimations: [{ fee_rate: 94, fee: 500 }, { fee_rate: 94, fee: 600 }, { fee_rate: 94, fee: 700 }], cost_scalar_change_by_byte: 0.0047 }; diff --git a/packages/chains-stacks/test/signatures.ts b/packages/chains-stacks/test/signatures.ts new file mode 100644 index 00000000..0f56cf8f --- /dev/null +++ b/packages/chains-stacks/test/signatures.ts @@ -0,0 +1,28 @@ +/** + * Signature fixtures, computed once OFFLINE (outside the repo) with the vault's own derivation and signEcdsa + * (packages/vault/src/derive.ts + sign.ts: RFC 6979, low-S) from the public BIP-39 test vector ("abandon" x11 + + * "about", empty passphrase) at m/44'/5757'/0'/0/0, this module's derivationPath(0). Each is r ‖ s (hex) ":" recovery + * id over the digest named in the key (see stacks.test.ts). The same key in @stacks/wallet-sdk account 0 gives the + * addresses below (packages/vault/test/families87.test.ts cross-checks it). + * + * AGENTS.md rule 1: no key or signing code outside packages/vault, tests included, so only the public key, addresses, + * digests' signatures and transactions live here. + */ +export const FIX = { + publicKey: "03d5d038bce81b3965314dba54f636f093c7dbdd6617cded013a53474fbccb100c", + mainnet: "SPC5KHM41H6WHAST7MWWDD807YSPRQKJ69FSH54J", + testnet: "STC5KHM41H6WHAST7MWWDD807YSPRQKJ68T330BQ", + /** Account 1 (m/44'/5757'/0'/0/1): somebody else in these tests. */ + otherPublicKey: "03121507b88c654be90c0973965b73f5b25597c393eb95b0a206d4217ee725582a", + otherTestnet: "ST3XHES5990FYDV5BHBZCJRFYFD2Z4X3FMEXRWMFR", + otherMainnet: "SP3XHES5990FYDV5BHBZCJRFYFD2Z4X3FMD2N3MGH", + sigs: { + "023eefe248a241dc": "6e6b626f22bd3431538ff513e8e11f300ab09ae80c589b3bb90a2612ecd95b39465fc9ace88e8cadc6d33013409ee145c9cb877317cedd371fd78908e40acb17:1", + "18bc87706cfb6b18": "be895ec7db80feca54973e3d53e576d95092a3f74865151171d6af31c2c26af70ffe7cc7c04ec4e26f0e408bc6123b9121bfcf3aa239754805ff547ab1a91e79:1", + "1b92e41fd31680e9": "9b00251402906769d067fe62345d7aa558a5373ffd7b85476ed166e329e1cbd36d51163cd092eb8043a7341c4c34621b343c2f6c1094b70f2ff16e7be5cf0413:0", + "42ae84c80bad64c6": "ce7d75016b3b5849e18686f33e96e1a37e31542ba7a5f650342f57506ac17d3d519f052dbb34d90d593efe8b9fb2f43f909738f9b852418e13da632bddc39099:0", + "6a5ac20b47bfa132": "034c974129a8c23984f70424b779f08144286fe474dc47914c53119e66fff3850b676597638fba39058a382a30e352ee1a02947f463e4732edbf746a870ef4a6:0", + "7d9f2007d107aab5": "0f237bae10f1697ce956c4c546fccb13882f8c85aa5a75482e41a297c754dab20511b27533aa97d5e08af1e07489bb8653ef21c94cd88925dabeb95a22cfabd0:0", + "e5663eecbcd636b9": "0e0a4ad500c541cbf992305796bd9e0cdf166c6ae80f480858350e530b2281ae7640f79ce411bbe67c5ef74d57b6d8b7b7020d32ec3ee208e2513b680f47be3c:1", + } as Record, +}; diff --git a/packages/chains-stacks/test/stacks.test.ts b/packages/chains-stacks/test/stacks.test.ts new file mode 100644 index 00000000..e4dd5120 --- /dev/null +++ b/packages/chains-stacks/test/stacks.test.ts @@ -0,0 +1,384 @@ +import { ClipError, type DappRequest, WALLET_ORIGIN } from "@clip-wallet/core"; +import { encodeMessage, hashMessage, verifyMessageSignatureRsv } from "@stacks/encryption"; +import { + AuthType, + Cl, + Pc, + PostConditionMode, + deserializeTransaction, + encodeStructuredDataBytes, + getAddressFromPublicKey, + makeUnsignedContractCall, + makeUnsignedContractDeploy, + makeUnsignedSTXTokenTransfer, + postConditionToHex, + sigHashPreSign, +} from "@stacks/transactions"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { describe, expect, it } from "vitest"; +import { + STACKS_METHODS, + STACKS_NETS, + createStacksModule, + cvFrom, + deserializeTx, + initialSighash, + messageHash, + netOf, + originPresignDigest, + parseAddress, + postConditionFrom, + serializeCV, + serializeTx, + structuredHash, + tokenAsset, + txidOf, + withOriginSignature, +} from "../src/index.js"; +import { fromHex, hex } from "../src/util.js"; +import { ACCOUNT, FEES, NONCES, STACKS_MAINNET, STACKS_TESTNET, ctxFor, mockFetch, reply, signer } from "./helpers.js"; +import { FIX } from "./signatures.js"; + +const API = STACKS_NETS.testnet.api.replace(/\./g, "\\."); +const ME = FIX.testnet; +const OTHER = FIX.otherTestnet; +const mod = createStacksModule(); + +const req = (method: string, params: unknown, extra: Partial = {}): DappRequest => ({ + id: `r-${Math.random().toString(36).slice(2)}`, + origin: "https://app.example", + via: "injected", + family: "stacks", + networkId: STACKS_TESTNET.id, + method, + params, + ...extra, +}); + +function routes(over: [RegExp, unknown][] = []) { + return mockFetch([ + ...over, + [new RegExp(`${API}/extended/v1/address/${ME}/nonces`), NONCES(7)], + [new RegExp(`${API}/v2/accounts/${ME}`), ACCOUNT(5_000_000n, 7)], + [new RegExp(`${API}/v2/fees/transaction`), FEES], + [new RegExp(`${API}/metadata/v1/ft/`), reply(404, { error: "not found" })], + ] as [RegExp, unknown][]); +} + +/** Runs decode → prepare → (fixture) sign → finalize. */ +async function run(r: DappRequest, ctx = ctxFor(routes().fetch)) { + const d = await mod.decode(r, ctx); + const payloads = await mod.prepare(r, ctx, "approval-1"); + const sigs = payloads.map((p) => signer.sign(p)); + const result = await mod.finalize(r, sigs, ctx); + return { d, payloads, result }; +} + +describe("addresses and networks", () => { + it("derives the SP/ST addresses of the abandon vector, as stacks.js does", () => { + const pub = fromHex(FIX.publicKey); + expect(mod.addressFromPublicKey(pub, STACKS_MAINNET)).toBe(FIX.mainnet); + expect(mod.addressFromPublicKey(pub, STACKS_TESTNET)).toBe(FIX.testnet); + expect(getAddressFromPublicKey(FIX.publicKey, "mainnet")).toBe(FIX.mainnet); + expect(getAddressFromPublicKey(FIX.publicKey, "testnet")).toBe(FIX.testnet); + // an uncompressed key gives the same (compressed-key P2PKH) address + expect(mod.addressFromPublicKey(secp256k1.Point.fromHex(FIX.publicKey).toBytes(false), STACKS_MAINNET)).toBe(FIX.mainnet); + expect(mod.derivationPath(3)).toBe("m/44'/5757'/0'/0/3"); + expect(mod.curve).toBe("secp256k1"); + }); + + it("checks c32check checksums and spells network per version", () => { + expect(mod.isAddress(FIX.mainnet)).toBe(true); + expect(mod.isAddress(`${FIX.testnet}.my-contract`)).toBe(true); + expect(mod.isAddress(FIX.mainnet.slice(0, -1) + (FIX.mainnet.endsWith("J") ? "K" : "J"))).toBe(false); + expect(mod.isAddress("0x1234")).toBe(false); + expect(parseAddress(FIX.mainnet)?.version).toBe(22); + expect(parseAddress(FIX.testnet)?.version).toBe(26); + // leading zero bytes in the hash shorten the address (the testnet boot address) + expect(parseAddress("ST000000000000000000002AMW42H")?.hash160).toEqual(new Uint8Array(20)); + expect(mod.isAddress("SP000000000000000000002Q6VF78")).toBe(true); + expect(mod.isAddress("ST000000000000000000002AMW42J")).toBe(false); + expect(mod.networksForAddress(FIX.mainnet, [STACKS_TESTNET, STACKS_MAINNET]).map((n) => n.id)).toEqual(["stacks:1"]); + expect(mod.networksForAddress(FIX.testnet, [STACKS_TESTNET, STACKS_MAINNET]).map((n) => n.id)).toEqual(["stacks:2147483648"]); + }); + + it("uses the CAIP-2 ids of the ChainAgnostic stacks namespace", async () => { + expect(STACKS_MAINNET.id).toBe("stacks:1"); + expect(STACKS_TESTNET.id).toBe("stacks:2147483648"); + expect(netOf("testnet")).toBe("testnet"); + expect(netOf(1)).toBe("mainnet"); + expect(await mod.receiveAddress(ctxFor(routes().fetch))).toBe(FIX.testnet); + expect(await mod.receiveAddress(ctxFor(routes().fetch, STACKS_MAINNET))).toBe(FIX.mainnet); + }); +}); + +describe("serialization matches @stacks/transactions", () => { + it("STX transfer with memo, its sighash and presign digest", async () => { + const theirs = await makeUnsignedSTXTokenTransfer({ recipient: OTHER, amount: 12345n, memo: "hello", fee: 300n, nonce: 9n, publicKey: FIX.publicKey, network: "testnet" }); + const ours = deserializeTx(theirs.serializeBytes()); + expect(hex(serializeTx(ours))).toBe(theirs.serialize()); + expect(hex(initialSighash(ours))).toBe(theirs.signBegin()); + expect(hex(originPresignDigest(ours))).toBe(sigHashPreSign(theirs.signBegin(), AuthType.Standard, 300n, 9n)); + expect(hex(txidOf(ours))).toBe(theirs.txid()); + }); + + it("contract call with Clarity args and every post-condition kind", async () => { + const pcs = [ + Pc.principal(ME).willSendEq(1000n).ustx(), + Pc.principal(ME).willSendLte(5n).ft("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx", "usdcx-token"), + Pc.principal(`${OTHER}.pool`).willSendGte(1n).ft("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx", "usdcx-token"), + Pc.principal(ME).willNotSendAsset().nft(`${OTHER}.nfts::nft`, Cl.uint(7)), + ]; + const theirs = await makeUnsignedContractCall({ + contractAddress: OTHER, + contractName: "pool", + functionName: "swap-x-for-y", + functionArgs: [ + Cl.uint(1000), + Cl.int(-5), + Cl.principal(ME), + Cl.contractPrincipal(OTHER, "token-x"), + Cl.some(Cl.bufferFromUtf8("memo")), + Cl.none(), + Cl.list([Cl.bool(true), Cl.bool(false)]), + Cl.tuple({ b: Cl.stringUtf8("héllo"), a: Cl.stringAscii("hi"), c: Cl.ok(Cl.uint(1)), d: Cl.error(Cl.int(2)) }), + ], + postConditions: pcs, + postConditionMode: PostConditionMode.Deny, + fee: 2000n, + nonce: 3n, + publicKey: FIX.publicKey, + network: "testnet", + }); + const ours = deserializeTx(theirs.serializeBytes()); + expect(hex(serializeTx(ours))).toBe(theirs.serialize()); + expect(hex(originPresignDigest(ours))).toBe(sigHashPreSign(theirs.signBegin(), AuthType.Standard, 2000n, 3n)); + // post-conditions from stacks.js hex and from SIP-030 JSON give the same bytes + for (const pc of pcs) expect(postConditionFrom(postConditionToHex(pc))).toEqual(postConditionFrom(pc)); + // Clarity values: hex and SIP-030 JSON agree with Cl.serialize + const v = Cl.tuple({ amount: Cl.uint(5), to: Cl.principal(OTHER), m: Cl.some(Cl.bufferFromHex("beef")) }); + expect(hex(serializeCV(cvFrom(Cl.serialize(v))))).toBe(Cl.serialize(v)); + expect(hex(serializeCV(cvFrom(JSON.parse(JSON.stringify(v, (_k, x) => (typeof x === "bigint" ? x.toString() : x))))))).toBe(Cl.serialize(v)); + }); + + it("sponsored contract deploy: origin signs with the standard flag over a cleared sponsor", async () => { + const theirs = await makeUnsignedContractDeploy({ contractName: "hello", codeBody: "(define-read-only (hi) u1)", fee: 0n, nonce: 1n, publicKey: FIX.publicKey, network: "testnet", sponsored: true }); + const ours = deserializeTx(theirs.serializeBytes()); + expect(hex(serializeTx(ours))).toBe(theirs.serialize()); + expect(hex(initialSighash(ours))).toBe(theirs.signBegin()); + expect(hex(originPresignDigest(ours))).toBe(sigHashPreSign(theirs.signBegin(), AuthType.Standard, 0n, 1n)); + }); + + it("an origin signature made over our digest verifies in stacks.js", async () => { + const theirs = await makeUnsignedSTXTokenTransfer({ recipient: OTHER, amount: 1n, fee: 180n, nonce: 0n, publicKey: FIX.publicKey, network: "testnet" }); + const ours = deserializeTx(theirs.serializeBytes()); + const sig = signer.sign({ accountId: "stacks:0", scheme: "ecdsa-secp256k1", bytes: originPresignDigest(ours), approvalId: "x" }); + const vrs = new Uint8Array([sig.recovery!, ...sig.bytes]); + const signed = deserializeTransaction(hex(serializeTx(withOriginSignature(ours, vrs)))); + expect(() => signed.verifyOrigin()).not.toThrow(); + }); +}); + +describe("message signing", () => { + it("plain messages hash like @stacks/encryption and verify with verifyMessageSignatureRsv", async () => { + const text = "Clip Wallet dapp matrix: sign-in check (testnet)"; + expect(hex(messageHash(text))).toBe(hex(hashMessage(text))); + expect(hex(encodeMessage(text)).startsWith("17537461636b73")).toBe(true); + const r = req(STACKS_METHODS.signMessage, { message: text }); + const { d, result } = await run(r); + expect(d.title).toBe("Sign a message for app.example"); + expect(d.lines).toEqual([{ label: "Message", value: text }]); + const out = result as { signature: string; publicKey: string }; + expect(out.publicKey).toBe(FIX.publicKey); + expect(verifyMessageSignatureRsv({ signature: out.signature, message: text, publicKey: FIX.publicKey })).toBe(true); + }); + + it("SIP-018 structured data hashes like stacks.js and verifies", async () => { + const domain = Cl.tuple({ name: Cl.stringAscii("Clip"), version: Cl.stringAscii("1.0.0"), "chain-id": Cl.uint(2147483648) }); + const message = Cl.tuple({ action: Cl.stringAscii("login"), nonce: Cl.uint(42) }); + expect(hex(structuredHash(cvFrom(Cl.serialize(domain)), cvFrom(Cl.serialize(message))))).toBe(hex(sha256(encodeStructuredDataBytes({ domain, message })))); + const r = req(STACKS_METHODS.signStructuredMessage, { domain: Cl.serialize(domain), message: Cl.serialize(message) }); + const { d, result } = await run(r); + expect(d.title).toBe("Sign data for app.example"); + expect(d.warnings).toEqual([]); + const out = result as { signature: string }; + expect(verifyMessageSignatureRsv({ signature: out.signature, message: sha256(encodeStructuredDataBytes({ domain, message })), publicKey: FIX.publicKey })).toBe(true); + }); + + it("warns when the SIP-018 domain names another chain", async () => { + const domain = Cl.tuple({ name: Cl.stringAscii("Clip"), version: Cl.stringAscii("1"), "chain-id": Cl.uint(1) }); + const d = await mod.decode(req(STACKS_METHODS.signStructuredMessage, { domain: Cl.serialize(domain), message: Cl.serialize(Cl.uint(1)) }), ctxFor(routes().fetch)); + expect(d.warnings[0]).toMatchObject({ level: "danger", code: "network-matters" }); + }); +}); + +describe("dapp transactions", () => { + it("stx_transferStx: decodes, signs the presign digest and broadcasts", async () => { + const posted: Uint8Array[] = []; + const m = routes([ + [ + new RegExp(`${API}/v2/transactions$`), + (_u: string, init?: RequestInit) => { + const raw = init!.body as Uint8Array; + posted.push(raw); + return JSON.stringify(hex(txidOf(raw))); + }, + ], + ]); + const r = req(STACKS_METHODS.transferStx, { recipient: OTHER, amount: "1", memo: "hi", network: "testnet" }); + const { d, payloads, result } = await run(r, ctxFor(m.fetch)); + expect(d.title).toBe(`Send 0.000001 STX to ${OTHER.slice(0, 5)}…${OTHER.slice(-4)}`); + expect(d.titleMsg?.id).toBe("bg.req.sendTo"); + expect(d.lines).toContainEqual({ label: "Memo", value: "hi" }); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "stx" }), delta: "-1" }]); + expect(d.fee).toMatchObject({ amount: "600" }); + expect(d.blind).toBe(false); + expect(payloads).toHaveLength(1); + expect(posted).toHaveLength(1); + const tx = deserializeTransaction(posted[0]!); + expect(() => tx.verifyOrigin()).not.toThrow(); + expect(tx.auth.spendingCondition.nonce).toBe(7n); + expect(tx.auth.spendingCondition.fee).toBe(600n); + expect(result).toEqual({ txid: tx.txid(), transaction: tx.serialize() }); + }); + + it("refuses a bad signature before broadcasting", async () => { + const m = routes([[new RegExp(`${API}/v2/transactions$`), "\"00\""]]); + const ctx = ctxFor(m.fetch); + const r = req(STACKS_METHODS.transferStx, { recipient: OTHER, amount: "1" }); + await mod.decode(r, ctx); + const [p] = await mod.prepare(r, ctx, "a"); + const good = signer.sign(p!); + const flipped = good.bytes.slice(); + flipped[5] = flipped[5]! ^ 1; + await expect(mod.finalize(r, [{ ...good, bytes: flipped }], ctx)).rejects.toMatchObject({ code: "stacks/bad-signature" }); + expect(m.calls.some((c) => c.method === "POST" && /\/v2\/transactions$/.test(c.url))).toBe(false); + }); + + it("refuses a request for another network or another account", async () => { + const ctx = ctxFor(routes().fetch); + await expect(mod.decode(req(STACKS_METHODS.transferStx, { recipient: OTHER, amount: 1, network: "mainnet" }), ctx)).rejects.toMatchObject({ code: "stacks/network-mismatch" }); + await expect(mod.decode(req(STACKS_METHODS.transferStx, { recipient: OTHER, amount: 1 }, { networkId: "stacks:1" }), ctx)).rejects.toMatchObject({ code: "stacks/network-mismatch" }); + await expect(mod.decode(req(STACKS_METHODS.signMessage, { message: "x", address: OTHER }), ctx)).rejects.toMatchObject({ code: "stacks/wrong-account" }); + // a mainnet transaction handed to the testnet connection + const main = await makeUnsignedSTXTokenTransfer({ recipient: FIX.otherMainnet, amount: 1n, fee: 180n, nonce: 0n, publicKey: FIX.publicKey, network: "mainnet" }); + await expect(mod.decode(req(STACKS_METHODS.signTransaction, { transaction: main.serialize() }), ctx)).rejects.toMatchObject({ code: "stacks/network-mismatch" }); + // somebody else's transaction + const theirs = await makeUnsignedSTXTokenTransfer({ recipient: ME, amount: 1n, fee: 180n, nonce: 0n, publicKey: FIX.otherPublicKey, network: "testnet" }); + await expect(mod.decode(req(STACKS_METHODS.signTransaction, { transaction: theirs.serialize() }), ctx)).rejects.toMatchObject({ code: "stacks/wrong-account" }); + }); + + it("an unreadable transaction is blind and can't be prepared", async () => { + const ctx = ctxFor(routes().fetch); + const r = req(STACKS_METHODS.signTransaction, { transaction: "80800000000400deadbeef" }); + const d = await mod.decode(r, ctx); + expect(d.blind).toBe(true); + expect(d.title).toBe("Approve an unreadable request from app.example"); + await expect(mod.prepare(r, ctx, "a")).rejects.toBeInstanceOf(ClipError); + }); + + it("stx_callContract in allow mode is a danger; post-conditions become lines and balance changes", async () => { + const pcs = [Pc.principal(ME).willSendEq(2_500_000n).ft("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx", "usdcx-token")].map(postConditionToHex); + const r = req(STACKS_METHODS.callContract, { + contract: `${OTHER}.amm`, + functionName: "swap", + functionArgs: [Cl.serialize(Cl.uint(2500000)), { type: "uint", value: "1" }], + postConditions: pcs, + postConditionMode: "allow", + }); + const d = await mod.decode(r, ctxFor(routes().fetch)); + expect(d.title).toBe("Use swap on contract amm"); + expect(d.warnings[0]).toMatchObject({ level: "danger", code: "unlimited-approval" }); + expect(d.warnings[0]!.msg?.id).toBe("bg.stacks.allowMode"); + expect(d.lines).toContainEqual({ label: "Argument 1", labelMsg: expect.objectContaining({ id: "bg.label.argumentN" }), value: "u2500000" }); + expect(d.lines).toContainEqual(expect.objectContaining({ label: "Post-condition", value: "You send exactly 2.5 USDCx" })); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "usdcx", bridged: true }), delta: "-2500000" }]); + }); + + it("deny mode without our post-conditions says nothing can leave", async () => { + const d = await mod.decode(req(STACKS_METHODS.callContract, { contract: `${OTHER}.counter`, functionName: "increment" }), ctxFor(routes().fetch)); + expect(d.lines).toContainEqual(expect.objectContaining({ value: "None of your assets can leave your account in this transaction, apart from the network fee." })); + expect(d.warnings.map((w) => w.code)).toEqual(["unknown-call"]); + expect(d.balanceChanges).toEqual([]); + }); + + it("stx_transferSip10Ft builds SIP-010 transfer with an exact post-condition", async () => { + const r = req(STACKS_METHODS.transferSip10Ft, { asset: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx::usdcx-token", recipient: OTHER, amount: "1500000" }); + const d = await mod.decode(r, ctxFor(routes().fetch)); + expect(d.title).toBe(`Send 1.5 USDCx to ${OTHER.slice(0, 5)}…${OTHER.slice(-4)}`); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "usdcx" }), delta: "-1500000" }]); + expect(d.warnings).toEqual([]); + }); + + it("stx_signTransaction (no broadcast) returns the signed transaction to the app", async () => { + const t = await makeUnsignedContractCall({ + contractAddress: OTHER, + contractName: "nft-market", + functionName: "list", + functionArgs: [Cl.uint(1)], + postConditionMode: PostConditionMode.Deny, + fee: 1000n, + nonce: 4n, + publicKey: FIX.publicKey, + network: "testnet", + }); + const m = routes(); + const { d, result } = await run(req(STACKS_METHODS.signTransaction, { transaction: t.serialize() }), ctxFor(m.fetch)); + expect(d.lines).toContainEqual({ label: "Sent by", value: "app.example (it gets the signed transaction)" }); + expect(m.calls.some((c) => c.method === "POST")).toBe(false); + const signed = deserializeTransaction((result as { transaction: string }).transaction); + expect(() => signed.verifyOrigin()).not.toThrow(); + }); + + it("maps the node's rejection to plain words", async () => { + const m = routes([[new RegExp(`${API}/v2/transactions$`), reply(400, { error: "transaction rejected", reason: "NotEnoughFunds", txid: "ab" })]]); + const r = req(STACKS_METHODS.transferStx, { recipient: OTHER, amount: "1" }); + await expect(run(r, ctxFor(m.fetch))).rejects.toMatchObject({ code: "stacks/insufficient-funds", userMessage: "You don't have enough STX to pay for this and its fee." }); + }); +}); + +describe("wallet sends and balances", () => { + it("buildTransfer: a native send through decode → prepare → finalize", async () => { + const posted: Uint8Array[] = []; + const m = routes([[new RegExp(`${API}/v2/transactions$`), (_u: string, init?: RequestInit) => (posted.push(init!.body as Uint8Array), JSON.stringify(hex(txidOf(init!.body as Uint8Array))))]]); + const ctx = ctxFor(m.fetch); + const r = await mod.buildTransfer({ asset: STACKS_TESTNET.nativeAsset, to: OTHER, amount: "250000" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, method: STACKS_METHODS.signTransaction, family: "stacks" }); + const { d, result } = await run(r, ctx); + expect(d.title).toBe(`Send 0.25 STX to ${OTHER.slice(0, 5)}…${OTHER.slice(-4)}`); + expect(posted).toHaveLength(1); + expect((result as { txid: string }).txid).toMatch(/^[0-9a-f]{64}$/); + }); + + it("buildTransfer refuses own address, other-network addresses and too-large amounts", async () => { + const ctx = ctxFor(routes().fetch); + await expect(mod.buildTransfer({ asset: STACKS_TESTNET.nativeAsset, to: ME, amount: "1" }, ctx)).rejects.toMatchObject({ code: "stacks/self-transfer" }); + await expect(mod.buildTransfer({ asset: STACKS_TESTNET.nativeAsset, to: FIX.otherMainnet, amount: "1" }, ctx)).rejects.toMatchObject({ code: "stacks/network-mismatch" }); + await expect(mod.buildTransfer({ asset: STACKS_TESTNET.nativeAsset, to: OTHER, amount: "5000000" }, ctx)).rejects.toMatchObject({ code: "stacks/insufficient-funds" }); + }); + + it("getBalances: STX, curated tokens and spam look-alikes", async () => { + const m = mockFetch([ + [ + new RegExp(`${API}/extended/v1/address/${ME}/balances`), + { + stx: { balance: "1500000", locked: "0" }, + fungible_tokens: { + "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx::usdcx-token": { balance: "2000000" }, + [`${OTHER}.fake-usdc::usdc`]: { balance: "999" }, + [`${OTHER}.zero::z`]: { balance: "0" }, + }, + }, + ], + [new RegExp(`${API}/metadata/v1/ft/${OTHER}.fake-usdc`), { name: "USD Coin", symbol: "USDC", decimals: 6 }], + ]); + const b = await mod.getBalances(ctxFor(m.fetch)); + expect(b.map((x) => [x.asset.key, x.amount, !!x.asset.spam])).toEqual([ + ["stx", "1500000", false], + ["usdcx", "2000000", false], + [`sip10:${OTHER}.fake-usdc::usdc`, "999", true], + ]); + expect(tokenAsset(STACKS_TESTNET.id, `${OTHER}.t::t`, { symbol: "COOL", decimals: 2 }).spam).toBeUndefined(); + }); +}); diff --git a/packages/chains-stacks/tsconfig.json b/packages/chains-stacks/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-stacks/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-substrate/README.md b/packages/chains-substrate/README.md index ba049d07..96cea556 100644 --- a/packages/chains-substrate/README.md +++ b/packages/chains-substrate/README.md @@ -1,6 +1,7 @@ # @clip-wallet/chains-substrate -Polkadot SDK (Substrate) `ChainModule` for Clip Wallet: Polkadot, Kusama, Westend and Paseo, plus their Asset Hubs. +Polkadot SDK (Substrate) `ChainModule` for Clip Wallet: Polkadot, Kusama, Westend and Paseo, plus their Asset Hubs, +and the Chainflip State Chain (mainnet and the Perseverance testnet). It decodes and builds extrinsics through runtime metadata and never touches keys. `prepare()` returns the signing payload for the vault to sign with sr25519, and `finalize()` checks the signature (`@scure/sr25519` `verify` only) and returns a MultiSignature or the signed extrinsic. @@ -69,6 +70,8 @@ hash. Genesis hashes, SS58 formats, decimals and symbols were read on 2026-10-03 | Kusama Asset Hub | `polkadot:48239ef607d7928874027a43a6768920` | 2 | KSM | no | | Westend Asset Hub | `polkadot:67f9723393ef76214df0118c34bbbd3d` | 42 | WND | yes | | Paseo Asset Hub | `polkadot:d6eec26135305a8ad257a20d00335728` | 42 | PAS | yes | +| Chainflip | `polkadot:8b8c140b0af9db70686583e3f6bf2a59` | 2112 | FLIP (18) | no | +| Chainflip Perseverance | `polkadot:7a5d4db858ada1d20ed6ded4933c3331` | 2112 | FLIP (18) | yes | - Paseo's genesis is `0x374057be…` on two independent providers (Dwellir, Stakeworld), and its SS58 format is now 42. @@ -78,6 +81,10 @@ hash. Genesis hashes, SS58 formats, decimals and symbols were read on 2026-10-03 the same native token. Polkadot Asset Hub assets 1337 (USDC, Circle-issued) → `usdc` and 1984 (USDT) → `usdt`. Other assets → `asset:`, with `address` = the asset id. Paseo Asset Hub's test USDC (1337) / USDT (1984) share `usdc` / `usdt` (same ids as Polkadot, owner `5Evfk4MM…`, sufficient, PAS pools; read 2026-10-03). +- Chainflip (read 2026-10-06): RPC `https://mainnet-rpc.chainflip.io`, `https://rpc.chainflip.io`; Perseverance + `https://archive.perseverance.chainflip.io`, `https://perseverance.chainflip.xyz` (the RPC the Perseverance LP + portal itself uses). Explorers `scan.chainflip.io` / `scan.perseverance.chainflip.io` (`/extrinsics/`). + Asset keys `flip` and `flip-testnet`. See "Chainflip" below. - `fromChainId` also accepts a full `0x` genesis hash (what `SignerPayloadJSON` and injectedWeb3 accounts carry). ## Accounts @@ -164,6 +171,51 @@ The extrinsic is `compact(len) ‖ 0x84 ‖ MultiAddress::Id(0x00 ‖ pubkey) - `buildTransfer`: `Balances.transfer_keep_alive`, or `Assets.transfer_keep_alive` when `asset.address` is an asset id. An address formatted for another network (prefix not this network's and not 42) is refused. +## Chainflip + +The Chainflip State Chain is a Substrate solo chain that the existing sr25519 account signs for (address `cF…`, +SS58 2112). Its runtime (chainflip-node 20216 on mainnet, 20302 on Perseverance) has no Balances pallet, so the +module recognises it from metadata (`isChainflip`: `Flip` and `Funding` pallets, no `Balances`) and changes three +things: + +- **Balances.** `System.Account.data` is `Null`; FLIP is `Flip.Account { balance, bond }`. `getBalances` returns + `balance` (bond included: fees are burned from all of it, per cf-flip `on_charge_transaction.rs`). + `getFlipAccount(ctx)` splits it: `balance`, `bond`, `redeemable` (= balance − bond, what `Funding.redeem` can + take) and any `pendingRedemption` (`Funding.PendingRedemptions`: amount and Ethereum address, no longer in + `balance`). `spendableNative` returns `redeemable` there. +- **Sending.** There's no transfer call: FLIP arrives by funding the account from Ethereum (StateChainGateway, e.g. + through auctions.chainflip.io or lp.chainflip.io) and leaves by `Funding.redeem` to an Ethereum address. + `buildTransfer` refuses with `substrate/no-transfers` ("FLIP can't be sent from one Chainflip account to + another. To move it, redeem it to an Ethereum address, or fund the other account from Ethereum."). +- **Plain words** for the calls Chainflip's own apps send (shapes from metadata, meaning from + chainflip-io/chainflip-backend and docs.chainflip.io): + - `Funding.redeem` ("Redeem 5 FLIP to 0xABaB…ABaB on Ethereum"): amount (or all that isn't bonded), the + EIP-55 Ethereum address, who can execute it ("Anyone" or the executor), that it's finished on Ethereum for ETH + gas before it expires, a `network-matters` caution, and the FLIP leaving. + - `Funding.bind_redeem_address` / `bind_executor_address`: danger warnings. The docs call binding "a one-off + irreversible operation" (docs.chainflip.io/validators/mainnet/funding); a wrong address locks the FLIP. + - `Funding.rebalance`, `LiquidityProvider.*` (register / refund address / deposit address / withdraw / + transfer / schedule_swap), `Swapping.request_swap_deposit_address(_with_affiliates)`, + `LendingPools.*` (Boost add/stop, lend, take back, borrow, repay) and `AccountRoles.as_sub_account` (the inner + call). Cross-chain addresses are shown in each chain's format (EIP-55, SS58 0, base58, Tron base58check, + Bitcoin text) with a `network-matters` caution. Asset amounts use each token's decimals ("1.5 USDC + (Ethereum)"). + - `LiquidityPools` order calls stay `Pallet.call(args)` with a caution, plus a `high-fee` caution: the runtime + holds up to 1 FLIP for each order call and multiplies the fee for repeated order calls in one block + (`LpOrderCallIndexer`), which `TransactionPaymentApi` doesn't show. +- **Signing.** The signed extensions are AuthorizeCall, CheckNonZeroSender, CheckSpecVersion, CheckTxVersion, + CheckGenesis, CheckMortality, CheckNonce, CheckWeight, ChargeTransactionPayment, CheckMetadataHash and + WeightReclaim; AuthorizeCall and WeightReclaim are empty both ways and encode nothing. Signing bytes and the + signed extrinsic match `@polkadot/types` 17 byte for byte with the mainnet metadata (with and without + CheckMetadataHash), and a wallet-built `register_lp_account` signed by the vault was validated read-only by + mainnet and Perseverance (`TaggedTransactionQueue_validate_transaction` → `Payment`: signature and extensions + accepted, only the fee missing). +- **Fees.** `TransactionPaymentApi_query_info` answers on both networks (about 0.00002 FLIP for a redeem); + when it doesn't, no fee line is shown. +- **Dapps.** lp.chainflip.io (and lp.perseverance.chainflip.io) lists every `window.injectedWeb3` entry + (`Object.keys(window.injectedWeb3)`) and re-encodes addresses to SS58 2112, so 1Mask's existing substrate + provider is found without changes. Funding from Ethereum uses the EVM provider. + ## Helpers for wallet features (staking, swaps) Added for `@clip-wallet/features` (`staking/polkadot.ts`, `swap/assethub.ts`). Reads only; every transaction @@ -201,12 +253,20 @@ AssetConversion pools (community test tokens), Paseo Asset Hub 21 including PAS/ ## Tests -`pnpm test` (29 tests) runs against real Westend Asset Hub metadata V15 (`test/fixtures`, gzipped, specVersion -1025001), with RPC, storage and runtime-API answers encoded through the same metadata. sr25519 signatures are -fixtures computed once offline with a throwaway key (`test/signatures.ts`). +`pnpm test` (45 tests) runs against real Westend Asset Hub metadata V15 (`test/fixtures`, gzipped, specVersion +1025001) and real Chainflip mainnet metadata V15 (specVersion 20216), with RPC, storage and runtime-API answers +encoded through the same metadata. sr25519 signatures are fixtures computed once offline: Westend with a +throwaway key (`test/signatures.ts`), Chainflip with the vault's account 0 of the public "abandon … about" phrase +(`test/chainflip-fixtures.ts`). ## Gaps +- Chainflip: LP limit/range orders aren't described in plain words yet (ticks → prices); FLIP funding from + Ethereum is an EVM transaction (StateChainGateway `fundStateChainAccount`) and isn't built here. +- Fixed: polkadot-api 0.21's `getSs58AddressInfo` reads two-byte SS58 prefixes as a plain u16 (2112 → 20488); + `networksForAddress` / `isAddress` / `buildTransfer` use `ss58Info`, which reads the SS58 bit layout. A malformed + sr25519 signature (no Schnorrkel marker) made `finalize` throw a raw error; it's now `substrate/bad-signature`. + - No transaction v5 (general transactions), and no ethereum (`AccountId20`) or ecdsa/ed25519 accounts. - Direct staking (bond / nominate) decodes but isn't built; only nomination pools are. - Foreign assets (XCM `Location` ids) and XCM transfers show as `Pallet.call(args)`. diff --git a/packages/chains-substrate/src/chainflip.ts b/packages/chains-substrate/src/chainflip.ts new file mode 100644 index 00000000..53ddcae3 --- /dev/null +++ b/packages/chains-substrate/src/chainflip.ts @@ -0,0 +1,310 @@ +/** + * Chainflip State Chain: plain words for the calls its own apps send (LP portal lp.chainflip.io, the funding / + * auctions app, Boost and lending), FLIP balances from `Flip.Account`, and cross-chain address formatting. + * + * Call shapes are from Chainflip runtime metadata (chainflip-node 20216 mainnet, 20302 Perseverance, read + * 2026-10-06); semantics from chainflip-io/chainflip-backend (state-chain/pallets/cf-funding, cf-flip, + * cf-lp, cf-swapping, cf-lending-pools) and docs.chainflip.io: + * - `Funding.redeem { amount: Max | Exact(u128), address: [u8;20], executor: Option<[u8;20]> }` asks the validators + * to register a redemption on Ethereum's StateChainGateway; the person then executes it on Ethereum before it + * expires (docs.chainflip.io/validators/mainnet/funding). + * - `Funding.bind_redeem_address` / `bind_executor_address` are "a one-off irreversible operation" (same page): + * the account can then only ever redeem to that address / be executed by that address. + * - Fees are burned from `Flip.Account.balance`, bond included (cf-flip on_charge_transaction.rs `try_debit`), + * while redemptions can only take the liquid part (`balance - bond`, `FlipAccount::liquid`). + */ +import type { AssetRef, BalanceChange, Warning } from "@clip-wallet/core"; +import { knownMsg, recallMsg, say } from "@clip-wallet/core"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { base58, createBase58check } from "@scure/base"; +import { fromBufferToBase58 } from "@polkadot-api/substrate-bindings"; +import { readStorage } from "./chain.js"; +import type { Runtime } from "./metadata.js"; +import type { SubstrateRpc } from "./rpc.js"; +import { formatUnits, fromHex, hex, isHex, short, textOf } from "./util.js"; + +/** A runtime is Chainflip's when it keeps FLIP in `Flip` and moves it with `Funding` (no Balances pallet). */ +export function isChainflip(rt: Runtime): boolean { + return rt.pallets.has("Flip") && rt.pallets.has("Funding") && !rt.pallets.has("Balances"); +} + +/** + * Chainflip's `Asset` enum (cf-primitives) → token, chain and decimals. Decimals are each token's own on its chain + * (docs.chainflip.io/protocol/supported-chains-assets/chains-assets: cbBTC 8, BSC USDT 18; the rest are the + * tokens' standard decimals). Variants not listed are shown by name with base units. + */ +export const CHAINFLIP_ASSETS: Record = { + Eth: { symbol: "ETH", chain: "Ethereum", decimals: 18 }, + Flip: { symbol: "FLIP", chain: "Ethereum", decimals: 18 }, + Usdc: { symbol: "USDC", chain: "Ethereum", decimals: 6 }, + Usdt: { symbol: "USDT", chain: "Ethereum", decimals: 6 }, + Wbtc: { symbol: "WBTC", chain: "Ethereum", decimals: 8 }, + Cbbtc: { symbol: "cbBTC", chain: "Ethereum", decimals: 8 }, + Dot: { symbol: "DOT", chain: "Polkadot", decimals: 10 }, + Btc: { symbol: "BTC", chain: "Bitcoin", decimals: 8 }, + ArbEth: { symbol: "ETH", chain: "Arbitrum", decimals: 18 }, + ArbUsdc: { symbol: "USDC", chain: "Arbitrum", decimals: 6 }, + ArbUsdt: { symbol: "USDT", chain: "Arbitrum", decimals: 6 }, + Sol: { symbol: "SOL", chain: "Solana", decimals: 9 }, + SolUsdc: { symbol: "USDC", chain: "Solana", decimals: 6 }, + SolUsdt: { symbol: "USDT", chain: "Solana", decimals: 6 }, + HubDot: { symbol: "DOT", chain: "Polkadot Asset Hub", decimals: 10 }, + HubUsdc: { symbol: "USDC", chain: "Polkadot Asset Hub", decimals: 6 }, + HubUsdt: { symbol: "USDT", chain: "Polkadot Asset Hub", decimals: 6 }, + Trx: { symbol: "TRX", chain: "Tron", decimals: 6 }, + TrxUsdt: { symbol: "USDT", chain: "Tron", decimals: 6 }, + Bnb: { symbol: "BNB", chain: "BNB Smart Chain", decimals: 18 }, + BscUsdt: { symbol: "USDT", chain: "BNB Smart Chain", decimals: 18 }, +}; + +/** `EncodedAddress` variants (cf-chains) → chain name. */ +const ADDRESS_CHAINS: Record = { + Eth: "Ethereum", + Arb: "Arbitrum", + Bsc: "BNB Smart Chain", + Dot: "Polkadot", + Hub: "Polkadot Asset Hub", + Sol: "Solana", + Btc: "Bitcoin", + Tron: "Tron", +}; + +const tronCheck = createBase58check(sha256); + +export function bytesOf(v: unknown): Uint8Array | null { + if (v instanceof Uint8Array) return v; + if (typeof v === "string" && isHex(v)) return fromHex(v); + if (v && typeof v === "object" && typeof (v as { asBytes?: unknown }).asBytes === "function") return (v as { asBytes(): Uint8Array }).asBytes(); + return null; +} + +/** EIP-55 checksummed 0x address. */ +export function ethAddress(v: unknown): string { + const b = bytesOf(v); + if (!b || b.length !== 20) return b ? `0x${hex(b)}` : String(v); + const lower = hex(b); + const h = hex(keccak_256(new TextEncoder().encode(lower))); + let out = "0x"; + for (let i = 0; i < 40; i++) out += parseInt(h[i]!, 16) >= 8 ? lower[i]!.toUpperCase() : lower[i]!; + return out; +} + +/** A Chainflip `EncodedAddress` ({ type: "Eth", value: bytes }) → the address as people write it on that chain. */ +export function foreignAddress(v: unknown): { chain: string; address: string } { + const e = v as { type?: string; value?: unknown } | undefined; + const chain = (e?.type && ADDRESS_CHAINS[e.type]) ?? e?.type ?? "another chain"; + const b = bytesOf(e?.value); + if (!b) return { chain, address: String(e?.value ?? "") }; + switch (e?.type) { + case "Eth": + case "Arb": + case "Bsc": + return { chain, address: ethAddress(b) }; + case "Dot": + case "Hub": + return { chain, address: b.length === 32 ? fromBufferToBase58(0)(b) : `0x${hex(b)}` }; + case "Sol": + return { chain, address: base58.encode(b) }; + case "Tron": + return { chain, address: b.length === 20 ? tronCheck.encode(Uint8Array.of(0x41, ...b)) : `0x${hex(b)}` }; + case "Btc": + // cf-chains EncodedAddress::Btc carries the address string's bytes. + return { chain, address: textOf(b) ?? `0x${hex(b)}` }; + default: + return { chain, address: `0x${hex(b)}` }; + } +} + +const big = (v: unknown): bigint => (typeof v === "bigint" ? v : typeof v === "number" ? BigInt(v) : 0n); +const variant = (v: unknown): string => (v && typeof v === "object" && typeof (v as { type?: unknown }).type === "string" ? (v as { type: string }).type : String(v)); + +/** "USDC (Ethereum)" for an Asset enum value. */ +export function assetName(v: unknown): string { + const a = CHAINFLIP_ASSETS[variant(v)]; + return a ? `${a.symbol} (${a.chain})` : variant(v); +} + +/** "1.5 USDC (Ethereum)" for an amount of an Asset enum value (base units when the asset is unknown). */ +export function assetAmount(v: unknown, amount: unknown): string { + const a = CHAINFLIP_ASSETS[variant(v)]; + return a ? `${formatUnits(big(amount), a.decimals)} ${a.symbol} (${a.chain})` : `${big(amount)} ${variant(v)}`; +} + +export interface FlipAccount { + /** Everything in the account, bond included (`Flip.Account.balance`). Fees are paid from this. */ + balance: bigint; + /** Bonded by a validator; it can't be redeemed (`Flip.Account.bond`). */ + bond: bigint; + /** What can be redeemed to Ethereum now: balance − bond. */ + redeemable: bigint; + /** FLIP already on its way to Ethereum (not in `balance`), with the Ethereum address it goes to. */ + pendingRedemption: { amount: bigint; to: string } | null; +} + +export async function readFlipAccount(rpc: SubstrateRpc, rt: Runtime, me: string): Promise { + const acct = await readStorage<{ balance: bigint; bond: bigint }>(rpc, rt, "Flip", "Account", me); + const balance = big(acct?.balance); + const bond = big(acct?.bond); + const pending = await readStorage<{ total: bigint; redeem_address: unknown } | null>(rpc, rt, "Funding", "PendingRedemptions", me).catch(() => null); + return { + balance, + bond, + redeemable: balance > bond ? balance - bond : 0n, + pendingRedemption: pending ? { amount: big(pending.total), to: ethAddress(pending.redeem_address) } : null, + }; +} + +interface Out { + title: string; + lines: { label: string; value: string }[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + blind: boolean; +} + +export interface ChainflipCtx { + native: AssetRef; + isMe(address: string): boolean; + /** Describes a nested call (AccountRoles.as_sub_account). */ + inner(call: { type: string; value: { type: string; value: unknown } }): Promise<{ title: string; lines: { label: string; value: string }[]; warnings: Warning[]; blind: boolean }>; +} + +/** Plain words for a Chainflip call, or null to fall back to the generic `Pallet.call(args)` view. */ +export async function describeChainflip(pallet: string, name: string, a: Record, c: ChainflipCtx): Promise { + const sym = c.native.symbol; + const flip = (v: unknown) => `${formatUnits(big(v), c.native.decimals)} ${sym}`; + const out: Out = { title: "", lines: [], balanceChanges: [], warnings: [], blind: false }; + const foreign = (v: unknown, label: string) => { + const f = foreignAddress(v); + out.lines.push({ label, value: `${f.address} (${f.chain})` }); + out.warnings.push({ level: "caution", code: "network-matters", message: say("bg.chainflip.foreignRecipient", { to: short(f.address), chain: f.chain }) }); + return f; + }; + const otherAccount = (to: string) => { + if (!c.isMe(to)) out.warnings.push({ level: "caution", code: "new-recipient", message: say("bg.chainflip.otherAccount", { to: short(to) }) }); + }; + + switch (`${pallet}.${name}`) { + case "Funding.redeem": { + const to = ethAddress(a.address); + const amount = a.amount as { type?: string; value?: unknown } | undefined; + const exact = amount?.type === "Exact"; + out.title = exact ? say("bg.chainflip.redeem", { amount: flip(amount.value), to: short(to) }) : say("bg.chainflip.redeemAll", { symbol: sym, to: short(to) }); + out.lines.push({ label: "Amount", value: exact ? flip(amount.value) : say("bg.chainflip.allRedeemable", { symbol: sym }) }); + out.lines.push({ label: "Goes to", value: `${to} (Ethereum)` }); + const ex = a.executor == null ? null : ethAddress(a.executor); + out.lines.push({ label: say("bg.chainflip.label.executor"), value: ex ?? say("bg.chainflip.anyone") }); + out.lines.push({ label: "What happens", value: say("bg.chainflip.redeemSteps") }); + out.warnings.push({ level: "caution", code: "network-matters", message: say("bg.chainflip.redeemWarn", { symbol: sym, to: short(to) }) }); + if (exact) out.balanceChanges.push({ asset: c.native, delta: (-big(amount.value)).toString() }); + return out; + } + case "Funding.bind_redeem_address": { + const to = ethAddress(a.address); + out.title = say("bg.chainflip.bindRedeem", { to: short(to) }); + out.lines.push({ label: "Address", value: `${to} (Ethereum)` }); + out.warnings.push({ level: "danger", code: "new-recipient", message: say("bg.chainflip.bindRedeemWarn", { symbol: sym, to: short(to) }) }); + return out; + } + case "Funding.bind_executor_address": { + const to = ethAddress(a.executor_address); + out.title = say("bg.chainflip.bindExecutor", { to: short(to) }); + out.lines.push({ label: "Address", value: `${to} (Ethereum)` }); + out.warnings.push({ level: "danger", code: "new-recipient", message: say("bg.chainflip.bindExecutorWarn", { symbol: sym, to: short(to) }) }); + return out; + } + case "Funding.rebalance": { + const to = String(a.recipient_account_id); + const amount = a.amount as { type?: string; value?: unknown } | undefined; + const exact = amount?.type === "Exact"; + out.title = exact ? say("bg.chainflip.moveTo", { amount: flip(amount.value), to: short(to) }) : say("bg.chainflip.moveAllTo", { symbol: sym, to: short(to) }); + out.lines.push({ label: "To", value: to }, { label: "Amount", value: exact ? flip(amount.value) : say("bg.chainflip.allRedeemable", { symbol: sym }) }); + if (a.redemption_address != null) out.lines.push({ label: "Goes to", value: `${ethAddress(a.redemption_address)} (Ethereum)` }); + otherAccount(to); + if (exact && !c.isMe(to)) out.balanceChanges.push({ asset: c.native, delta: (-big(amount.value)).toString() }); + return out; + } + case "LiquidityProvider.register_lp_account": + out.title = say("bg.chainflip.registerLp"); + return out; + case "LiquidityProvider.deregister_lp_account": + out.title = say("bg.chainflip.deregisterLp"); + return out; + case "LiquidityProvider.register_liquidity_refund_address": { + const f = foreignAddress(a.address); + out.title = say("bg.chainflip.refundAddress", { chain: f.chain, to: short(f.address) }); + out.lines.push({ label: "Address", value: `${f.address} (${f.chain})` }); + out.warnings.push({ level: "caution", code: "network-matters", message: say("bg.chainflip.foreignRecipient", { to: short(f.address), chain: f.chain }) }); + return out; + } + case "LiquidityProvider.request_liquidity_deposit_address": + out.title = say("bg.chainflip.depositAddress", { asset: assetName(a.asset) }); + if (big(a.boost_fee) > 0n) out.lines.push({ label: say("bg.chainflip.label.boostFee"), value: `${big(a.boost_fee)} bps` }); + return out; + case "LiquidityProvider.withdraw_asset": { + const f = foreignAddress(a.destination_address); + out.title = say("bg.chainflip.withdraw", { amount: assetAmount(a.asset, a.amount), to: short(f.address), chain: f.chain }); + out.lines.push({ label: "Amount", value: assetAmount(a.asset, a.amount) }); + foreign(a.destination_address, "Goes to"); + return out; + } + case "LiquidityProvider.transfer_asset": { + const to = String(a.destination); + out.title = say("bg.chainflip.moveTo", { amount: assetAmount(a.asset, a.amount), to: short(to) }); + out.lines.push({ label: "To", value: to }, { label: "Amount", value: assetAmount(a.asset, a.amount) }); + otherAccount(to); + return out; + } + case "LiquidityProvider.schedule_swap": + out.title = say("bg.chainflip.swap", { amount: assetAmount(a.input_asset, a.amount), asset: assetName(a.output_asset) }); + out.lines.push({ label: "You pay", value: assetAmount(a.input_asset, a.amount) }, { label: "You get", value: assetName(a.output_asset) }); + return out; + case "Swapping.request_swap_deposit_address": + case "Swapping.request_swap_deposit_address_with_affiliates": { + out.title = say("bg.chainflip.swapChannel", { from: assetName(a.source_asset), to: assetName(a.destination_asset) }); + foreign(a.destination_address, "Sends what you get to"); + const refund = (a.refund_parameters as { refund_address?: unknown } | undefined)?.refund_address; + if (refund) { + const r = foreignAddress(refund); + out.lines.push({ label: say("bg.chainflip.label.refundTo"), value: `${r.address} (${r.chain})` }); + } + if (big(a.broker_commission) > 0n) out.lines.push({ label: "Fees", value: `${big(a.broker_commission)} bps` }); + if (big(a.boost_fee) > 0n) out.lines.push({ label: say("bg.chainflip.label.boostFee"), value: `${big(a.boost_fee)} bps` }); + return out; + } + case "LendingPools.add_boost_funds": + out.title = say("bg.chainflip.addBoost", { amount: assetAmount(a.asset, a.amount), tier: String(big(a.pool_tier)) }); + out.lines.push({ label: "Amount", value: assetAmount(a.asset, a.amount) }, { label: "Pool", value: `${assetName(a.asset)}, ${big(a.pool_tier)} bps` }); + return out; + case "LendingPools.stop_boosting": + out.title = say("bg.chainflip.stopBoost", { asset: assetName(a.asset), tier: String(big(a.pool_tier)) }); + return out; + case "LendingPools.add_lender_funds": + out.title = say("bg.chainflip.lend", { amount: assetAmount(a.asset, a.amount) }); + return out; + case "LendingPools.remove_lender_funds": + out.title = a.amount == null ? say("bg.chainflip.unlendAll", { asset: assetName(a.asset) }) : say("bg.chainflip.unlend", { amount: assetAmount(a.asset, a.amount) }); + return out; + case "LendingPools.request_loan": + out.title = say("bg.chainflip.borrow", { amount: assetAmount(a.loan_asset, a.loan_amount) }); + return out; + case "LendingPools.make_repayment": { + const amount = a.amount as { type?: string; value?: unknown } | undefined; + // The call carries no asset (it's the loan's), so a partial amount is shown in the asset's base units. + out.title = amount?.type === "Exact" ? say("bg.chainflip.repay", { id: String(big(a.loan_id)) }) : say("bg.chainflip.repayAll", { id: String(big(a.loan_id)) }); + if (amount?.type === "Exact") out.lines.push({ label: "Amount", value: String(big(amount.value)) }); + return out; + } + case "AccountRoles.as_sub_account": { + const inner = await c.inner(a.call as { type: string; value: { type: string; value: unknown } }); + const innerMsg = recallMsg(inner.title) ?? knownMsg(inner.title); + out.title = say("bg.chainflip.onSubAccount", { n: String(big(a.sub_account_index)), inner: innerMsg ?? inner.title }); + out.lines.push(...inner.lines); + out.warnings.push(...inner.warnings); + out.blind = inner.blind; + return out; + } + } + return null; +} diff --git a/packages/chains-substrate/src/defi.ts b/packages/chains-substrate/src/defi.ts index 6a27af29..5e52ad74 100644 --- a/packages/chains-substrate/src/defi.ts +++ b/packages/chains-substrate/src/defi.ts @@ -5,6 +5,7 @@ import { type ChainContext, ClipError } from "@clip-wallet/core"; import { Enum, fromBufferToBase58 } from "@polkadot-api/substrate-bindings"; import { constantOf, readStorage } from "./chain.js"; +import { isChainflip, readFlipAccount } from "./chainflip.js"; import { type Runtime, loadRuntime } from "./metadata.js"; import { type SubstrateSpec, specOf } from "./networks.js"; import { SubstrateRpc } from "./rpc.js"; @@ -76,6 +77,8 @@ export function existentialDeposit(rt: Runtime): bigint { * Polkadot SDK fungible "reducible balance" rule with Preservation::Preserve. */ export async function spendableNative(c: Connection): Promise { + // Chainflip: no Balances pallet; FLIP that can leave the account is Flip.Account balance − bond. + if (isChainflip(c.rt)) return (await readFlipAccount(c.rpc, c.rt, c.me)).redeemable; const acct = await readStorage<{ data: { free: bigint; reserved: bigint; frozen?: bigint } }>(c.rpc, c.rt, "System", "Account", c.me); const free = acct?.data.free ?? 0n; const reserved = acct?.data.reserved ?? 0n; diff --git a/packages/chains-substrate/src/describe.ts b/packages/chains-substrate/src/describe.ts index b68c3134..952e6b3f 100644 --- a/packages/chains-substrate/src/describe.ts +++ b/packages/chains-substrate/src/describe.ts @@ -5,6 +5,7 @@ */ import type { AssetRef, BalanceChange, NetworkId, Warning } from "@clip-wallet/core"; import { getSs58AddressInfo } from "@polkadot-api/substrate-bindings"; +import { describeChainflip } from "./chainflip.js"; import { locationAsset } from "./defi.js"; import { assetKey } from "./networks.js"; import { equal, formatUnits, hex, joinWords, short, textOf } from "./util.js"; @@ -28,6 +29,8 @@ export interface DescribeCtx { host: string; /** Asset Hub asset metadata by id (curated list or Assets.Metadata). */ asset(id: number): Promise; + /** Chainflip runtime (`isChainflip`): its Funding / LP / Swapping / LendingPools calls get plain words. */ + chainflip?: boolean; } export interface Described { @@ -300,6 +303,11 @@ export async function describeCall(call: DecodedCall, c: DescribeCtx, depth = 0) } } + if (c.chainflip) { + const cf = await describeChainflip(pallet, name, a, { native: c.native, isMe: (x) => isMe(x, c.me), inner: (x) => describeCall(x, c, depth + 1) }); + if (cf) return cf; + } + // Not described: show the decoded call. Root-level powers stay blind. out.title = `${pallet}.${name} for ${c.host}`; out.lines.push({ label: "Action", value: `${pallet}.${name}(${show(a)})` }); @@ -309,6 +317,11 @@ export async function describeCall(call: DecodedCall, c: DescribeCtx, depth = 0) } else { out.warnings.push({ level: "caution", code: "blind-signing", message: "Clip Wallet can't explain this action in plain words yet. Check the details before you sign." }); } + // Chainflip escrows up to 1 FLIP for LP order calls and scales their fee per call in a block (cf-flip + // on_charge_transaction.rs, runtime LpOrderCallIndexer), which TransactionPaymentApi's estimate doesn't show. + if (c.chainflip && pallet === "LiquidityPools" && /^(set|update)_(limit|range)_order$/.test(name)) { + out.warnings.push({ level: "caution", code: "high-fee", message: say("bg.chainflip.orderFee", { symbol: sym }) }); + } return out; } diff --git a/packages/chains-substrate/src/index.ts b/packages/chains-substrate/src/index.ts index 2dab0348..19dc1b77 100644 --- a/packages/chains-substrate/src/index.ts +++ b/packages/chains-substrate/src/index.ts @@ -1,4 +1,5 @@ export { readStorage, runtimeCall, storageKeys } from "./chain.js"; +export { CHAINFLIP_ASSETS, type FlipAccount, assetAmount, ethAddress, foreignAddress, isChainflip, readFlipAccount } from "./chainflip.js"; export { type AssetInfo, type DecodedCall, type Described, describeCall, show } from "./describe.js"; export { type Runtime, clearRuntimeCache, loadRuntime, provideRuntime, runtimeFromBytes } from "./metadata.js"; export * from "./module.js"; diff --git a/packages/chains-substrate/src/module.ts b/packages/chains-substrate/src/module.ts index ce78a0c6..334a980f 100644 --- a/packages/chains-substrate/src/module.ts +++ b/packages/chains-substrate/src/module.ts @@ -1,13 +1,14 @@ import { type AssetRef, type ChainContext, type ChainModule, ClipError, type DappRequest, type DecodedRequest, type Network, type Nft, type Signature, type SignablePayload, type TokenBalance, type Warning, WALLET_ORIGIN, msg, titled, type Msg } from "@clip-wallet/core"; -import { Enum, fromBufferToBase58, getSs58AddressInfo, u32 } from "@polkadot-api/substrate-bindings"; +import { Enum, fromBufferToBase58, u32 } from "@polkadot-api/substrate-bindings"; import { verify as sr25519Verify } from "@scure/sr25519"; import { readStorage, runtimeCall, storageKeys } from "./chain.js"; +import { type FlipAccount, isChainflip, readFlipAccount } from "./chainflip.js"; import { type AssetInfo, type DecodedCall, type Described, describeCall, mergeChanges } from "./describe.js"; import { type Runtime, loadRuntime, runtimeVersion } from "./metadata.js"; import { assetKey, fromChainId, nativeAsset, specOf, type SubstrateSpec } from "./networks.js"; import { type Payload, type SignerPayloadJSON, eraInfo, extensionParts, mortalEra, multiSignature, parsePayload, publicKeyOf, signedExtrinsic, signingBytes } from "./payload.js"; import { RpcError, SubstrateRpc, plainSubstrateError } from "./rpc.js"; -import { concat, equal, formatUnits, fromHex, hex0x, hostOf, isHex, randomId, textOf } from "./util.js"; +import { concat, equal, formatUnits, fromHex, hex0x, hostOf, isHex, randomId, ss58Info, textOf } from "./util.js"; /** * DappRequest methods. Injected (1Mask injectedWeb3 signer) and WalletConnect "polkadot" namespace names. @@ -68,6 +69,11 @@ export interface SubstrateModule extends ChainModule { * (finalized head, mortal era, next nonce, mode 0). decode() describes it like any other payload. */ buildCall(p: CallSpec, ctx: ChainContext): Promise; + /** + * Chainflip: FLIP in this account split into what pays fees (the whole balance, bond included), what can be + * redeemed to Ethereum (balance − bond) and any redemption in flight. Null on other networks. + */ + getFlipAccount(ctx: ChainContext): Promise; } type Normalized = @@ -136,7 +142,16 @@ function checkMine(ctx: ChainContext, address: string | Uint8Array): void { if (!equal(pub, myKey(ctx))) throw new ClipError("This request is for a different account than the one you connected.", "substrate/wrong-account"); } -/** 6-second blocks on relay chains and Asset Hubs (async backing). */ +/** sr25519 verify that answers false (never throws) for malformed signatures (no Schnorrkel marker bit). */ +function verifies(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean { + try { + return sr25519Verify(message, signature, publicKey); + } catch { + return false; + } +} + +/** 6-second blocks on relay chains, Asset Hubs (async backing) and Chainflip (measured 2026-10-06). */ const BLOCK_SECONDS = 6; export function createSubstrateModule(options: SubstrateModuleOptions = {}): SubstrateModule { @@ -232,6 +247,7 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub me: myKey(ctx), host, asset: (id) => assetInfo(rt, ctx, id), + chainflip: isChainflip(rt), }); } catch { d = { @@ -306,7 +322,7 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub const n = normalize(request); const bytes = await bytesToSign(n, ctx); const sig = signatures[0]; - if (!sig || signatures.length !== 1 || sig.scheme !== "sr25519" || sig.bytes.length !== 64 || !sr25519Verify(bytes, sig.bytes, myKey(ctx))) { + if (!sig || signatures.length !== 1 || sig.scheme !== "sr25519" || sig.bytes.length !== 64 || !verifies(bytes, sig.bytes, myKey(ctx))) { throw new ClipError("The signature didn't match. Nothing was sent.", "substrate/bad-signature"); } if (n.kind === "raw") return { signature: hex0x(multiSignature(null, sig.bytes)) }; @@ -333,6 +349,9 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub const spec = specFor(ctx); const { rt } = await runtimeFor(ctx); const me = ss58(myKey(ctx), rt.ss58); + // Chainflip has no Balances pallet (System.Account.data is Null): FLIP is Flip.Account.balance, bond included. + // That whole balance pays fees; only balance − bond can be redeemed (getFlipAccount has the split). + if (isChainflip(rt)) return [{ asset: nativeAsset(spec), amount: (await readFlipAccount(rpc, rt, me)).balance.toString() }]; const acct = await readStorage<{ data: { free: bigint; reserved: bigint } }>(rpc, rt, "System", "Account", me); const out: TokenBalance[] = [{ asset: nativeAsset(spec), amount: (acct?.data.free ?? 0n).toString() }]; if (!rt.pallets.has("Assets")) return out; @@ -354,6 +373,12 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub return out; } + async function getFlipAccount(ctx: ChainContext): Promise { + const { rt } = await runtimeFor(ctx); + if (!isChainflip(rt)) return null; + return readFlipAccount(rpcFor(ctx), rt, ss58(myKey(ctx), rt.ss58)); + } + async function offchain(data: Uint8Array, f: typeof fetch): Promise<{ name?: string; image?: string; attributes?: { trait: string; value: string }[] } | null> { const text = textOf(data)?.trim(); if (!text) return null; @@ -487,14 +512,16 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { const spec = specFor(ctx); - const info = getSs58AddressInfo(p.to.trim()); + const { rt } = await runtimeFor(ctx); + // Chainflip has no transfer call: FLIP enters by funding from Ethereum and leaves by Funding.redeem. + if (isChainflip(rt)) throw new ClipError(msg("bg.chainflip.noTransfer", { symbol: spec.symbol }), "substrate/no-transfers"); + const info = ss58Info(p.to.trim()); if (!info.isValid || info.publicKey.length !== 32) throw new ClipError("That doesn't look like a Polkadot address.", "substrate/bad-address"); if (info.ss58Format !== spec.ss58 && info.ss58Format !== 42) { throw new ClipError(`That address is formatted for a different network than ${spec.name}. Check it with the recipient.`, "substrate/wrong-network"); } if (equal(info.publicKey, myKey(ctx))) throw new ClipError("That's your own address.", "substrate/self-transfer"); const amount = amountOf(p.amount); - const { rt } = await runtimeFor(ctx); const dest = ss58(info.publicKey, rt.ss58); if (!p.asset.address) return payloadFor(ctx, rt, "Balances", "transfer_keep_alive", { dest: Enum("Id", dest), value: amount }); const id = Number(p.asset.address); @@ -545,7 +572,7 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub }, isAddress(value: string): boolean { try { - const i = getSs58AddressInfo(value.trim()); + const i = ss58Info(value.trim()); return i.isValid && i.publicKey.length === 32; } catch { return false; @@ -555,7 +582,7 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub networksForAddress(value: string, candidates: Network[]): Network[] { let info; try { - info = getSs58AddressInfo(value.trim()); + info = ss58Info(value.trim()); } catch { return []; } @@ -571,6 +598,7 @@ export function createSubstrateModule(options: SubstrateModuleOptions = {}): Sub getStaking, buildStake, buildCall, + getFlipAccount, }; } diff --git a/packages/chains-substrate/src/networks.ts b/packages/chains-substrate/src/networks.ts index 754d2e08..8245d1b1 100644 --- a/packages/chains-substrate/src/networks.ts +++ b/packages/chains-substrate/src/networks.ts @@ -29,6 +29,10 @@ export interface SubstrateSpec { * start times at historical blocks: Polkadot 24 h, Kusama 6 h, Westend 6 h, Paseo 6 h. */ eraHours?: number; + /** Native token name when it isn't the network's name (Chainflip's FLIP). */ + tokenName?: string; + /** Explorer path for an extrinsic hash (default "extrinsic", Subscan). */ + txPath?: string; } export const SUBSTRATE_SPECS: SubstrateSpec[] = [ @@ -154,12 +158,54 @@ export const SUBSTRATE_SPECS: SubstrateSpec[] = [ { id: 1984, symbol: "USDT", name: "Tether USD", decimals: 6, key: "usdt" }, ], }, + /* + * Chainflip State Chain (a Substrate solo chain). Read 2026-10-06 with chain_getBlockHash(0), system_properties + * ({ ss58Format: 2112, tokenDecimals: 18, tokenSymbol: "FLIP" }) and state_getRuntimeVersion (chainflip-node + * 20216 on mainnet, 20302 on Perseverance, transactionVersion 13). There's no Balances pallet: FLIP lives in + * `Flip.Account` ({ balance, bond }), reaches the State Chain by funding from Ethereum (StateChainGateway) and + * leaves by `Funding.redeem` to an Ethereum address. scan.chainflip.io takes an extrinsic hash at /extrinsics/. + */ + { + slug: "chainflip", + name: "Chainflip", + genesisHash: "0x8b8c140b0af9db70686583e3f6bf2a59052bfe9584b97d20c45068281e976eb9", + ss58: 2112, + symbol: "FLIP", + decimals: 18, + key: "flip", + testnet: false, + rpc: ["https://mainnet-rpc.chainflip.io", "https://rpc.chainflip.io"], + explorer: "https://scan.chainflip.io", + assetHub: false, + tokenName: "Chainflip", + txPath: "extrinsics", + }, + /* + * Perseverance, Chainflip's public testnet ("Chainflip-Perseverance"). Its FLIP is funded from tFLIP on Ethereum + * Sepolia (0xdC27c60956cB065D19F08bb69a707E37b36d8086, from the Chainflip Discord faucet) through + * auctions.perseverance.chainflip.io; the chain itself reports the symbol FLIP. + */ + { + slug: "chainflip-perseverance", + name: "Chainflip Perseverance", + genesisHash: "0x7a5d4db858ada1d20ed6ded4933c33313fc9673e5fffab560d0ca714782f2080", + ss58: 2112, + symbol: "FLIP", + decimals: 18, + key: "flip-testnet", + testnet: true, + rpc: ["https://archive.perseverance.chainflip.io", "https://perseverance.chainflip.xyz"], + explorer: "https://scan.perseverance.chainflip.io", + assetHub: false, + tokenName: "Test FLIP", + txPath: "extrinsics", + }, ]; export const caip2Of = (genesisHash: string): NetworkId => `polkadot:${genesisHash.replace(/^0x/, "").slice(0, 32).toLowerCase()}`; export function nativeAsset(spec: SubstrateSpec): AssetRef { - return { key: spec.key, symbol: spec.symbol, name: spec.name.replace(/ Asset Hub$/, ""), decimals: spec.decimals, networkId: caip2Of(spec.genesisHash) }; + return { key: spec.key, symbol: spec.symbol, name: spec.tokenName ?? spec.name.replace(/ Asset Hub$/, ""), decimals: spec.decimals, networkId: caip2Of(spec.genesisHash) }; } function network(spec: SubstrateSpec): Network { @@ -201,11 +247,13 @@ export const POLKADOT = SUBSTRATE_NETWORK("polkadot"); export const POLKADOT_ASSET_HUB = SUBSTRATE_NETWORK("polkadot-asset-hub"); export const KUSAMA = SUBSTRATE_NETWORK("kusama"); export const KUSAMA_ASSET_HUB = SUBSTRATE_NETWORK("kusama-asset-hub"); +export const CHAINFLIP = SUBSTRATE_NETWORK("chainflip"); +export const CHAINFLIP_PERSEVERANCE = SUBSTRATE_NETWORK("chainflip-perseverance"); export function assetKey(networkId: NetworkId, assetId: number): string { return specOf(networkId)?.assets?.find((a) => a.id === assetId)?.key ?? `asset:${assetId}`; } export function explorerTxUrl(net: Network, hash: string): string { - return `${net.explorerUrl}/extrinsic/${hash}`; + return `${net.explorerUrl}/${specOf(net.id)?.txPath ?? "extrinsic"}/${hash}`; } diff --git a/packages/chains-substrate/src/util.ts b/packages/chains-substrate/src/util.ts index e4d54653..055354b0 100644 --- a/packages/chains-substrate/src/util.ts +++ b/packages/chains-substrate/src/util.ts @@ -1,3 +1,6 @@ +import { getSs58AddressInfo } from "@polkadot-api/substrate-bindings"; +import { base58 } from "@scure/base"; + export function hex(bytes: Uint8Array): string { let s = ""; for (const b of bytes) s += b.toString(16).padStart(2, "0"); @@ -81,3 +84,16 @@ export function toBig(v: unknown, what: string): bigint { if (typeof v === "string" && /^\d+$/.test(v)) return BigInt(v); throw new Error(`bad ${what}`); } + +/** + * getSs58AddressInfo with the network format read correctly. polkadot-api 0.21 returns a two-byte prefix as a + * plain big-endian u16 (Chainflip's 2112 comes back as 20488) although its own fromBufferToBase58 writes the SS58 + * bit layout; this undoes that layout (ss58-registry formats 64..16383). + */ +export function ss58Info(address: string): ReturnType { + const i = getSs58AddressInfo(address); + if (!i.isValid) return i; + const b = base58.decode(address); + const ss58Format = b[0]! & 0x40 ? ((b[0]! & 0x3f) << 2) | (b[1]! >> 6) | ((b[1]! & 0x3f) << 8) : b[0]!; + return { ...i, ss58Format }; +} diff --git a/packages/chains-substrate/test/chainflip-fixtures.ts b/packages/chains-substrate/test/chainflip-fixtures.ts new file mode 100644 index 00000000..38b24c90 --- /dev/null +++ b/packages/chains-substrate/test/chainflip-fixtures.ts @@ -0,0 +1,85 @@ +/** + * Chainflip fixtures: real mainnet runtime metadata V15 (chainflip-node specVersion 20216, transactionVersion 13, + * read from https://mainnet-rpc.chainflip.io on 2026-10-06 with Metadata_metadata_at_version(15)), and payloads + * shaped like the ones lp.chainflip.io hands an injectedWeb3 signer. + */ +import type { Account } from "@clip-wallet/core"; +import { fromBufferToBase58 } from "@polkadot-api/substrate-bindings"; +import { readFileSync } from "node:fs"; +import { gunzipSync } from "node:zlib"; +import { CHAINFLIP, Enum, type Runtime, type SignerPayloadJSON, runtimeFromBytes } from "../src/index.js"; +import { concat, fromHex, hex0x } from "../src/util.js"; + +export const CF_METADATA = gunzipSync(readFileSync(new URL("./fixtures/chainflip-metadata-v15.scale.gz", import.meta.url))); +export const CF_VERSION = { specName: "chainflip-node", specVersion: 20216, transactionVersion: 13 }; +export const CF_GENESIS = "0x8b8c140b0af9db70686583e3f6bf2a59052bfe9584b97d20c45068281e976eb9"; +export const CF_BLOCK_HASH = `0x${"ab".repeat(32)}`; + +let rt: Runtime | undefined; +export function cfRuntime(): Runtime { + rt ??= runtimeFromBytes(CF_METADATA, CF_GENESIS, CF_VERSION); + return rt; +} + +/** + * sr25519 account 0 of the public "abandon … about" test phrase (the vault's substrate derivation: root key, no + * junctions; generic address 5EPCUjPx…, as packages/vault/test/families.test.ts), and another account. + * + * redeemSig: signingBytes(cfRuntime(), cfPayload(me, redeemCall())), signed once offline with the vault's + * signSr25519 in a scratch script outside the repo (AGENTS.md rule 1). sr25519 signatures are randomised, so + * tests only verify it. The same scratch run checked these signing bytes and the signed extrinsic byte for byte + * against @polkadot/types 17 (`ExtrinsicPayload.toU8a({ method: true })`, `Extrinsic.addSignature`) with this + * metadata, with and without CheckMetadataHash, and had a wallet-built register_lp_account from this account + * validated read-only by mainnet and Perseverance (`TaggedTransactionQueue_validate_transaction` → Payment, + * i.e. the signature and extensions were accepted and only FLIP for the fee was missing). + */ +export const CF_FIX = { + pub: "66933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972", + other: "29699b799d38ff33a0a722186972e05cf07b26a602407a52591e9a7cda1b94f1", + redeemSig: "82ced53a475513a34de349344ab836f24de7cfd77dd97499fc2f1d6572f1283b20dd6eab1ab390adc72ac9df03c33c4fdc6cfbf5ab017bc04d29d6cff3d37e8d", +} as const; + +export const cf = (pub: string) => fromBufferToBase58(2112)(fromHex(pub)); + +export function cfAccount(pub: string): Account { + return { id: "substrate:0", family: "substrate", index: 0, curve: "sr25519", derivationPath: "", publicKey: pub, address: fromBufferToBase58(42)(fromHex(pub)) }; +} + +export function cfCall(pallet: string, call: string, args: unknown): Uint8Array { + const c = cfRuntime().builder.buildCall(pallet, call); + return concat(Uint8Array.from(c.location), c.codec.enc(args)); +} + +/** Funding.redeem(Exact 5 FLIP) to 0xabab…ab, any executor: the call the LP portal's "Redeem" button sends. */ +export const REDEEM_TO = `0x${"ab".repeat(20)}`; +export const redeemCall = () => cfCall("Funding", "redeem", { amount: Enum("Exact", 5n * 10n ** 18n), address: REDEEM_TO, executor: undefined }); + +/** A SignerPayloadJSON as polkadot.js builds it for Chainflip (era mortal 64 from block 12345678, nonce 7). */ +export function cfPayload(address: string, method: Uint8Array, over: Partial = {}): SignerPayloadJSON { + return { + address, + assetId: null, + blockHash: CF_BLOCK_HASH, + blockNumber: "0x00bc614e", + era: "0xe500", + genesisHash: CF_GENESIS, + metadataHash: null, + method: hex0x(method), + mode: 0, + nonce: "0x00000007", + signedExtensions: cfRuntime().extensions.map((e) => e.identifier), + specVersion: "0x00004ef8", + tip: "0x00000000000000000000000000000000", + transactionVersion: "0x0000000d", + version: 4, + ...over, + }; +} + +export const cfEnc = { + storageKey: (pallet: string, entry: string, ...keys: unknown[]) => cfRuntime().builder.buildStorage(pallet, entry).keys.enc(...keys), + storageValue: (pallet: string, entry: string, value: unknown) => hex0x(cfRuntime().builder.buildStorage(pallet, entry).value.enc(value)), + apiResult: (api: string, method: string, value: unknown) => hex0x(cfRuntime().builder.buildRuntimeCall(api, method).value.enc(value)), +}; + +export { CHAINFLIP }; diff --git a/packages/chains-substrate/test/chainflip.test.ts b/packages/chains-substrate/test/chainflip.test.ts new file mode 100644 index 00000000..dd74e4d1 --- /dev/null +++ b/packages/chains-substrate/test/chainflip.test.ts @@ -0,0 +1,282 @@ +import type { DappRequest } from "@clip-wallet/core"; +import { Binary, compact } from "@polkadot-api/substrate-bindings"; +import { beforeEach, describe, expect, it } from "vitest"; +import { + CHAINFLIP, + CHAINFLIP_PERSEVERANCE, + Enum, + SUBSTRATE_METHODS, + SUBSTRATE_NETWORKS, + clearRuntimeCache, + createSubstrateModule, + ethAddress, + explorerTxUrl, + foreignAddress, + fromChainId, + isChainflip, + parsePayload, + provideRuntime, + signingBytes, +} from "../src/index.js"; +import { CF_BLOCK_HASH, CF_FIX, CF_GENESIS, cf, cfAccount, cfCall, cfEnc, cfPayload, cfRuntime, redeemCall, REDEEM_TO } from "./chainflip-fixtures.js"; +import { fixtureSigner, fromHex, hex, hex0x, mockRpc, ss58 } from "./helpers.js"; + +const ME = cf(CF_FIX.pub); +const OTHER = cf(CF_FIX.other); +const FEE = 17_783_696_000_000n; // TransactionPaymentApi.query_info partial_fee mainnet returned for a redeem (2026-10-06) + +const BASE = { + chain_getFinalizedHead: () => CF_BLOCK_HASH, + chain_getHeader: () => ({ number: "0xbc614e" }), + system_accountNextIndex: () => 7, + state_getRuntimeVersion: () => ({ specName: "chainflip-node", specVersion: 20216, transactionVersion: 13 }), +}; + +function setup(storage: Record = {}, methods: Record unknown> = {}, fee: bigint | null = FEE) { + clearRuntimeCache(); + provideRuntime(cfRuntime()); + const calls: Record = + fee === null ? {} : { TransactionPaymentApi_query_info: cfEnc.apiResult("TransactionPaymentApi", "query_info", { weight: { ref_time: 1000n, proof_size: 100n }, class: Enum("Normal"), partial_fee: fee }) }; + const rpc = mockRpc({ ...BASE, ...methods }, calls, storage); + return { m: createSubstrateModule(), ctx: { network: CHAINFLIP, account: cfAccount(CF_FIX.pub), fetch: rpc.fetch }, log: rpc.log }; +} + +const req = (method: string, params: unknown, origin = "https://lp.chainflip.io"): DappRequest => ({ + id: "r1", + origin, + via: "injected", + family: "substrate", + networkId: CHAINFLIP.id, + method, + params, +}); + +const signPayload = (method: Uint8Array, over = {}) => req(SUBSTRATE_METHODS.signPayload, cfPayload(ME, method, over)); +const lines = (d: { lines: { label: string; value: string }[] }) => Object.fromEntries(d.lines.map((l) => [l.label, l.value])); +const decodeCall = (bytes: Uint8Array) => cfRuntime().builder.buildDefinition(cfRuntime().callType).dec(bytes); + +beforeEach(() => clearRuntimeCache()); + +describe("Chainflip networks", () => { + it("has mainnet and Perseverance with the chain's own facts (genesis, ss58 2112, FLIP 18)", () => { + expect(CHAINFLIP).toMatchObject({ id: "polkadot:8b8c140b0af9db70686583e3f6bf2a59", family: "substrate", testnet: false, explorerUrl: "https://scan.chainflip.io" }); + expect(CHAINFLIP.rpcUrls).toEqual(["https://mainnet-rpc.chainflip.io", "https://rpc.chainflip.io"]); + expect(CHAINFLIP.nativeAsset).toEqual({ key: "flip", symbol: "FLIP", name: "Chainflip", decimals: 18, networkId: CHAINFLIP.id }); + expect(CHAINFLIP_PERSEVERANCE).toMatchObject({ id: "polkadot:7a5d4db858ada1d20ed6ded4933c3331", testnet: true, explorerUrl: "https://scan.perseverance.chainflip.io" }); + expect(CHAINFLIP_PERSEVERANCE.nativeAsset).toMatchObject({ key: "flip-testnet", symbol: "FLIP", decimals: 18 }); + expect(fromChainId(CF_GENESIS)).toBe(CHAINFLIP.id); + expect(fromChainId("chainflip-perseverance")).toBe(CHAINFLIP_PERSEVERANCE.id); + expect(explorerTxUrl(CHAINFLIP, "0x12")).toBe("https://scan.chainflip.io/extrinsics/0x12"); + }); + + it("spells the account cF… (ss58 2112) and matches cF addresses to Chainflip networks only", () => { + const m = createSubstrateModule(); + expect(ss58(CF_FIX.pub)).toBe("5EPCUjPxiHAcNooYipQFWr9NmmXJKpNG5RhcntXwbtUySrgH"); // vault account 0 (families.test.ts) + expect(m.addressFromPublicKey(fromHex(CF_FIX.pub), CHAINFLIP)).toBe(ME); + expect(ME.startsWith("cF")).toBe(true); + expect(m.networksForAddress(ME, SUBSTRATE_NETWORKS).map((n) => n.name)).toEqual(["Chainflip", "Chainflip Perseverance"]); + }); + + it("reads the runtime as Chainflip: Flip + Funding, no Balances, its own signed extensions", () => { + const rt = cfRuntime(); + expect(isChainflip(rt)).toBe(true); + expect(rt.ss58).toBe(2112); + expect(rt.extensions.map((e) => e.identifier)).toEqual([ + "AuthorizeCall", + "CheckNonZeroSender", + "CheckSpecVersion", + "CheckTxVersion", + "CheckGenesis", + "CheckMortality", + "CheckNonce", + "CheckWeight", + "ChargeTransactionPayment", + "CheckMetadataHash", + "WeightReclaim", + ]); + // AuthorizeCall and WeightReclaim are empty both ways: they must add no bytes. + for (const id of ["AuthorizeCall", "WeightReclaim"]) { + const e = rt.extensions.find((x) => x.identifier === id)!; + expect([rt.lookup(e.type).type, rt.lookup(e.additionalSigned).type]).toEqual(["void", "void"]); + } + }); +}); + +describe("Chainflip balances", () => { + it("reads FLIP from Flip.Account (bond included) and splits fees vs redeemable vs redeeming", async () => { + const { m, ctx } = setup({ + [cfEnc.storageKey("Flip", "Account", ME)]: cfEnc.storageValue("Flip", "Account", { balance: 7_500_000_000_000_000_000n, bond: 5_000_000_000_000_000_000n }), + [cfEnc.storageKey("Funding", "PendingRedemptions", ME)]: cfEnc.storageValue("Funding", "PendingRedemptions", { total: 10n ** 18n, restricted: 0n, redeem_address: REDEEM_TO }), + }); + expect(await m.getBalances(ctx)).toEqual([{ asset: CHAINFLIP.nativeAsset, amount: "7500000000000000000" }]); + await expect(m.getFlipAccount(ctx)).resolves.toEqual({ + balance: 7_500_000_000_000_000_000n, + bond: 5_000_000_000_000_000_000n, + redeemable: 2_500_000_000_000_000_000n, + pendingRedemption: { amount: 10n ** 18n, to: ethAddress(REDEEM_TO) }, + }); + }); + + it("an account Chainflip has never seen holds 0 FLIP", async () => { + const { m, ctx } = setup(); + expect(await m.getBalances(ctx)).toEqual([{ asset: CHAINFLIP.nativeAsset, amount: "0" }]); + }); +}); + +describe("Chainflip sends", () => { + it("refuses a FLIP transfer in plain words: redeem to Ethereum or fund from Ethereum", async () => { + const { m, ctx } = setup(); + const err = await m.buildTransfer({ asset: CHAINFLIP.nativeAsset, to: OTHER, amount: "1000" }, ctx).catch((e: unknown) => e); + expect(err).toMatchObject({ code: "substrate/no-transfers" }); + expect((err as { userMessage: string }).userMessage).toBe( + "FLIP can't be sent from one Chainflip account to another. To move it, redeem it to an Ethereum address, or fund the other account from Ethereum.", + ); + expect((err as { msg?: { id: string } }).msg?.id).toBe("bg.chainflip.noTransfer"); + }); +}); + +describe("Chainflip decode (LP portal / funding app calls)", () => { + it("Funding.redeem: amount, checksummed Ethereum address, executor, a caution and the fee", async () => { + const { m, ctx } = setup(); + const d = await m.decode(signPayload(redeemCall()), ctx); + const to = ethAddress(REDEEM_TO); + expect(to).toBe("0xABaBaBaBABabABabAbAbABAbABabababaBaBABaB"); + expect(d.title).toBe("Redeem 5 FLIP to 0xABaB…ABaB on Ethereum"); + expect(d.blind).toBe(false); + expect(lines(d)).toMatchObject({ Amount: "5 FLIP", "Goes to": `${to} (Ethereum)`, "Who can finish it": "Anyone", "Network fee": "0.000017783696 FLIP" }); + expect(d.balanceChanges).toEqual([{ asset: CHAINFLIP.nativeAsset, delta: "-5000000000000000000" }]); + expect(d.warnings).toContainEqual(expect.objectContaining({ level: "caution", code: "network-matters" })); + expect(d.fee).toEqual({ asset: CHAINFLIP.nativeAsset, amount: FEE.toString() }); + + const max = await m.decode(signPayload(cfCall("Funding", "redeem", { amount: Enum("Max"), address: REDEEM_TO, executor: `0x${"cd".repeat(20)}` })), ctx); + expect(max.title).toBe("Redeem all your FLIP to 0xABaB…ABaB on Ethereum"); + expect(lines(max)["Who can finish it"]).toBe(ethAddress(`0x${"cd".repeat(20)}`)); + expect(max.balanceChanges).toEqual([]); + }); + + it("bind_redeem_address / bind_executor_address are permanent: danger warnings", async () => { + const { m, ctx } = setup(); + const bind = await m.decode(signPayload(cfCall("Funding", "bind_redeem_address", { address: REDEEM_TO })), ctx); + expect(bind.title).toBe("Allow redemptions only to 0xABaB…ABaB, forever"); + expect(bind.warnings).toContainEqual(expect.objectContaining({ level: "danger", message: expect.stringMatching(/can never be undone/) })); + const ex = await m.decode(signPayload(cfCall("Funding", "bind_executor_address", { executor_address: REDEEM_TO })), ctx); + expect(ex.title).toBe("Let only 0xABaB…ABaB finish your redemptions, forever"); + expect(ex.warnings[0]).toMatchObject({ level: "danger", code: "new-recipient" }); + }); + + it("LiquidityProvider withdraw / transfer and swap channels show each chain's own address format", async () => { + const { m, ctx } = setup(); + const w = await m.decode(signPayload(cfCall("LiquidityProvider", "withdraw_asset", { amount: 1_500_000n, asset: Enum("SolUsdc"), destination_address: Enum("Sol", `0x${"01".repeat(32)}`) })), ctx); + const sol = foreignAddress(Enum("Sol", `0x${"01".repeat(32)}`)).address; + expect(sol).toBe("4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi"); + expect(w.title).toBe(`Withdraw 1.5 USDC (Solana) to ${sol.slice(0, 6)}…${sol.slice(-4)} on Solana`); + expect(w.warnings).toContainEqual(expect.objectContaining({ code: "network-matters" })); + + const t = await m.decode(signPayload(cfCall("LiquidityProvider", "transfer_asset", { amount: 10n ** 8n, asset: Enum("Btc"), destination: OTHER })), ctx); + expect(t.title).toBe(`Move 1 BTC (Bitcoin) to Chainflip account ${OTHER.slice(0, 6)}…${OTHER.slice(-4)}`); + expect(t.warnings).toContainEqual(expect.objectContaining({ code: "new-recipient" })); + + const swap = await m.decode( + signPayload( + cfCall("Swapping", "request_swap_deposit_address", { + source_asset: Enum("Eth"), + destination_asset: Enum("Btc"), + destination_address: Enum("Btc", Binary.fromText("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq")), + broker_commission: 0, + channel_metadata: undefined, + boost_fee: 5, + refund_parameters: { retry_duration: 100, refund_address: Enum("Eth", REDEEM_TO), min_price: [0n, 0n, 0n, 0n], refund_ccm_metadata: undefined, max_oracle_price_slippage: undefined }, + }), + ), + ctx, + ); + expect(swap.title).toBe("Open a channel to swap ETH (Ethereum) for BTC (Bitcoin)"); + expect(lines(swap)).toMatchObject({ "Sends what you get to": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq (Bitcoin)", "Refunds go to": `${ethAddress(REDEEM_TO)} (Ethereum)`, "Boost fee": "5 bps" }); + }); + + it("Boost, lending, LP registration and sub-account calls get plain words", async () => { + const { m, ctx } = setup(); + const title = async (bytes: Uint8Array) => (await m.decode(signPayload(bytes), ctx)).title; + expect(await title(cfCall("LendingPools", "add_boost_funds", { asset: Enum("Btc"), amount: 10_000_000n, pool_tier: 5 }))).toBe("Boost: add 0.1 BTC (Bitcoin) to the 5 bps pool"); + expect(await title(cfCall("LendingPools", "stop_boosting", { asset: Enum("Btc"), pool_tier: 5 }))).toBe("Stop boosting BTC (Bitcoin) in the 5 bps pool"); + expect(await title(cfCall("LendingPools", "add_lender_funds", { asset: Enum("Usdc"), amount: 250_000_000n }))).toBe("Lend 250 USDC (Ethereum)"); + expect(await title(cfCall("LendingPools", "remove_lender_funds", { asset: Enum("Usdc"), amount: undefined }))).toBe("Take back all the USDC (Ethereum) you lent"); + expect(await title(cfCall("LiquidityProvider", "register_lp_account", undefined))).toBe("Register as a Chainflip liquidity provider"); + expect(await title(cfCall("LiquidityProvider", "request_liquidity_deposit_address", { asset: Enum("ArbUsdc"), boost_fee: 0 }))).toBe("Get a deposit address for USDC (Arbitrum)"); + const inner = decodeCall(cfCall("LendingPools", "add_lender_funds", { asset: Enum("Usdt"), amount: 5_000_000n })); + expect(await title(cfCall("AccountRoles", "as_sub_account", { sub_account_index: 2, call: inner }))).toBe("On sub-account #2: Lend 5 USDT (Ethereum)"); + }); + + it("LP order calls stay as Pallet.call(args) with a caution about the 1 FLIP order escrow", async () => { + const { m, ctx } = setup(); + const d = await m.decode( + signPayload(cfCall("LiquidityPools", "set_limit_order", { base_asset: Enum("Eth"), quote_asset: Enum("Usdc"), side: Enum("Sell"), id: 1n, option_tick: 100, sell_amount: 10n ** 18n, dispatch_at: undefined, close_order_at: undefined })), + ctx, + ); + expect(d.title).toBe("LiquidityPools.set_limit_order for lp.chainflip.io"); + expect(d.lines[0]!.value).toMatch(/^LiquidityPools\.set_limit_order\(\{ base_asset: Eth, quote_asset: Usdc, side: Sell/); + expect(d.warnings.map((w) => w.code)).toEqual(["blind-signing", "high-fee"]); + expect(d.blind).toBe(false); + }); + + it("says nothing about the fee when TransactionPaymentApi can't answer", async () => { + const { m, ctx } = setup({}, {}, null); + const d = await m.decode(signPayload(redeemCall()), ctx); + expect(d.fee).toBeUndefined(); + expect(d.lines.some((l) => l.label === "Network fee")).toBe(false); + }); + + it("refuses a payload for another network", async () => { + const { m, ctx } = setup(); + await expect(m.decode(signPayload(redeemCall(), { genesisHash: "0x7a5d4db858ada1d20ed6ded4933c33313fc9673e5fffab560d0ca714782f2080" }), ctx)).rejects.toMatchObject({ + code: "substrate/network-mismatch", + }); + }); +}); + +describe("Chainflip signing", () => { + it("signs call ‖ extra ‖ additionalSigned with Chainflip's extensions (empty AuthorizeCall / WeightReclaim add nothing)", async () => { + const { m, ctx } = setup(); + const [p] = await m.prepare(signPayload(redeemCall()), ctx, "a1"); + // extra: era, nonce, tip, metadata mode | additional: spec, tx version, genesis, block (mortal), metadata hash None + const expected = new Uint8Array([ + ...redeemCall(), + ...fromHex("e500"), + ...compact.enc(7), + ...compact.enc(0), + 0, + ...fromHex("f84e0000"), + ...fromHex("0d000000"), + ...fromHex(CF_GENESIS), + ...fromHex(CF_BLOCK_HASH), + 0, + ]); + expect(hex(p!.bytes)).toBe(hex(expected)); + // CheckMetadataHash on: Some(RFC-0078 digest of this metadata with FLIP / 18). + const digest = hex0x(cfRuntime().metadataHash(18, "FLIP")); + const on = signingBytes(cfRuntime(), parsePayload(cfPayload(ME, redeemCall(), { mode: 1, metadataHash: digest }))); + expect(hex(on).endsWith(`01${digest.slice(2)}`)).toBe(true); + const d = await m.decode(signPayload(redeemCall(), { mode: 1, metadataHash: digest }), ctx); + expect(lines(d)["Metadata check"]).toBe("On (matches this network)"); + }); + + it("returns the MultiSignature and signed extrinsic, submits wallet-built payloads, refuses bad signatures before sending", async () => { + const { m, ctx, log } = setup({}, { author_submitExtrinsic: () => `0x${"ee".repeat(32)}` }); + const signer = fixtureSigner(CF_FIX.pub, [CF_FIX.redeemSig]); + const r = req(SUBSTRATE_METHODS.signPayload, { ...cfPayload(ME, redeemCall()), withSignedTransaction: true }); + const [p] = await m.prepare(r, ctx, "a1"); + const out = (await m.finalize(r, [signer.sign(p!)], ctx)) as { signature: string; signedTransaction: string }; + expect(out.signature).toBe(`0x01${CF_FIX.redeemSig}`); + const xt = fromHex(out.signedTransaction); + const body = xt.subarray([1, 2, 4].find((n) => compact.enc(xt.length - n).length === n)!); + expect(hex(body.subarray(0, 99))).toBe(`8400${CF_FIX.pub}01${CF_FIX.redeemSig}`); + expect(hex(body).endsWith(`e500${hex(compact.enc(7))}0000${hex(redeemCall())}`)).toBe(true); + + const submit = req(SUBSTRATE_METHODS.signAndSubmit, { payload: cfPayload(ME, redeemCall()) }, "clip-wallet"); + const bad = { scheme: "sr25519" as const, bytes: new Uint8Array(64).fill(7), publicKey: CF_FIX.pub }; + await expect(m.finalize(submit, [bad], ctx)).rejects.toMatchObject({ code: "substrate/bad-signature" }); + expect(log.some((c) => c.method === "author_submitExtrinsic")).toBe(false); + await expect(m.finalize(submit, [signer.sign(p!)], ctx)).resolves.toEqual({ txHash: `0x${"ee".repeat(32)}` }); + expect(log.filter((c) => c.method === "author_submitExtrinsic")).toHaveLength(1); + }); +}); diff --git a/packages/chains-substrate/test/fixtures/chainflip-metadata-v15.scale.gz b/packages/chains-substrate/test/fixtures/chainflip-metadata-v15.scale.gz new file mode 100644 index 00000000..d8590694 Binary files /dev/null and b/packages/chains-substrate/test/fixtures/chainflip-metadata-v15.scale.gz differ diff --git a/packages/chains-substrate/test/substrate.test.ts b/packages/chains-substrate/test/substrate.test.ts index d99be07c..d3644d9e 100644 --- a/packages/chains-substrate/test/substrate.test.ts +++ b/packages/chains-substrate/test/substrate.test.ts @@ -85,7 +85,7 @@ describe("networks and addresses", () => { expect(POLKADOT.id).toBe("polkadot:91b171bb158e2d3848fa23a9f1c25182"); expect(WESTEND_ASSET_HUB.id).toBe("polkadot:67f9723393ef76214df0118c34bbbd3d"); expect(fromChainId(GENESIS)).toBe(WESTEND_ASSET_HUB.id); - expect(SUBSTRATE_NETWORKS.filter((n) => n.testnet).map((n) => n.name)).toEqual(["Westend", "Paseo", "Westend Asset Hub", "Paseo Asset Hub"]); + expect(SUBSTRATE_NETWORKS.filter((n) => n.testnet).map((n) => n.name)).toEqual(["Westend", "Paseo", "Westend Asset Hub", "Paseo Asset Hub", "Chainflip Perseverance"]); expect(POLKADOT_ASSET_HUB.nativeAsset).toMatchObject({ key: "dot", symbol: "DOT", decimals: 10 }); }); diff --git a/packages/chains-tron/LICENSE b/packages/chains-tron/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-tron/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-tron/NOTICE b/packages/chains-tron/NOTICE new file mode 100644 index 00000000..a052f54a --- /dev/null +++ b/packages/chains-tron/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-tron +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-tron/README.md b/packages/chains-tron/README.md new file mode 100644 index 00000000..94213f47 --- /dev/null +++ b/packages/chains-tron/README.md @@ -0,0 +1,139 @@ +# @clip-wallet/chains-tron + +TRON `ChainModule` for Clip Wallet. It builds and decodes TRON transactions with hand-written protobuf (no TronWeb, +no protobuf runtime) and never touches keys. `prepare()` returns the 32-byte digest the vault signs (`txID` = +sha256(raw_data) for transactions, the TIP-104 keccak digest for messages). `finalize()` checks the vault's signature +with `secp256k1.verify` and recovers the account key with the recovery id before anything leaves the wallet, then +returns r ‖ s ‖ v (v = 27 + recovery, as TronWeb) and broadcasts when the method asks. + +Runtime dependencies: `@noble/curves`, `@noble/hashes`, `@scure/base`. TronWeb is a devDependency (tests cross-check +against it). + +## Networks + +| NetworkId (CAIP-2, Reown form) | chain id (`eth_chainId`) | endpoints (`rpcUrls`, keyless) | explorer | +|---|---|---|---| +| `tron:0xcd8690dc` Nile testnet (default) | 0xcd8690dc | https://nile.trongrid.io, https://api.nileex.io | https://nile.tronscan.org | +| `tron:0x94a9059e` Shasta testnet | 0x94a9059e | https://api.shasta.trongrid.io | https://shasta.tronscan.org | +| `tron:0x2b6653dc` mainnet | 0x2b6653dc | https://api.trongrid.io, https://api.tronstack.io, https://tron-rpc.publicnode.com | https://tronscan.org | + +- Ids are WalletConnect / Reown's `tron:` + the hex chain id (the last 4 bytes of the genesis block id). The + ChainAgnostic `tron` namespace (Draft) writes the same number in decimal (`tron:728126428`); `fromChainId` / + `netOf` accept both, the bare chain id and the genesis block id. +- Every endpoint was checked with `POST /wallet/getnowblock` and `/wallet/getblockbynum {"num":0}` (genesis ids in + `TRON_NETS`). The next endpoint is tried when one is down or rate-limited; none answering is `tron/offline`. +- Only TronGrid and api.nileex.io answer `/wallet/estimateenergy`; elsewhere the module uses `energy_used` (+ + `energy_penalty`) from `/wallet/triggerconstantcontract`. + +Assets: + +- TRX is `trx` (6 decimals, amounts in sun). +- Tether USDT is `usdt` on mainnet (`TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`). The Nile faucet's USDT test token + (`TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf`) is `trc20:TXYZ…`, never merged with real USDT. Shasta has none. +- USDC isn't listed: Circle stopped issuing USDC on TRON. USDD isn't listed: two contracts (old and 2.0) are live. +- Other TRC-20 tokens are `trc20:
`, TRC-10 tokens `trc10:`; a token named like USDT/USDC/USDD/TUSD/TRX/WTRX + that isn't the canonical contract is marked `spam`. + +## Accounts and addresses + +- `derivationPath(i)` = `m/44'/195'/0'/0/i` (TronLink, TronWeb.fromMnemonic), secp256k1. +- Address = base58check(0x41 ‖ keccak256(uncompressed key)[12..]). The tests reproduce TronWeb for the public + "abandon … about" phrase (`TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH`, `TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK`). +- `isAddress` accepts `T…` with a valid checksum only (dapps' hex `41…` is accepted inside transactions and params). +- The same address works on every TRON network (no `receiveAddress`). +- An account must exist on chain before it can send (`tron/not-open`). If its owner permission (or the active + permission a transaction names) doesn't give this key the threshold, signing is refused (`tron/not-controlled`). + That's what happened to the public test account on Nile: someone moved its owner permission to another key. + +## Methods (`TRON_METHODS`) + +| method | params | result | +|---|---|---| +| `tron_signTransaction` (Reown) | `{ address, transaction }`: a TronWeb transaction (`raw_data_hex`, optional `txID`, `raw_data`, `visible`), flat or the legacy nested `{ transaction: { … } }` | the transaction with `txID` and `signature: [r‖s‖v hex]` | +| `tron_signMessage` (Reown) | `{ address, message, encoding?: "hex" }` | `{ signature: "0x" + r‖s‖v }` (TronWeb signMessageV2) | +| `tron_signAndSendTransaction` (Clip) | as `tron_signTransaction` | `{ txID, result: true, transaction }` after broadcast | + +Checks on every transaction: only the hex is trusted. + +- `txID`, when given, must equal sha256(raw_data_hex) (`tron/txid-mismatch`). +- `raw_data` JSON, when given, must say what the hex says: block reference, expiration, timestamp, fee limit, memo, + contract type, type_url, Permission_id and every value field Clip explains (`tron/json-mismatch`). The tests check + this agrees with TronWeb's own `txCheck`. +- The reference block (ref_block_bytes / ref_block_hash) must be a block of this network (`tron/network-mismatch`): + a Nile transaction is never signed as a mainnet one. +- The contract's owner_address must be this account (`tron/not-a-signer`); expired transactions are refused + (`tron/expired`); the request's `address` must be this account (`tron/wrong-account`). + +## decode() + +Fee = the most TRX the transaction can burn with what the account has right now ("Network fee at most"): + +- Bandwidth: the signed size (raw + 67 signature bytes + 64 result bytes) is free if staked or the daily free + bandwidth covers all of it, else bytes × `getTransactionFee` (1000 sun). +- A TRX or TRC-10 transfer to an address that isn't on chain yet opens it: `getCreateNewAccountFeeInSystemContract` + (1 TRX) + `getCreateAccountFee` (0.1 TRX) unless staked bandwidth covers it. Shown as an info `new-recipient` + warning ("Sending to it also costs 1.1 TRX to open it"). +- A memo costs `getMemoFee` (1 TRX). +- Contract calls: estimated energy (`estimateenergy`, else `triggerconstantcontract`) minus the account's available + energy, × `getEnergyFee` (100 sun), at most the fee limit. A simulation that reverts is a danger `simulation-failed` + ("expected to fail … Reason: …") and the fee shown is the whole fee limit. + +| contract | title | notes | +|---|---|---| +| TransferContract | "Send 1.5 TRX to TSeJ…NQZK" | balance change; refused to yourself; TRX to a smart contract is refused (`tron/contract-recipient`) | +| TransferAssetContract (TRC-10) | "Send 5 ABC to …" | token name and precision from /wallet/getassetissuebyid; look-alikes marked spam | +| TriggerSmartContract `transfer` | "Send 1 USDT to …" | symbol/decimals from the known list or symbol()/decimals()/name() | +| TriggerSmartContract `transferFrom` | "Move 1 USDT from … to …" | | +| TriggerSmartContract `approve` | "Allow … to spend 5 USDT" / "…spend unlimited USDT" / "Stop … from spending your USDT" | danger `unlimited-approval` at ≥ 2^255 | +| TriggerSmartContract, other calldata | "Use 0x12345678 on contract …" | contract + 4-byte selector, `blind: true` | +| FreezeBalanceV2 / UnfreezeBalanceV2 | "Stake 10 TRX for energy" / "Unstake 1 TRX" | unstaking period from `getUnfreezeDelayDays` | +| WithdrawExpireUnfreeze / CancelAllUnfreezeV2 | "Withdraw unstaked TRX" / "Cancel your pending unstaking…" | | +| DelegateResource / UnDelegateResource | "Lend the energy of 5 TRX you've staked to …" / "Take back…" | lock period in hours | +| VoteWitness | "Vote for 2 Super Representatives" | each vote; info: replaces earlier votes | +| WithdrawBalance | "Claim your voting rewards" | | +| AccountPermissionUpdate | "Change who controls your TRON account" | danger `account-takeover`, owner/active keys and thresholds | +| other contract types | "Approve a transaction" (+ the type) | `blind: true` | +| bytes that don't parse, extra fields Clip doesn't explain, more than one contract | "Approve an unreadable request from …" | `blind: true` | + +Messages: TIP-104 / TronWeb `signMessageV2`, keccak256("\x19TRON Signed Message:\n" ‖ byte length ‖ message). +The legacy `signMessage` (fixed "32" length) isn't offered. + +## Builders + +`buildTransfer({ asset, to, amount })` → `tron_signAndSendTransaction`: + +- TRX → TransferContract; TRC-20 (`asset.address`) → TriggerSmartContract `transfer(address,uint256)` with + fee_limit = estimated energy × energy fee × 1.3, rounded up to 0.1 TRX (capped at `getMaxFeeLimit`). +- Reference block = the latest block; expiration = its time + 10 minutes (`expirationMs`). +- Refused: own address, a bad address, zero, an account that isn't open, not enough TRX for amount + worst-case fee, + not enough of the token, a simulation that reverts. +- After broadcasting (`/wallet/broadcasthex`), `/wallet/gettransactioninfobyid` is polled (10 × 3 s); a contract + that ran out of energy or reverted is `tron/failed`. Node errors become plain words (`plainTronError`). + +`staking(ctx)` returns staked TRX (Stake 2.0, including delegated), unstaking and withdrawable amounts, and energy and +bandwidth use, from the same calls as balances. `getBalances` returns TRX and USDT only. + +## Dapps + +1Mask's TRON provider (`packages/1mask/src/inpage/tron.ts`) implements TIP-1193 + TIP-1102 + TIP-3326 on +`window.clipwallet.tron`, announced with TIP-6963 under the wallet's own identity, and maps TronWeb's +`trx.sign` / `trx.signMessageV2` onto the two Reown methods above. + +## Tests + +`test/tron.test.ts`, no network: + +- Unsigned transactions captured from Nile with curl (`test/fixtures.ts`), cross-checked with TronWeb's + `txCheck`, `txJsonToPb` / `txPbToRawDataHex`, `ecRecover`, `hashMessage` and `Trx.verifyMessageV2`. +- Signatures precomputed offline with the vault from the public "abandon … about" vector (`test/signatures.ts`). + +## Sources + +- java-tron protobuf: https://github.com/tronprotocol/java-tron/tree/develop/protocol/src/main/protos/core +- Full-node HTTP API: https://developers.tron.network/reference/full-node-api-overview +- Resource model (bandwidth, energy, account creation): https://developers.tron.network/docs/resource-model +- TIP-104 (data signing), TIP-191, TIP-1193, TIP-1102, TIP-3326, TIP-6963: https://github.com/tronprotocol/tips +- TronWeb message signing: https://github.com/tronprotocol/tronweb/blob/master/src/utils/message.ts +- WalletConnect TRON methods and chain ids: https://github.com/reown-com/reown-docs/blob/main/advanced/multichain/rpc-reference/tron-rpc.mdx +- ChainAgnostic tron namespace (Draft): https://github.com/ChainAgnostic/namespaces/blob/main/tron/caip2.md +- Nile faucet and USDT test token: https://nileex.io/join/getJoinPage diff --git a/packages/chains-tron/package.json b/packages/chains-tron/package.json new file mode 100644 index 00000000..cb742964 --- /dev/null +++ b/packages/chains-tron/package.json @@ -0,0 +1,79 @@ +{ + "name": "@clip-wallet/chains-tron", + "version": "0.1.0", + "description": "TRON ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "tron", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-tron#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-tron" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "devDependencies": { + "tronweb": "^6.5.1" + } +} diff --git a/packages/chains-tron/src/abi.ts b/packages/chains-tron/src/abi.ts new file mode 100644 index 00000000..10b5bba3 --- /dev/null +++ b/packages/chains-tron/src/abi.ts @@ -0,0 +1,81 @@ +import { evmWord, fromEvmWord } from "./address.js"; +import { concat, fromHex, hex } from "./util.js"; + +/** + * The TRC-20 calls Clip explains (TIP-20, same ABI as ERC-20: https://github.com/tronprotocol/tips/blob/master/tip-20.md). + * Selectors are the first 4 bytes of keccak256 of the signature. + */ +export const SELECTORS = { + transfer: "a9059cbb", // transfer(address,uint256) + transferFrom: "23b872dd", // transferFrom(address,address,uint256) + approve: "095ea7b3", // approve(address,uint256) + balanceOf: "70a08231", // balanceOf(address) +} as const; + +export type Trc20Call = + | { kind: "transfer"; to: Uint8Array; amount: bigint } + | { kind: "transferFrom"; from: Uint8Array; to: Uint8Array; amount: bigint } + | { kind: "approve"; spender: Uint8Array; amount: bigint }; + +const word = (b: Uint8Array, i: number) => b.subarray(4 + i * 32, 4 + (i + 1) * 32); +const uint = (w: Uint8Array) => BigInt(`0x${hex(w) || "0"}`); +/** An ABI address word: 12 zero bytes then 20 bytes. */ +const isAddressWord = (w: Uint8Array) => w.length === 32 && w.subarray(0, 12).every((x) => x === 0); + +/** Decodes exactly-sized TRC-20 transfer / transferFrom / approve calldata; anything else is null. */ +export function decodeTrc20(data: Uint8Array): Trc20Call | null { + if (data.length < 4) return null; + const sel = hex(data.subarray(0, 4)); + const n = (data.length - 4) / 32; + if (sel === SELECTORS.transfer && n === 2 && isAddressWord(word(data, 0))) { + return { kind: "transfer", to: fromEvmWord(word(data, 0)), amount: uint(word(data, 1)) }; + } + if (sel === SELECTORS.transferFrom && n === 3 && isAddressWord(word(data, 0)) && isAddressWord(word(data, 1))) { + return { kind: "transferFrom", from: fromEvmWord(word(data, 0)), to: fromEvmWord(word(data, 1)), amount: uint(word(data, 2)) }; + } + if (sel === SELECTORS.approve && n === 2 && isAddressWord(word(data, 0))) { + return { kind: "approve", spender: fromEvmWord(word(data, 0)), amount: uint(word(data, 1)) }; + } + return null; +} + +function uintWord(v: bigint): Uint8Array { + if (v < 0n || v >= 1n << 256n) throw new Error("uint256 out of range"); + return fromHex(v.toString(16).padStart(64, "0")); +} + +const addressWord = (bytes21: Uint8Array) => concat(new Uint8Array(12), evmWord(bytes21)); + +export function encodeTransferCall(to21: Uint8Array, amount: bigint): Uint8Array { + return concat(fromHex(SELECTORS.transfer), addressWord(to21), uintWord(amount)); +} + +export function encodeBalanceOf(owner21: Uint8Array): Uint8Array { + return concat(fromHex(SELECTORS.balanceOf), addressWord(owner21)); +} + +/** uint256 from a constant call result (hex). */ +export function decodeUint(resultHex: string | undefined): bigint | null { + if (!resultHex || !/^[0-9a-f]{64,}$/i.test(resultHex)) return null; + return BigInt(`0x${resultHex.slice(0, 64)}`); +} + +/** ABI `string` (or a bytes32 symbol, as some old tokens return) from a constant call result. */ +export function decodeString(resultHex: string | undefined): string | null { + if (!resultHex || !/^([0-9a-f]{2})+$/i.test(resultHex)) return null; + const b = fromHex(resultHex); + try { + if (b.length === 32) return new TextDecoder("utf-8", { fatal: true }).decode(b.subarray(0, b.indexOf(0) < 0 ? 32 : b.indexOf(0))) || null; + if (b.length < 64) return null; + const off = Number(uint(b.subarray(0, 32)) & 0xffffffffn); + if (off + 32 > b.length) return null; + const len = Number(uint(b.subarray(off, off + 32)) & 0xffffffffn); + if (off + 32 + len > b.length || len > 64) return null; + return new TextDecoder("utf-8", { fatal: true }).decode(b.subarray(off + 32, off + 32 + len)) || null; + } catch { + return null; + } +} + +/** An approval this large is "unlimited" (max uint256, and anything past 2^255 that some apps use for it). */ +export const isUnlimited = (amount: bigint) => amount >= 1n << 255n; diff --git a/packages/chains-tron/src/address.ts b/packages/chains-tron/src/address.ts new file mode 100644 index 00000000..2f24bfa0 --- /dev/null +++ b/packages/chains-tron/src/address.ts @@ -0,0 +1,70 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { createBase58check } from "@scure/base"; +import { bytesEqual, concat, fromHex, hex } from "./util.js"; + +/** + * TRON addresses (https://developers.tron.network/docs/account): 21 bytes 0x41 ‖ keccak256(uncompressed key)[12..32], + * shown as base58check ("T…", 34 characters; checksum = first 4 bytes of sha256(sha256(payload))) or as hex "41…". + * Contract addresses use the same form. + */ +export const ADDRESS_PREFIX = 0x41; +const b58check = createBase58check(sha256); + +/** 21-byte address (0x41 ‖ 20 bytes) for a secp256k1 public key (33-byte compressed or 65-byte uncompressed). */ +export function addressBytesFromPublicKey(publicKey: Uint8Array): Uint8Array { + if (publicKey.length !== 33 && publicKey.length !== 65) throw new Error("secp256k1 public key must be 33 or 65 bytes"); + const uncompressed = secp256k1.Point.fromBytes(publicKey).toBytes(false); + return concat(new Uint8Array([ADDRESS_PREFIX]), keccak_256(uncompressed.subarray(1)).subarray(12)); +} + +export function encodeAddress(bytes21: Uint8Array): string { + if (bytes21.length !== 21 || bytes21[0] !== ADDRESS_PREFIX) throw new Error("not a TRON address"); + return b58check.encode(bytes21); +} + +/** 21-byte address from base58check "T…" or hex "41…" (0x optional). Null for anything else, including a bad checksum. */ +export function addressBytes(value: string): Uint8Array | null { + const v = value.trim(); + if (/^(0x)?41[0-9a-fA-F]{40}$/.test(v)) return fromHex(v.replace(/^0x/, "")); + if (!/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(v)) return null; + try { + const b = b58check.decode(v); + return b.length === 21 && b[0] === ADDRESS_PREFIX ? b : null; + } catch { + return null; + } +} + +/** Base58 "T…" for base58 or hex input; null if it isn't a TRON address. */ +export function toBase58(value: string): string | null { + const b = addressBytes(value); + return b ? encodeAddress(b) : null; +} + +/** "41…" hex for base58 or hex input. */ +export function toHex41(value: string): string | null { + const b = addressBytes(value); + return b ? hex(b) : null; +} + +export function isTronAddress(value: string): boolean { + return /^T/.test(value.trim()) && addressBytes(value) !== null; +} + +export function sameAddress(a: string | Uint8Array, b: string | Uint8Array): boolean { + const x = typeof a === "string" ? addressBytes(a) : a; + const y = typeof b === "string" ? addressBytes(b) : b; + return !!x && !!y && bytesEqual(x, y); +} + +/** The 20-byte EVM-style form TRON contracts see (ABI `address`), from a 21-byte address. */ +export function evmWord(bytes21: Uint8Array): Uint8Array { + return bytes21.subarray(1); +} + +/** A 21-byte TRON address from an ABI `address` word (last 20 bytes of 32). */ +export function fromEvmWord(word: Uint8Array): Uint8Array { + return concat(new Uint8Array([ADDRESS_PREFIX]), word.subarray(word.length - 20)); +} diff --git a/packages/chains-tron/src/describe.ts b/packages/chains-tron/src/describe.ts new file mode 100644 index 00000000..11798d03 --- /dev/null +++ b/packages/chains-tron/src/describe.ts @@ -0,0 +1,309 @@ +import { type AssetRef, type BalanceChange, ClipError, type Msg, type NetworkId, type Warning, msg, titled } from "@clip-wallet/core"; +import { decodeString, decodeTrc20, decodeUint, isUnlimited } from "./abi.js"; +import { encodeAddress, sameAddress } from "./address.js"; +import { trxAsset, usdtAsset } from "./networks.js"; +import type { TronRpc } from "./rpc.js"; +import { + type ContractEntry, + type PermissionParams, + parseDelegate, + parsePermissionUpdate, + parseResource, + parseTransfer, + parseTransferAsset, + parseTrigger, + parseVote, + showAddress, +} from "./tx.js"; +import { formatUnits, fromHex, hex, short } from "./util.js"; + +type Line = { label: string; value: string; labelMsg?: Msg; valueMsg?: Msg }; + +export interface Described { + title: string; + titleMsg?: Msg; + lines: Line[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + blind: boolean; + /** TransferContract / TransferAssetContract: the recipient, for the activation fee. */ + recipient?: Uint8Array; + /** TriggerSmartContract: what to run for the energy estimate and simulation. */ + call?: { contract: Uint8Array; data: Uint8Array; callValue: bigint }; +} + +export interface TokenInfo { + symbol: string; + decimals: number; + name: string; + known: boolean; +} + +export interface DescribeEnv { + networkId: NetworkId; + me: Uint8Array; + rpc: TronRpc; + token(contract: Uint8Array): Promise; + /** Chain parameter getUnfreezeDelayDays (14 on mainnet). */ + unstakeDays(): Promise; +} + +const trx = (sun: bigint) => `${formatUnits(sun, 6)} TRX`; +const resourceMsg = (code: bigint): Msg => (code === 1n ? msg("bg.tron.energy") : code === 2n ? msg("bg.tron.tronPower") : msg("bg.tron.bandwidth")); +const blindWarning = (): Warning => ({ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }); +const neg = (asset: AssetRef, amount: bigint): BalanceChange => ({ asset, delta: (-amount).toString() }); +const pos = (asset: AssetRef, amount: bigint): BalanceChange => ({ asset, delta: amount.toString() }); + +/** A TRC-20 token as an AssetRef: Tether USDT keeps its key; anything else is "trc20:
". */ +export function trc20Asset(networkId: NetworkId, contract: Uint8Array, info: TokenInfo | null): AssetRef { + const address = encodeAddress(contract); + const usdt = usdtAsset(networkId); + if (usdt && usdt.address === address) return usdt; + const symbol = info?.symbol ?? "tokens"; + const lookalike = /^(USDT|USDC|USDD|TUSD|TRX|WTRX)$/i.test(symbol); + return { key: `trc20:${address}`, symbol, name: info?.name ?? symbol, decimals: info?.decimals ?? 0, networkId, address, ...(lookalike ? { spam: true } : {}) }; +} + +/** Token metadata by constant calls (symbol(), decimals(), name()). */ +export async function readTokenInfo(rpc: TronRpc, owner: string, contract: Uint8Array, networkId: NetworkId): Promise { + const usdt = usdtAsset(networkId); + const address = encodeAddress(contract); + if (usdt && usdt.address === address) return { symbol: usdt.symbol, decimals: usdt.decimals, name: usdt.name, known: true }; + const call = async (selector: string) => (await rpc.triggerConstant(owner, address, fromHex(selector)).catch(() => null))?.constant_result?.[0]; + const [sym, dec, name] = await Promise.all([call("95d89b41"), call("313ce567"), call("06fdde03")]); + const symbol = decodeString(sym); + const decimals = decodeUint(dec); + if (!symbol || decimals === null || decimals > 36n) return null; + return { symbol: symbol.slice(0, 16), decimals: Number(decimals), name: (decodeString(name) ?? symbol).slice(0, 48), known: false }; +} + +function permissionLine(p: PermissionParams): string { + const keys = p.keys.map((k) => `${showAddress(k.address)} ×${k.weight}`).join(", "); + return msg("bg.tron.keysThreshold", { keys, threshold: p.threshold.toString() }).fallback; +} + +export async function describeContract(c: ContractEntry, env: DescribeEnv): Promise { + const TRX = trxAsset(env.networkId); + switch (c.name) { + case "TransferContract": { + const p = parseTransfer(c.value); + if (sameAddress(p.to, env.me)) throw new ClipError("That's your own address.", "tron/self-transfer"); + const to = showAddress(p.to); + return { + ...titled(msg("bg.req.sendTo", { amount: trx(p.amount), to: short(to) })), + lines: [{ label: "To", value: to }], + balanceChanges: [neg(TRX, p.amount)], + warnings: [], + blind: false, + recipient: p.to, + }; + } + case "TransferAssetContract": { + const p = parseTransferAsset(c.value); + if (sameAddress(p.to, env.me)) throw new ClipError("That's your own address.", "tron/self-transfer"); + const to = showAddress(p.to); + const info = await env.rpc + .post<{ abbr?: string; name?: string; precision?: number }>("/wallet/getassetissuebyid", { value: p.assetName }) + .catch(() => ({}) as { abbr?: string; name?: string; precision?: number }); + const text = (h?: string) => { + try { + return h ? new TextDecoder("utf-8", { fatal: true }).decode(fromHex(h)) : undefined; + } catch { + return undefined; + } + }; + const abbr = (text(info.abbr) ?? text(info.name) ?? "tokens").slice(0, 16); + const decimals = typeof info.precision === "number" && info.precision >= 0 && info.precision <= 18 ? info.precision : 0; + const lookalike = /^(USDT|USDC|USDD|TUSD|TRX|WTRX)$/i.test(abbr); + const asset: AssetRef = { key: `trc10:${p.assetName}`, symbol: abbr, name: text(info.name)?.slice(0, 48) ?? abbr, decimals, networkId: env.networkId, address: p.assetName, ...(lookalike ? { spam: true } : {}) }; + const warnings: Warning[] = lookalike ? [{ level: "caution", code: "known-scam", message: msg("bg.warn.spamToken", { symbol: abbr }).fallback, msg: msg("bg.warn.spamToken", { symbol: abbr }) }] : []; + return { + ...titled(msg("bg.req.sendTo", { amount: `${formatUnits(p.amount, decimals)} ${abbr}`, to: short(to) })), + lines: [ + { label: "To", value: to }, + { label: "Token", value: `${abbr} (TRC-10 ${p.assetName})` }, + ], + balanceChanges: [neg(asset, p.amount)], + warnings, + blind: false, + recipient: p.to, + }; + } + case "TriggerSmartContract": + return describeTrigger(c, env); + case "FreezeBalanceV2Contract": { + const p = parseResource(c.value); + const days = await env.unstakeDays(); + return { + ...titled(msg("bg.tron.stakeFor", { amount: trx(p.amount), resource: resourceMsg(p.resource) })), + lines: days !== null ? [{ label: "Unstaking takes", value: msg("bg.tron.days", { count: days.toString() }).fallback, valueMsg: msg("bg.tron.days", { count: days.toString() }) }] : [], + balanceChanges: [neg(TRX, p.amount)], + warnings: [], + blind: false, + }; + } + case "UnfreezeBalanceV2Contract": { + const p = parseResource(c.value); + const days = await env.unstakeDays(); + return { + ...titled(msg("bg.req.unstake", { amount: trx(p.amount) })), + lines: [ + { label: "Unstake", value: resourceMsg(p.resource).fallback, valueMsg: resourceMsg(p.resource) }, + ...(days !== null ? [{ label: "Unstaking takes", value: msg("bg.tron.days", { count: days.toString() }).fallback, valueMsg: msg("bg.tron.days", { count: days.toString() }) }] : []), + ], + balanceChanges: [], + warnings: [], + blind: false, + }; + } + case "WithdrawExpireUnfreezeContract": + return { ...titled(msg("bg.req.withdrawUnstaked", { symbol: "TRX" })), lines: [], balanceChanges: [], warnings: [], blind: false }; + case "CancelAllUnfreezeV2Contract": + return { ...titled(msg("bg.tron.cancelUnstaking")), lines: [], balanceChanges: [], warnings: [], blind: false }; + case "DelegateResourceContract": { + const p = parseDelegate(c.value); + const to = showAddress(p.receiver); + const lines: Line[] = [{ label: "To", value: to }]; + if (p.lock) { + const hours = ((p.lockPeriod > 0n ? p.lockPeriod : 86_400n) * 3n) / 3600n; // lock_period in 3-second blocks; 0 = the default 3 days + lines.push({ label: "Locked", value: msg("bg.tron.lockedHours", { hours: hours.toString() }).fallback, valueMsg: msg("bg.tron.lockedHours", { hours: hours.toString() }) }); + } + return { ...titled(msg("bg.tron.lend", { resource: resourceMsg(p.resource), amount: trx(p.balance), to: short(to) })), lines, balanceChanges: [], warnings: [], blind: false }; + } + case "UnDelegateResourceContract": { + const p = parseDelegate(c.value); + const to = showAddress(p.receiver); + return { + ...titled(msg("bg.tron.stopLending", { resource: resourceMsg(p.resource), amount: trx(p.balance), to: short(to) })), + lines: [{ label: "To", value: to }], + balanceChanges: [], + warnings: [], + blind: false, + }; + } + case "VoteWitnessContract": { + const p = parseVote(c.value); + return { + ...titled(msg("bg.tron.vote", { count: p.votes.length })), + lines: p.votes.map((v) => ({ label: "Validator", value: `${showAddress(v.address)}: ${v.count}` })), + balanceChanges: [], + warnings: [{ level: "info", code: "network-matters", message: msg("bg.tron.votesReplace").fallback, msg: msg("bg.tron.votesReplace") }], + blind: false, + }; + } + case "WithdrawBalanceContract": + return { ...titled(msg("bg.tron.claimVoteRewards")), lines: [], balanceChanges: [], warnings: [], blind: false }; + case "AccountPermissionUpdateContract": { + const p = parsePermissionUpdate(c.value); + const lines: Line[] = []; + if (p.ownerPermission) lines.push({ label: "Owner", value: permissionLine(p.ownerPermission) }); + if (p.witness) lines.push({ label: "Validator", value: permissionLine(p.witness) }); + p.actives.forEach((a) => lines.push({ label: "Permission", value: `${a.name || `#${a.id}`}: ${permissionLine(a)}` })); + return { + ...titled(msg("bg.tron.changePermissions")), + lines, + balanceChanges: [], + warnings: [{ level: "danger", code: "account-takeover", message: "This hands control of your account to someone else. They could take everything in it." }], + blind: false, + }; + } + default: + return { + ...titled(msg("bg.req.approveTx")), + lines: [{ label: "Type", value: c.name ?? `#${c.type}` }], + balanceChanges: [], + warnings: [blindWarning()], + blind: true, + }; + } +} + +async function describeTrigger(c: ContractEntry, env: DescribeEnv): Promise { + const TRX = trxAsset(env.networkId); + const p = parseTrigger(c.value); + const contract = showAddress(p.contract); + const call = { contract: p.contract, data: p.data, callValue: p.callValue }; + const alsoTrx: Line[] = p.callValue > 0n ? [{ label: "Also sends", value: trx(p.callValue) }] : []; + const trxOut: BalanceChange[] = p.callValue > 0n ? [neg(TRX, p.callValue)] : []; + const t = p.callTokenValue === 0n ? decodeTrc20(p.data) : null; + if (!t) { + const selector = p.data.length >= 4 ? `0x${hex(p.data.subarray(0, 4))}` : "(none)"; + return { + ...titled(msg("bg.req.useFnOnContract", { fn: selector, contract: short(contract) })), + lines: [ + { label: "Contract", value: contract }, + { label: "Function", value: selector }, + ...alsoTrx, + ...(p.callTokenValue > 0n ? [{ label: "Also sends", value: `${p.callTokenValue} (TRC-10 ${p.tokenId})` }] : []), + ], + balanceChanges: trxOut, + warnings: [blindWarning()], + blind: true, + call, + }; + } + const info = await env.token(p.contract); + const asset = trc20Asset(env.networkId, p.contract, info); + const amountOf = (v: bigint) => (info ? `${formatUnits(v, info.decimals)} ${info.symbol}` : `${v} ${asset.symbol}`); + const warnings: Warning[] = []; + if (!info) warnings.push({ level: "caution", code: "unknown-call", message: "Clip Wallet can name this call but can't fully read it, so some of its effects may not be shown." }); + if (asset.spam) warnings.push({ level: "caution", code: "known-scam", message: msg("bg.warn.spamToken", { symbol: asset.symbol }).fallback, msg: msg("bg.warn.spamToken", { symbol: asset.symbol }) }); + const tokenLine: Line = { label: "Token", value: `${asset.symbol} (${contract})` }; + + if (t.kind === "transfer") { + if (sameAddress(t.to, env.me)) throw new ClipError("That's your own address.", "tron/self-transfer"); + const to = showAddress(t.to); + return { + ...titled(msg("bg.req.sendTo", { amount: amountOf(t.amount), to: short(to) })), + lines: [{ label: "To", value: to }, tokenLine, ...alsoTrx], + balanceChanges: [neg(asset, t.amount), ...trxOut], + warnings, + blind: false, + call, + }; + } + if (t.kind === "transferFrom") { + const from = showAddress(t.from); + const to = showAddress(t.to); + const changes: BalanceChange[] = []; + if (sameAddress(t.from, env.me) && !sameAddress(t.to, env.me)) changes.push(neg(asset, t.amount)); + if (sameAddress(t.to, env.me) && !sameAddress(t.from, env.me)) changes.push(pos(asset, t.amount)); + return { + ...titled(msg("bg.req.moveFromTo", { amount: amountOf(t.amount), from: short(from), to: short(to) })), + lines: [{ label: "From", value: from }, { label: "To", value: to }, tokenLine, ...alsoTrx], + balanceChanges: [...changes, ...trxOut], + warnings, + blind: false, + call, + }; + } + const spender = showAddress(t.spender); + const lines: Line[] = [{ label: "Spender", value: spender }, tokenLine, ...alsoTrx]; + if (t.amount === 0n) { + return { ...titled(msg("bg.req.stopSpending", { spender: short(spender), symbol: asset.symbol })), lines, balanceChanges: trxOut, warnings, blind: false, call }; + } + if (isUnlimited(t.amount)) { + const m = msg("bg.warn.letsTakeAll", { spender: short(spender), symbol: asset.symbol }); + return { + ...titled(msg("bg.req.allowSpendUnlimited", { spender: short(spender), symbol: asset.symbol })), + lines, + balanceChanges: trxOut, + warnings: [{ level: "danger", code: "unlimited-approval", message: m.fallback, msg: m }, ...warnings], + blind: false, + call, + }; + } + return { ...titled(msg("bg.req.allowSpend", { spender: short(spender), amount: amountOf(t.amount) })), lines, balanceChanges: trxOut, warnings, blind: false, call }; +} + +/** Memo (raw.data) as a line. */ +export function memoLine(data: Uint8Array): Line | null { + if (!data.length) return null; + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(data); + if (!/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s)) return { label: "Memo", value: s }; + } catch { + /* not text */ + } + return { label: "Data (not text)", value: `0x${hex(data)}` }; +} diff --git a/packages/chains-tron/src/index.ts b/packages/chains-tron/src/index.ts new file mode 100644 index 00000000..bfff6568 --- /dev/null +++ b/packages/chains-tron/src/index.ts @@ -0,0 +1,28 @@ +export { TRON_METHODS, type TronModuleOptions, type TronStaking, controls, createTronModule, normalize } from "./module.js"; +export { + TRON_MAINNET, + TRON_NETS, + TRON_NETWORKS, + TRON_NILE, + TRON_SHASTA, + type TronNet, + type TronNetSpec, + chainIdHex, + explorerTxUrl, + fromChainId, + netOf, + specFor, + trxAsset, + usdtAsset, +} from "./networks.js"; +export { addressBytes, addressBytesFromPublicKey, encodeAddress, isTronAddress, sameAddress, toBase58, toHex41 } from "./address.js"; +export { TRON_MESSAGE_PREFIX, messageHash } from "./message.js"; +export { CONTRACT_TYPES, type ContractName, type RawTx, type TronWebTx, bandwidthBytes, encodeRaw, jsonMismatches, parseRaw, refBlockFields, txIdOf } from "./tx.js"; +export { SELECTORS, decodeTrc20, encodeTransferCall } from "./abi.js"; +export { trc20Asset } from "./describe.js"; +export { TronRpc, plainTronError } from "./rpc.js"; + +import { createTronModule } from "./module.js"; + +/** Default instance. */ +export const tronModule = createTronModule(); diff --git a/packages/chains-tron/src/message.ts b/packages/chains-tron/src/message.ts new file mode 100644 index 00000000..a7bcb276 --- /dev/null +++ b/packages/chains-tron/src/message.ts @@ -0,0 +1,16 @@ +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { concat, utf8 } from "./util.js"; + +/** + * TronWeb `trx.signMessageV2` / `Trx.verifyMessageV2` (TIP-104 "Data Signing", TIP-191-compatible 0x19 prefix): + * keccak256("\x19TRON Signed Message:\n" ‖ decimal byte length ‖ message), signature r ‖ s ‖ v with v = 27 + recovery. + * Source: tronweb src/utils/message.ts (`TRON_MESSAGE_PREFIX`, `hashMessage`, `signMessage` via ethers SigningKey + + * joinSignature): https://github.com/tronprotocol/tronweb/blob/master/src/utils/message.ts and + * https://github.com/tronprotocol/tips/blob/master/tip-104.md. The legacy `signMessage` (v1, fixed "32" length, + * hex-only input) isn't offered: its length field is wrong for anything but 32-byte messages. + */ +export const TRON_MESSAGE_PREFIX = "\x19TRON Signed Message:\n"; + +export function messageHash(message: Uint8Array): Uint8Array { + return keccak_256(concat(utf8(TRON_MESSAGE_PREFIX), utf8(String(message.length)), message)); +} diff --git a/packages/chains-tron/src/module.ts b/packages/chains-tron/src/module.ts new file mode 100644 index 00000000..8f729066 --- /dev/null +++ b/packages/chains-tron/src/module.ts @@ -0,0 +1,535 @@ +import { + type AssetRef, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + type Warning, + WALLET_ORIGIN, + isWalletOrigin, + msg, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { decodeUint, encodeBalanceOf, encodeTransferCall } from "./abi.js"; +import { addressBytes, addressBytesFromPublicKey, encodeAddress, isTronAddress, sameAddress } from "./address.js"; +import { type Described, type TokenInfo, describeContract, memoLine, readTokenInfo } from "./describe.js"; +import { messageHash } from "./message.js"; +import { specFor, trxAsset, usdtAsset } from "./networks.js"; +import { type TronAccount, type TronResources, type TronRpc, nodeMessage, plainTronError, rpcFor } from "./rpc.js"; +import { Writer } from "./proto.js"; +import { + ProtoError, + type RawTx, + type TronWebTx, + bandwidthBytes, + encodeRaw, + encodeTransfer, + encodeTrigger, + jsonMismatches, + ownerOf, + parseRaw, + txIdOf, + txObjectOf, +} from "./tx.js"; +import { big, bytesEqual, formatUnits, fromHex, hex, hostOf, randomId, sleep as defaultSleep, textOf, utf8 } from "./util.js"; + +/** + * Request methods. + * - `tron_signTransaction` / `tron_signMessage`: the WalletConnect (Reown) TRON methods + * (https://github.com/reown-com/reown-docs/blob/main/advanced/multichain/rpc-reference/tron-rpc.mdx). Params + * `{ address, transaction }` (a TronWeb transaction object, flat or the legacy nested `{ transaction: {…} }`) and + * `{ address, message }`. Results: the transaction with `signature: [hex]` added, and `{ signature: "0x…" }`. + * `tron_signMessage` also takes `encoding: "hex"` for byte messages (TronWeb's signMessageV2 accepts bytes); that's + * Clip's extension, used by 1Mask's TronWeb subset. + * - `tron_signAndSendTransaction`: Clip's own (same params as tron_signTransaction); the wallet also broadcasts and + * waits for the transaction to land. buildTransfer uses it. Result `{ txID, result: true, transaction }`. + */ +export const TRON_METHODS = { + signTransaction: "tron_signTransaction", + signMessage: "tron_signMessage", + signAndSendTransaction: "tron_signAndSendTransaction", +} as const; + +export interface TronModuleOptions { + now?: () => number; + sleep?: (ms: number) => Promise; + /** Expiration of transactions Clip builds, after the reference block's time (default 10 min; TRON allows 24 h). */ + expirationMs?: number; + /** Polls of /wallet/gettransactioninfobyid after broadcasting (default 10, every 3 s: about one block each). */ + confirmAttempts?: number; + confirmPollMs?: number; + /** Extra energy allowed in the fee limit of TRC-20 sends Clip builds, in percent of the estimate (default 30). */ + energyMarginPercent?: number; +} + +/** Staked TRX (Stake 2.0) and resources, in sun / units. Read from the same /wallet/getaccount call as balances. */ +export interface TronStaking { + staked: string; + /** Unstaked, still waiting out the unstaking period. */ + unstaking: string; + /** Unstaked and ready to withdraw (WithdrawExpireUnfreeze). */ + withdrawable: string; + energy: { used: string; limit: string }; + bandwidth: { used: string; limit: string; freeUsed: string; freeLimit: string }; +} + +type Normalized = + | { kind: "message"; bytes: Uint8Array } + | { kind: "tx"; tx: TronWebTx; rawBytes: Uint8Array; raw: RawTx | null; txId: Uint8Array; send: boolean }; + +const isObj = (p: unknown): p is Record => !!p && typeof p === "object" && !Array.isArray(p); +const trx = (sun: bigint) => `${formatUnits(sun, 6)} TRX`; +const NOT_CONTROLLED = () => new ClipError(msg("bg.tron.notControlled"), "tron/not-controlled"); +const WRONG_NETWORK = "This is for a different network, so Clip Wallet stopped it. Nothing was signed."; +const DEFAULT_ENERGY = 130_000n; // a TRC-20 transfer to an address that never held the token, when no estimate is possible + +export function normalize(request: DappRequest, ctx: ChainContext): Normalized { + const method = request.method; + if (method !== TRON_METHODS.signTransaction && method !== TRON_METHODS.signMessage && method !== TRON_METHODS.signAndSendTransaction) { + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "tron/unsupported-method"); + } + if (!specFor(request.networkId) || request.networkId !== ctx.network.id || ctx.network.family !== "tron") throw new ClipError(WRONG_NETWORK, "tron/network-mismatch"); + const p = isObj(request.params) ? request.params : {}; + if (p.address !== undefined && (typeof p.address !== "string" || !sameAddress(p.address, ctx.account.address))) { + throw new ClipError("This request is for a different account than the one you connected.", "tron/wrong-account"); + } + if (method === TRON_METHODS.signMessage) { + if (typeof p.message !== "string") throw new ClipError("This request from the app is malformed, so we stopped it.", "tron/malformed"); + if (p.encoding === "hex") { + if (!/^(0x)?([0-9a-f]{2})*$/i.test(p.message)) throw new ClipError("This request from the app is malformed, so we stopped it.", "tron/malformed"); + return { kind: "message", bytes: fromHex(p.message) }; + } + return { kind: "message", bytes: utf8(p.message) }; + } + let tx: TronWebTx; + try { + tx = txObjectOf(p.transaction ?? p); + } catch (e) { + throw new ClipError("This request from the app is malformed, so we stopped it.", "tron/malformed", e); + } + const rawBytes = fromHex(tx.raw_data_hex); + const txId = txIdOf(rawBytes); + // The txID the app shows must be the hash of the bytes that get signed. + if (tx.txID !== undefined && !bytesEqual(fromHex(tx.txID), txId)) throw new ClipError("This request from the app is malformed, so we stopped it.", "tron/txid-mismatch"); + let raw: RawTx | null = null; + try { + raw = parseRaw(rawBytes); + } catch (e) { + if (!(e instanceof ProtoError) && !(e instanceof TypeError)) throw e; + } + // Trust only the hex: JSON that says something else is refused. + if (raw && isObj(tx.raw_data) && jsonMismatches(tx.raw_data, raw).length) { + throw new ClipError("This request from the app is malformed, so we stopped it.", "tron/json-mismatch"); + } + return { kind: "tx", tx, rawBytes, raw, txId, send: method === TRON_METHODS.signAndSendTransaction }; +} + +/** Does `me` alone meet the threshold of the permission this transaction uses (0 = owner, ≥ 2 = an active one)? */ +export function controls(account: TronAccount, me: string, permissionId: number): boolean { + if (!account.address) return permissionId === 0; // not on chain yet: the default owner permission is the address itself + const perm = permissionId === 0 ? account.owner_permission : account.active_permission?.find((p) => (p.id ?? 2) === permissionId); + if (!perm) return permissionId === 0; + const weight = (perm.keys ?? []).filter((k) => sameAddress(k.address, me)).reduce((a, k) => a + BigInt(k.weight ?? 0), 0n); + return weight >= BigInt(perm.threshold ?? 1); +} + +export function createTronModule(options: TronModuleOptions = {}): ChainModule & { + normalize: typeof normalize; + staking(ctx: ChainContext): Promise; +} { + const now = options.now ?? (() => Date.now()); + const sleep = options.sleep ?? defaultSleep; + const expirationMs = BigInt(options.expirationMs ?? 10 * 60_000); + const attempts = options.confirmAttempts ?? 10; + const pollMs = options.confirmPollMs ?? 3000; + const margin = BigInt(options.energyMarginPercent ?? 30); + const paramsCache = new Map }>(); + const tokenCache = new Map>(); + + async function chainParams(ctx: ChainContext, rpc: TronRpc): Promise> { + const c = paramsCache.get(ctx.network.id); + if (c && now() - c.at < 10 * 60_000) return c.p; + const p = await rpc.chainParameters(); + paramsCache.set(ctx.network.id, { at: now(), p }); + return p; + } + const param = (p: Map, key: string, fallback: bigint) => p.get(key) ?? fallback; + + function tokenInfo(ctx: ChainContext, rpc: TronRpc, contract: Uint8Array): Promise { + const key = `${ctx.network.id}:${hex(contract)}`; + let v = tokenCache.get(key); + if (!v) { + v = readTokenInfo(rpc, ctx.account.address, contract, ctx.network.id); + tokenCache.set(key, v); + v.then((r) => r === null && tokenCache.delete(key)).catch(() => tokenCache.delete(key)); + } + return v; + } + + /** The reference block must be a block of THIS network (TAPOS): a transaction made for another TRON network is refused. */ + async function checkRefBlock(rpc: TronRpc, raw: RawTx): Promise { + if (raw.refBlockBytes.length !== 2 || raw.refBlockHash.length !== 8) throw new ClipError(WRONG_NETWORK, "tron/network-mismatch"); + const head = await rpc.nowBlock(); + const ref = (raw.refBlockBytes[0]! << 8) | raw.refBlockBytes[1]!; + const delta = BigInt((Number(head.number & 0xffffn) - ref + 65536) % 65536); + const num = head.number - delta; + if (num < 0n) throw new ClipError(WRONG_NETWORK, "tron/network-mismatch"); + const id = await rpc.blockId(num).catch(() => null); + if (id !== null && !bytesEqual(fromHex(id).subarray(8, 16), raw.refBlockHash)) throw new ClipError(WRONG_NETWORK, "tron/network-mismatch"); + } + + interface Fee { + total: bigint; + activation: bigint; + energy: bigint | null; + simulated: boolean; + warnings: Warning[]; + } + + /** + * The most TRX this transaction can burn, given what the account has right now (resources used earlier recover over + * 24 h; that recovery isn't counted, so this errs high). Bandwidth: the signed size in bytes, free if staked or the + * daily free bandwidth covers ALL of it, else bytes × getTransactionFee. A TRX/TRC-10 transfer that opens the + * recipient's account: getCreateNewAccountFeeInSystemContract, plus getCreateAccountFee unless staked bandwidth + * covers it (free bandwidth can't). A memo: getMemoFee. A contract call: (estimated energy − available energy) × + * getEnergyFee, at most the fee limit. (java-tron BandwidthProcessor / EnergyProcessor; TRON docs "Resource Model".) + */ + async function feeOf(ctx: ChainContext, rpc: TronRpc, raw: RawTx, d: Described): Promise { + const me = ctx.account.address; + const [p, res] = await Promise.all([chainParams(ctx, rpc), rpc.resources(me).catch((): TronResources => ({}))]); + const bytes = BigInt(bandwidthBytes(raw.bytes.length, 1, raw.contracts.length)); + const stakedNet = big(res.NetLimit) - big(res.NetUsed); + const freeNet = big(res.freeNetLimit) - big(res.freeNetUsed); + const warnings: Warning[] = []; + let bandwidth = stakedNet >= bytes || freeNet >= bytes ? 0n : bytes * param(p, "getTransactionFee", 1000n); + let activation = 0n; + if (d.recipient) { + const to = await rpc.account(encodeAddress(d.recipient)); + if (to.type === "Contract" && raw.contracts[0]?.name === "TransferContract") throw new ClipError(msg("bg.tron.contractRecipient"), "tron/contract-recipient"); + if (!to.address) { + activation = param(p, "getCreateNewAccountFeeInSystemContract", 1_000_000n) + (stakedNet >= bytes ? 0n : param(p, "getCreateAccountFee", 100_000n)); + bandwidth = 0n; + const m = msg("bg.tron.newAccountFee", { amount: trx(activation) }); + warnings.push({ level: "info", code: "new-recipient", message: m.fallback, msg: m }); + } + } + const memo = raw.data.length ? param(p, "getMemoFee", 1_000_000n) : 0n; + let energyBurn = 0n; + let energy: bigint | null = null; + let simulated = false; + if (d.call) { + const contract = encodeAddress(d.call.contract); + const sim = await rpc.triggerConstant(me, contract, d.call.data, d.call.callValue).catch(() => null); + simulated = !!sim?.result; + if (sim?.result && !sim.result.result) { + const reason = nodeMessage(sim.result.message).slice(0, 160); + const m = reason ? msg("bg.warn.expectedToFailReason", { reason }) : msg("bg.warn.expectedToFail"); + warnings.push({ level: "danger", code: "simulation-failed", message: m.fallback, msg: m }); + } + energy = await rpc.estimateEnergy(me, contract, d.call.data, d.call.callValue); + if (energy === null && sim?.result?.result && typeof sim.energy_used === "number") energy = BigInt(sim.energy_used) + BigInt(sim.energy_penalty ?? 0); + const available = big(res.EnergyLimit) - big(res.EnergyUsed); + const feeLimit = raw.feeLimit; + if (energy === null) { + energyBurn = feeLimit; + if (!sim) warnings.push({ level: "caution", code: "simulation-failed", message: "We couldn't preview the exact result of this on the network. Check the details before you approve." }); + } else { + energyBurn = (energy > available ? energy - available : 0n) * param(p, "getEnergyFee", 100n); + if (energyBurn > feeLimit) { + // Over the fee limit the call runs out of energy and fails, burning the whole limit. + energyBurn = feeLimit; + if (!warnings.some((w) => w.code === "simulation-failed")) warnings.push({ level: "danger", code: "simulation-failed", message: msg("bg.warn.expectedToFail").fallback, msg: msg("bg.warn.expectedToFail") }); + } + } + } + return { total: bandwidth + activation + memo + energyBurn, activation, energy, simulated, warnings }; + } + + async function account(rpc: TronRpc, me: string): Promise { + return rpc.account(me); + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const TRX = trxAsset(ctx.network.id); + const host = hostOf(req.origin); + if (n.kind === "message") { + const text = textOf(n.bytes); + const warnings: Warning[] = text === null ? [{ level: "caution", code: "blind-signing", message: "This message isn't readable text. It can't move funds by itself, but only sign it if you trust the site." }] : []; + const t = msg("bg.req.signMessage", { host }); + return { + requestId: req.id, + networkId: req.networkId, + title: t.fallback, + titleMsg: t, + lines: [text !== null ? { label: "Message", value: text } : { label: "Message (not text)", value: `0x${hex(n.bytes)}` }], + balanceChanges: [], + simulated: false, + blind: false, + warnings, + }; + } + + const me = ctx.account.address; + const me21 = addressBytes(me); + if (!me21) throw new ClipError("That address isn't valid. Check it and try again.", "tron/bad-account"); + const raw = n.raw; + const blind = (): DecodedRequest => ({ + requestId: req.id, + networkId: req.networkId, + title: msg("bg.req.unreadableFrom", { host }).fallback, + titleMsg: msg("bg.req.unreadableFrom", { host }), + lines: [{ label: "Data", value: `0x${hex(n.rawBytes).slice(0, 120)}${n.rawBytes.length > 60 ? "…" : ""}` }], + balanceChanges: [], + simulated: false, + blind: true, + warnings: [{ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }], + }); + // Exactly one contract (java-tron accepts only one), nothing Clip doesn't explain. + if (!raw || raw.contracts.length !== 1 || raw.oddities.length) return blind(); + const c = raw.contracts[0]!; + if (!c.name || c.extra || c.typeUrl !== `type.googleapis.com/protocol.${c.name}`) return blind(); + let owner: Uint8Array | null; + try { + owner = ownerOf(c); + } catch { + return blind(); + } + if (owner && !bytesEqual(owner, me21)) throw new ClipError("This transaction doesn't need your signature.", "tron/not-a-signer"); + if (raw.expiration <= BigInt(now())) throw new ClipError("This request expired. Please try again from the app.", "tron/expired"); + + const rpc = rpcFor(ctx); + const [acct] = await Promise.all([account(rpc, me), checkRefBlock(rpc, raw)]); + if (!controls(acct, me, c.permissionId)) throw NOT_CONTROLLED(); + + let d: Described; + try { + d = await describeContract(c, { + networkId: ctx.network.id, + me: me21, + rpc, + token: (contract) => tokenInfo(ctx, rpc, contract), + unstakeDays: async () => (await chainParams(ctx, rpc).catch(() => null))?.get("getUnfreezeDelayDays") ?? null, + }); + } catch (e) { + if (e instanceof ProtoError || e instanceof TypeError) return blind(); + throw e; + } + const fee = await feeOf(ctx, rpc, raw, d); + const lines = [...d.lines]; + const memo = memoLine(raw.data); + if (memo) lines.push(memo); + lines.push({ label: "Network fee at most", value: trx(fee.total) }); + if (!n.send && !isWalletOrigin(req.origin)) { + const m = msg("bg.tron.getsSigned", { host }); + lines.push({ label: "Sent by", value: m.fallback, valueMsg: m }); + } + const balanceChanges = [...d.balanceChanges]; + return { + requestId: req.id, + networkId: req.networkId, + title: d.title, + ...(d.titleMsg ? { titleMsg: d.titleMsg } : {}), + lines, + balanceChanges, + fee: { asset: TRX, amount: fee.total.toString() }, + simulated: fee.simulated, + blind: d.blind, + warnings: [...d.warnings, ...fee.warnings], + }; + } + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + const n = normalize(req, ctx); + const bytes = n.kind === "message" ? messageHash(n.bytes) : n.txId; + return [{ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes, approvalId }]; + } + + /** r ‖ s ‖ v (v = 27 + recovery, as TronWeb's ECKeySign / joinSignature), after checking it against the account key. */ + function signatureHex(sig: Signature | undefined, digest: Uint8Array, ctx: ChainContext): string { + const pub = fromHex(ctx.account.publicKey); + if (!sig || sig.scheme !== "ecdsa-secp256k1" || sig.bytes.length !== 64) throw new ClipError("The signature didn't match. Nothing was sent.", "tron/bad-signature"); + let ok = false; + try { + ok = secp256k1.verify(sig.bytes, digest, pub, { prehash: false }); + } catch { + ok = false; + } + if (!ok) throw new ClipError("The signature didn't match. Nothing was sent.", "tron/bad-signature"); + const s = secp256k1.Signature.fromBytes(sig.bytes, "compact"); + const want = secp256k1.Point.fromBytes(pub); + const order = sig.recovery === 1 ? [1, 0] : [0, 1]; + for (const rec of order) { + try { + if (s.addRecoveryBit(rec).recoverPublicKey(digest).equals(want)) return `${hex(sig.bytes)}${(27 + rec).toString(16)}`; + } catch { + /* try the other */ + } + } + throw new ClipError("The signature didn't match. Nothing was sent.", "tron/bad-signature"); + } + + async function confirm(rpc: TronRpc, txId: string): Promise { + for (let k = 0; k < attempts; k++) { + if (pollMs > 0) await sleep(pollMs); + const info = await rpc.post<{ id?: string; receipt?: { result?: string }; result?: string; resMessage?: string }>("/wallet/gettransactioninfobyid", { value: txId }).catch(() => null); + if (!info?.id) continue; + const r = info.receipt?.result; + if (info.result === "FAILED" || (r && r !== "SUCCESS" && r !== "DEFAULT")) { + throw new ClipError("This didn't go through. Check Activity before you try again.", "tron/failed"); + } + return; + } + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const n = normalize(req, ctx); + if (signatures.length !== 1) throw new ClipError("Some signatures are missing. Nothing was sent.", "tron/bad-signature"); + if (n.kind === "message") return { signature: `0x${signatureHex(signatures[0], messageHash(n.bytes), ctx)}` }; + + const sigHex = signatureHex(signatures[0], n.txId, ctx); + const txID = hex(n.txId); + const signed = { ...n.tx, txID, signature: [...(n.tx.signature ?? []).filter((s) => s.replace(/^0x/, "").toLowerCase() !== sigHex), sigHex] }; + if (!n.send) return signed; + + const body = new Writer().message(1, n.rawBytes); + for (const s of signed.signature) body.bytes(2, fromHex(s)); + const rpc = rpcFor(ctx); + const res = await rpc.broadcastHex(hex(body.finish())); + if (!res.result) throw new ClipError(plainTronError(res.code, nodeMessage(res.message)), "tron/send-failed"); + await confirm(rpc, txID); + return { txID, result: true, transaction: signed }; + } + + async function balanceOf(rpc: TronRpc, me: string, token: string): Promise { + const r = await rpc.triggerConstant(me, token, encodeBalanceOf(addressBytes(me)!)).catch(() => null); + return r?.result?.result ? decodeUint(r.constant_result?.[0]) : null; + } + + async function getBalances(ctx: ChainContext): Promise { + const rpc = rpcFor(ctx); + const me = ctx.account.address; + const acct = await rpc.account(me); + const out: TokenBalance[] = [{ asset: trxAsset(ctx.network.id), amount: big(acct.balance).toString() }]; + const usdt = usdtAsset(ctx.network.id); + if (usdt?.address) { + const v = await balanceOf(rpc, me, usdt.address); + if (v !== null) out.push({ asset: usdt, amount: v.toString() }); + } + return out; + } + + async function staking(ctx: ChainContext): Promise { + const rpc = rpcFor(ctx); + const me = ctx.account.address; + const [acct, res] = await Promise.all([rpc.account(me), rpc.resources(me).catch((): TronResources => ({}))]); + const t = BigInt(now()); + const staked = + (acct.frozenV2 ?? []).reduce((a, f) => a + big(f.amount), 0n) + + big(acct.delegated_frozenV2_balance_for_bandwidth) + + big(acct.account_resource?.delegated_frozenV2_balance_for_energy); + let unstaking = 0n; + let withdrawable = 0n; + for (const u of acct.unfrozenV2 ?? []) { + if (big(u.unfreeze_expire_time) <= t) withdrawable += big(u.unfreeze_amount); + else unstaking += big(u.unfreeze_amount); + } + return { + staked: staked.toString(), + unstaking: unstaking.toString(), + withdrawable: withdrawable.toString(), + energy: { used: big(res.EnergyUsed).toString(), limit: big(res.EnergyLimit).toString() }, + bandwidth: { used: big(res.NetUsed).toString(), limit: big(res.NetLimit).toString(), freeUsed: big(res.freeNetUsed).toString(), freeLimit: big(res.freeNetLimit).toString() }, + }; + } + + function request(ctx: ChainContext, rawBytes: Uint8Array): DappRequest { + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "tron", + networkId: ctx.network.id, + method: TRON_METHODS.signAndSendTransaction, + params: { address: ctx.account.address, transaction: { txID: hex(txIdOf(rawBytes)), raw_data_hex: hex(rawBytes), visible: true } }, + }; + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + if (ctx.network.family !== "tron" || !specFor(ctx.network.id)) throw new ClipError("That network isn't available in this wallet.", "tron/network-mismatch"); + const me = ctx.account.address; + const me21 = addressBytes(me)!; + const to = p.to.trim(); + if (!isTronAddress(to)) throw new ClipError("That address isn't valid. Check it and try again.", "tron/bad-address"); + if (sameAddress(to, me)) throw new ClipError("That's your own address.", "tron/self-transfer"); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "tron/bad-amount"); + const amount = BigInt(p.amount); + const to21 = addressBytes(to)!; + + const rpc = rpcFor(ctx); + const [acct, head] = await Promise.all([rpc.account(me), rpc.nowBlock()]); + if (!acct.address) throw new ClipError(msg("bg.tron.notOpen"), "tron/not-open"); + if (!controls(acct, me, 0)) throw NOT_CONTROLLED(); + const balance = big(acct.balance); + const base = { ref: head, timestamp: BigInt(now()), expiration: head.timestamp + expirationMs }; + + if (!p.asset.address) { + const rawBytes = encodeRaw({ ...base, name: "TransferContract", value: encodeTransfer(me21, to21, amount) }); + const raw = parseRaw(rawBytes); + const fee = await feeOf(ctx, rpc, raw, { title: "", lines: [], balanceChanges: [], warnings: [], blind: false, recipient: to21 }); + if (amount + fee.total > balance) throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: "TRX" }), "tron/insufficient-funds"); + return request(ctx, rawBytes); + } + + const token = addressBytes(p.asset.address); + if (!token || !isTronAddress(p.asset.address)) throw new ClipError("That token couldn't be found.", "tron/bad-asset"); + const symbol = p.asset.symbol; + const held = await balanceOf(rpc, me, p.asset.address); + if (held === null) throw new ClipError("That token couldn't be found.", "tron/bad-asset"); + if (held < amount) throw new ClipError(msg("bg.err.notEnough", { symbol }), "tron/insufficient-token"); + const data = encodeTransferCall(to21, amount); + const params = await chainParams(ctx, rpc); + const sim = await rpc.triggerConstant(me, p.asset.address, data); + if (sim.result && !sim.result.result) throw new ClipError("This is expected to fail and would still cost a fee.", "tron/would-fail"); + const estimate = + (await rpc.estimateEnergy(me, p.asset.address, data)) ?? + (typeof sim.energy_used === "number" ? BigInt(sim.energy_used) + BigInt(sim.energy_penalty ?? 0) : DEFAULT_ENERGY); + const maxFeeLimit = param(params, "getMaxFeeLimit", 15_000_000_000n); + let feeLimit = (estimate * param(params, "getEnergyFee", 100n) * (100n + margin)) / 100n; + feeLimit = ((feeLimit + 99_999n) / 100_000n) * 100_000n; // round up to 0.1 TRX + if (feeLimit > maxFeeLimit) feeLimit = maxFeeLimit; + const rawBytes = encodeRaw({ ...base, feeLimit, name: "TriggerSmartContract", value: encodeTrigger(me21, token, data) }); + const raw = parseRaw(rawBytes); + const fee = await feeOf(ctx, rpc, raw, { title: "", lines: [], balanceChanges: [], warnings: [], blind: false, call: { contract: token, data, callValue: 0n } }); + if (fee.total > balance) throw new ClipError(msg("bg.err.notEnoughForFee", { symbol: "TRX" }), "tron/insufficient-funds"); + return request(ctx, rawBytes); + } + + return { + family: "tron", + curve: "secp256k1", + /** TronLink / TronWeb.fromMnemonic: m/44'/195'/0'/0/i (account i = address index i). */ + derivationPath: (index: number) => `m/44'/195'/0'/0/${index}`, + addressFromPublicKey(publicKey: Uint8Array, _network: Network): string { + return encodeAddress(addressBytesFromPublicKey(publicKey)); + }, + isAddress: (value: string) => isTronAddress(value), + /** The same address works on every TRON network. */ + networksForAddress: (value: string, candidates: Network[]) => (isTronAddress(value) ? candidates.filter((c) => c.family === "tron") : []), + getBalances, + getNfts: async (_ctx: ChainContext): Promise => [], + decode, + prepare, + finalize, + buildTransfer, + normalize, + staking, + }; +} + diff --git a/packages/chains-tron/src/networks.ts b/packages/chains-tron/src/networks.ts new file mode 100644 index 00000000..25eab479 --- /dev/null +++ b/packages/chains-tron/src/networks.ts @@ -0,0 +1,129 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * TRON networks. + * + * Ids: the CAIP-2 `tron:` namespace as WalletConnect / Reown use it, "tron:" + the TRON chain id in hex, i.e. the last + * 4 bytes of the genesis block id (the value TRON's `eth_chainId` returns and TIP-1193 / TIP-3326 use): + * - Reown TRON RPC reference (chainId "tron:0xcd8690dc" for Nile): + * https://github.com/reown-com/reown-docs/blob/main/advanced/multichain/rpc-reference/tron-rpc.mdx + * - tronwallet-adapter's WalletConnect adapter (`tron:${chainId}`, mainnet 0x2b6653dc / Shasta 0x94a9059e / Nile + * 0xcd8690dc): https://github.com/tronweb3/tronwallet-adapter/blob/main/packages/adapters/walletconnect/src/adapter.ts + * - ChainAgnostic namespaces/tron/caip2.md (Draft, 2026-01) writes the same chain ids in decimal ("tron:728126428"). + * `fromChainId` accepts both spellings and the bare hex / decimal chain id. + * Genesis block ids were checked with POST /wallet/getblockbynum {"num":0} on every endpoint below + * (mainnet …1ebf88508a03865c71d452e25f4d51194196a1d22b6653dc, Nile …d698d4192c56cb6be724a558448e2684802de4d6cd8690dc, + * Shasta …de1aa88295e1fcf982742f773e0419c5a9c134c994a9059e). + * + * Endpoints: full-node HTTP API without an API key (TronGrid answers keyless requests, rate-limited). Second endpoints + * answer the same /wallet and /walletsolidity paths: TRON's own api.nileex.io (Nile) and api.tronstack.io / + * tron-rpc.publicnode.com (mainnet). Only TronGrid and api.nileex.io answer /wallet/estimateenergy; the module falls + * back to /wallet/triggerconstantcontract elsewhere. + */ +export type TronNet = "mainnet" | "nile" | "shasta"; + +export interface TronNetSpec { + caip2: NetworkId; + /** TRON chain id: last 4 bytes of the genesis block id, "0x…" (eth_chainId). */ + chainId: string; + /** Full genesis block id (hex). */ + genesisBlockId: string; + rpc: string[]; + explorer: string; + /** Tether USDT (TRC-20) on mainnet; the Nile faucet's USDT test token on Nile. */ + usdt?: string; +} + +export const TRON_NETS: Record = { + nile: { + caip2: "tron:0xcd8690dc", + chainId: "0xcd8690dc", + genesisBlockId: "0000000000000000d698d4192c56cb6be724a558448e2684802de4d6cd8690dc", + rpc: ["https://nile.trongrid.io", "https://api.nileex.io"], + explorer: "https://nile.tronscan.org", + // "USDT test tokens (trc20, token address: TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf)": https://nileex.io/join/getJoinPage + usdt: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + }, + shasta: { + caip2: "tron:0x94a9059e", + chainId: "0x94a9059e", + genesisBlockId: "0000000000000000de1aa88295e1fcf982742f773e0419c5a9c134c994a9059e", + rpc: ["https://api.shasta.trongrid.io"], + explorer: "https://shasta.tronscan.org", + }, + mainnet: { + caip2: "tron:0x2b6653dc", + chainId: "0x2b6653dc", + genesisBlockId: "00000000000000001ebf88508a03865c71d452e25f4d51194196a1d22b6653dc", + rpc: ["https://api.trongrid.io", "https://api.tronstack.io", "https://tron-rpc.publicnode.com"], + explorer: "https://tronscan.org", + // Tether USDT on TRON: https://tether.to/en/supported-protocols + usdt: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + }, +}; + +export function trxAsset(networkId: NetworkId): AssetRef { + return { key: "trx", symbol: "TRX", name: "TRON", decimals: 6, networkId }; +} + +/** Tether USDT (mainnet) shares the "usdt" key; the Nile test token is its own asset, never merged with real USDT. */ +export function usdtAsset(networkId: NetworkId): AssetRef | null { + const s = specFor(networkId); + if (!s?.usdt) return null; + const mainnet = s.caip2 === TRON_NETS.mainnet.caip2; + return { key: mainnet ? "usdt" : `trc20:${s.usdt}`, symbol: "USDT", name: mainnet ? "Tether USD" : "Tether USD (Nile test token)", decimals: 6, networkId, address: s.usdt }; +} + +function network(net: TronNet): Network { + const c = TRON_NETS[net]; + return { + id: c.caip2, + family: "tron", + name: net === "mainnet" ? "TRON" : net === "nile" ? "TRON Nile Testnet" : "TRON Shasta Testnet", + nativeAsset: trxAsset(c.caip2), + testnet: net !== "mainnet", + rpcUrls: c.rpc, + explorerUrl: c.explorer, + }; +} + +export const TRON_NILE = network("nile"); +export const TRON_SHASTA = network("shasta"); +export const TRON_MAINNET = network("mainnet"); +/** Testnets first: testnet by default. */ +export const TRON_NETWORKS: Network[] = [TRON_NILE, TRON_SHASTA, TRON_MAINNET]; + +/** + * Accepts "tron:0xcd8690dc", "tron:3448148188", "0xcd8690dc", "3448148188", a full genesis block id, or a name + * ("nile", "shasta", "mainnet"). + */ +export function netOf(chainId: string | number): TronNet | null { + let raw = String(chainId).trim().replace(/^tron:/i, "").toLowerCase(); + if ((Object.keys(TRON_NETS) as TronNet[]).includes(raw as TronNet)) return raw as TronNet; + if (/^[0-9a-f]{64}$/.test(raw)) raw = `0x${raw.slice(-8)}`; + let n: number | null = null; + if (/^0x[0-9a-f]{1,8}$/.test(raw)) n = parseInt(raw.slice(2), 16); + else if (/^\d{1,10}$/.test(raw)) n = Number(raw); + if (n === null) return null; + for (const [name, c] of Object.entries(TRON_NETS) as [TronNet, TronNetSpec][]) if (parseInt(c.chainId.slice(2), 16) === n) return name; + return null; +} + +export function fromChainId(chainId: string | number): NetworkId | null { + const n = netOf(chainId); + return n ? TRON_NETS[n].caip2 : null; +} + +export function specFor(networkId: NetworkId): TronNetSpec | null { + const n = netOf(networkId); + return n ? TRON_NETS[n] : null; +} + +/** The "0x…" chain id dapps see (TIP-1193 connect/chainChanged, eth_chainId). */ +export function chainIdHex(networkId: NetworkId): string | null { + return specFor(networkId)?.chainId ?? null; +} + +export function explorerTxUrl(net: Network, txId: string): string { + return `${net.explorerUrl}/#/transaction/${txId}`; +} diff --git a/packages/chains-tron/src/proto.ts b/packages/chains-tron/src/proto.ts new file mode 100644 index 00000000..b2be1f95 --- /dev/null +++ b/packages/chains-tron/src/proto.ts @@ -0,0 +1,134 @@ +/** + * The little protobuf (proto3 wire format) TRON transactions need: varints (int32/int64/bool/enum) and + * length-delimited fields (bytes, strings, messages). https://protobuf.dev/programming-guides/encoding/ + * int64 fields are two's complement 64-bit varints; negative values never appear in what Clip builds. + */ + +export type Field = { no: number; wire: 0; value: bigint } | { no: number; wire: 2; value: Uint8Array }; + +export class ProtoError extends Error {} + +export function readFields(buf: Uint8Array): Field[] { + const out: Field[] = []; + let i = 0; + const varint = (): bigint => { + let shift = 0n; + let v = 0n; + for (let n = 0; n < 10; n++) { + if (i >= buf.length) throw new ProtoError("truncated varint"); + const b = buf[i++]!; + v |= BigInt(b & 0x7f) << shift; + if (!(b & 0x80)) return v; + shift += 7n; + } + throw new ProtoError("varint too long"); + }; + while (i < buf.length) { + const key = varint(); + const no = Number(key >> 3n); + const wire = Number(key & 7n); + if (no < 1) throw new ProtoError("bad field number"); + if (wire === 0) out.push({ no, wire, value: varint() }); + else if (wire === 2) { + const len = Number(varint()); + if (len < 0 || i + len > buf.length) throw new ProtoError("truncated field"); + out.push({ no, wire, value: buf.subarray(i, i + len) }); + i += len; + } else throw new ProtoError(`unsupported wire type ${wire}`); // no TRON transaction field is fixed32/fixed64 + } + return out; +} + +/** int64 as protobuf stores it (two's complement over 64 bits). */ +export const int64 = (v: bigint): bigint => BigInt.asIntN(64, v); + +export class Fields { + constructor(readonly list: Field[]) {} + static parse(buf: Uint8Array): Fields { + return new Fields(readFields(buf)); + } + /** Last occurrence wins, as protobuf parsers do. */ + private last(no: number): Field | undefined { + for (let k = this.list.length - 1; k >= 0; k--) if (this.list[k]!.no === no) return this.list[k]; + return undefined; + } + int(no: number): bigint { + const f = this.last(no); + if (!f) return 0n; + if (f.wire !== 0) throw new ProtoError(`field ${no} is not a varint`); + return int64(f.value); + } + bool(no: number): boolean { + return this.int(no) !== 0n; + } + bytes(no: number): Uint8Array { + const f = this.last(no); + if (!f) return new Uint8Array(); + if (f.wire !== 2) throw new ProtoError(`field ${no} is not length-delimited`); + return f.value; + } + string(no: number): string { + return new TextDecoder("utf-8", { fatal: true }).decode(this.bytes(no)); + } + repeated(no: number): Uint8Array[] { + return this.list.filter((f) => f.no === no).map((f) => { + if (f.wire !== 2) throw new ProtoError(`field ${no} is not length-delimited`); + return f.value; + }); + } + has(no: number): boolean { + return this.list.some((f) => f.no === no); + } + /** Field numbers present that aren't in `known`. */ + unknown(known: readonly number[]): number[] { + return [...new Set(this.list.map((f) => f.no))].filter((n) => !known.includes(n)); + } +} + +/* ------------------------------------------------------------------ writer */ + +function varintBytes(v: bigint): number[] { + let x = BigInt.asUintN(64, v); + const out: number[] = []; + do { + let b = Number(x & 0x7fn); + x >>= 7n; + if (x > 0n) b |= 0x80; + out.push(b); + } while (x > 0n); + return out; +} + +export class Writer { + private parts: number[] = []; + /** Varint field; proto3 omits zero values. */ + int(no: number, v: bigint | number | boolean): this { + const x = typeof v === "boolean" ? (v ? 1n : 0n) : BigInt(v); + if (x === 0n) return this; + this.parts.push(...varintBytes(BigInt((no << 3) | 0)), ...varintBytes(x)); + return this; + } + /** Length-delimited field; proto3 omits empty values. */ + bytes(no: number, v: Uint8Array | undefined): this { + if (!v || v.length === 0) return this; + this.parts.push(...varintBytes(BigInt((no << 3) | 2)), ...varintBytes(BigInt(v.length))); + for (const b of v) this.parts.push(b); + return this; + } + /** Embedded message: written even when empty (presence matters for messages). */ + message(no: number, v: Uint8Array): this { + this.parts.push(...varintBytes(BigInt((no << 3) | 2)), ...varintBytes(BigInt(v.length))); + for (const b of v) this.parts.push(b); + return this; + } + string(no: number, v: string): this { + return this.bytes(no, new TextEncoder().encode(v)); + } + finish(): Uint8Array { + return Uint8Array.from(this.parts); + } +} + +export function varintLength(n: number): number { + return varintBytes(BigInt(n)).length; +} diff --git a/packages/chains-tron/src/rpc.ts b/packages/chains-tron/src/rpc.ts new file mode 100644 index 00000000..1c2730bd --- /dev/null +++ b/packages/chains-tron/src/rpc.ts @@ -0,0 +1,171 @@ +import { type ChainContext, ClipError } from "@clip-wallet/core"; +import { big, fromHex, hex } from "./util.js"; +import type { RefBlock } from "./tx.js"; + +/** + * java-tron full-node HTTP API (https://developers.tron.network/reference/full-node-api-overview), called with plain + * `ctx.fetch` over the network's endpoints in order (the next one is tried when one can't be reached or is busy). + * Addresses go out as base58 with `visible: true`. + */ + +export interface TronAccount { + address?: string; + balance?: number; + type?: string; + create_time?: number; + owner_permission?: TronPermission; + active_permission?: TronPermission[]; + frozenV2?: { type?: string; amount?: number }[]; + unfrozenV2?: { type?: string; unfreeze_amount?: number; unfreeze_expire_time?: number }[]; + delegated_frozenV2_balance_for_bandwidth?: number; + account_resource?: { delegated_frozenV2_balance_for_energy?: number }; +} + +export interface TronPermission { + type?: string; + id?: number; + permission_name?: string; + threshold?: number; + operations?: string; + keys?: { address: string; weight: number }[]; +} + +export interface TronResources { + freeNetUsed?: number; + freeNetLimit?: number; + NetUsed?: number; + NetLimit?: number; + EnergyUsed?: number; + EnergyLimit?: number; +} + +export interface ConstantResult { + result?: { result?: boolean; code?: string; message?: string }; + constant_result?: string[]; + energy_used?: number; + energy_penalty?: number; +} + +export class TronRpcError extends Error {} + +/** Messages java-tron returns hex-encoded (broadcast, contract results). */ +export function nodeMessage(m: unknown): string { + if (typeof m !== "string") return ""; + if (/^([0-9a-f]{2})+$/i.test(m)) { + try { + return new TextDecoder("utf-8", { fatal: true }).decode(fromHex(m)); + } catch { + return m; + } + } + return m; +} + +export class TronRpc { + constructor( + private readonly urls: string[], + private readonly f: typeof fetch, + ) {} + + async post(path: string, body: unknown = {}): Promise { + let last: unknown; + for (const base of this.urls) { + try { + const res = await this.f(`${base.replace(/\/$/, "")}${path}`, { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + // TronGrid answers keyless requests over its limit with 403/429; try the next endpoint. + last = new TronRpcError(`HTTP ${res.status}`); + continue; + } + const text = await res.text(); + return (text ? JSON.parse(text) : {}) as T; + } catch (e) { + last = e; + } + } + throw new ClipError("Couldn't reach the TRON network. Check your connection and try again.", "tron/offline", last); + } + + async nowBlock(): Promise { + const b = await this.post<{ blockID?: string; block_header?: { raw_data?: { number?: number; timestamp?: number } } }>("/wallet/getnowblock"); + if (!b.blockID || !/^[0-9a-f]{64}$/i.test(b.blockID)) throw new ClipError("The TRON network didn't answer properly. Try again.", "tron/offline"); + return { number: big(b.block_header?.raw_data?.number), blockId: b.blockID, timestamp: big(b.block_header?.raw_data?.timestamp) }; + } + + async blockId(num: bigint): Promise { + const b = await this.post<{ blockID?: string }>("/wallet/getblockbynum", { num: Number(num) }); + return b.blockID && /^[0-9a-f]{64}$/i.test(b.blockID) ? b.blockID.toLowerCase() : null; + } + + /** `{}` (no `address`) for an account that isn't on chain yet. */ + account(address: string): Promise { + return this.post("/wallet/getaccount", { address, visible: true }); + } + + resources(address: string): Promise { + return this.post("/wallet/getaccountresource", { address, visible: true }); + } + + async chainParameters(): Promise> { + const r = await this.post<{ chainParameter?: { key: string; value?: number }[] }>("/wallet/getchainparameters"); + return new Map((r.chainParameter ?? []).map((p) => [p.key, big(p.value)])); + } + + /** Runs a contract call without sending it (no fee, no signature). */ + triggerConstant(owner: string, contract: string, data: Uint8Array, callValue = 0n): Promise { + return this.post("/wallet/triggerconstantcontract", { + owner_address: owner, + contract_address: contract, + data: hex(data), + ...(callValue ? { call_value: Number(callValue) } : {}), + visible: true, + }); + } + + /** Energy the call needs (null where the node doesn't offer /wallet/estimateenergy, or the call reverts). */ + async estimateEnergy(owner: string, contract: string, data: Uint8Array, callValue = 0n): Promise { + const r = await this.post<{ result?: { result?: boolean }; energy_required?: number }>("/wallet/estimateenergy", { + owner_address: owner, + contract_address: contract, + data: hex(data), + ...(callValue ? { call_value: Number(callValue) } : {}), + visible: true, + }).catch(() => null); + return r?.result?.result && typeof r.energy_required === "number" ? BigInt(r.energy_required) : null; + } + + broadcastHex(signedHex: string): Promise<{ result?: boolean; txid?: string; code?: string; message?: string }> { + return this.post("/wallet/broadcasthex", { transaction: signedHex }); + } + + /** Solidified transaction info ({} until the transaction is in a solidified block). */ + async txInfo(txId: string): Promise<{ id?: string; blockNumber?: number; receipt?: { result?: string }; result?: string; resMessage?: string }> { + return this.post("/walletsolidity/gettransactioninfobyid", { value: txId }); + } +} + +export const rpcFor = (ctx: ChainContext) => new TronRpc(ctx.network.rpcUrls, ctx.fetch); + +/** + * Broadcast errors in plain words (java-tron `Return.response_code` plus its message). Hints carry what the module + * knows that the node doesn't say. + */ +export function plainTronError(code: string | undefined, message: string): string { + const m = message.toLowerCase(); + if (code === "SIGERROR" || m.includes("validate signature error")) return "The network didn't accept the signature. Nothing was sent."; + if (code === "BANDWITH_ERROR" || m.includes("account resource insufficient")) return "You need a little TRX to pay for this transaction's bandwidth. Nothing was sent."; + if (code === "TAPOS_ERROR" || m.includes("tapos")) return "This transaction was made for a different TRON network, or too long ago. Nothing was sent."; + if (code === "TRANSACTION_EXPIRATION_ERROR" || m.includes("expired")) return "This transaction expired before it was sent. Try again."; + if (code === "DUP_TRANSACTION_ERROR" || m.includes("dup transaction")) return "This transaction was already sent."; + if (m.includes("balance is not sufficient") || m.includes("balance is not enough") || m.includes("not enough balance")) return "You don't have enough TRX for this, including the network fee. Nothing was sent."; + if (m.includes("cannot transfer trx to yourself")) return "That's your own address."; + if (m.includes("smart contract")) return "TRX can't be sent straight to a smart contract. Nothing was sent."; + if (m.includes("does not exist") || m.includes("not exists")) return "Your TRON account isn't open yet. Receive some TRX first."; + if (m.includes("permission")) return "This account's permissions don't let this key sign that. Nothing was sent."; + if (code === "SERVER_BUSY" || code === "NOT_ENOUGH_EFFECTIVE_CONNECTION") return "The TRON network is busy. Wait a minute and try again."; + return "The TRON network rejected this. Nothing was sent."; +} diff --git a/packages/chains-tron/src/tx.ts b/packages/chains-tron/src/tx.ts new file mode 100644 index 00000000..eb294d4e --- /dev/null +++ b/packages/chains-tron/src/tx.ts @@ -0,0 +1,416 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { addressBytes, encodeAddress } from "./address.js"; +import { Fields, ProtoError, Writer, varintLength } from "./proto.js"; +import { bytesEqual, fromHex, hex, isHex, utf8 } from "./util.js"; + +/** + * TRON transactions, hand-written against java-tron's protobuf definitions + * (https://github.com/tronprotocol/java-tron/tree/develop/protocol/src/main/protos/core: Tron.proto `Transaction`, + * contract/balance_contract.proto, smart_contract.proto, asset_issue_contract.proto, witness_contract.proto, + * account_contract.proto, common.proto `ResourceCode`). + * + * Transaction.raw { bytes ref_block_bytes = 1; int64 ref_block_num = 3; bytes ref_block_hash = 4; int64 expiration = 8; + * repeated authority auths = 9; bytes data = 10; repeated Contract contract = 11; bytes scripts = 12; + * int64 timestamp = 14; int64 fee_limit = 18; } + * Contract { ContractType type = 1; google.protobuf.Any parameter = 2; bytes provider = 3; bytes ContractName = 4; + * int32 Permission_id = 5; } Any { string type_url = 1; bytes value = 2; } + * + * txID = sha256(raw_data bytes) (what TronWeb calls `txID`); the signature covers exactly that digest. + */ + +export const CONTRACT_TYPES = { + AccountCreateContract: 0, + TransferContract: 1, + TransferAssetContract: 2, + VoteAssetContract: 3, + VoteWitnessContract: 4, + WitnessCreateContract: 5, + AssetIssueContract: 6, + WitnessUpdateContract: 8, + ParticipateAssetIssueContract: 9, + AccountUpdateContract: 10, + FreezeBalanceContract: 11, + UnfreezeBalanceContract: 12, + WithdrawBalanceContract: 13, + UnfreezeAssetContract: 14, + UpdateAssetContract: 15, + ProposalCreateContract: 16, + ProposalApproveContract: 17, + ProposalDeleteContract: 18, + SetAccountIdContract: 19, + CustomContract: 20, + CreateSmartContract: 30, + TriggerSmartContract: 31, + GetContract: 32, + UpdateSettingContract: 33, + ExchangeCreateContract: 41, + ExchangeInjectContract: 42, + ExchangeWithdrawContract: 43, + ExchangeTransactionContract: 44, + UpdateEnergyLimitContract: 45, + AccountPermissionUpdateContract: 46, + ClearABIContract: 48, + UpdateBrokerageContract: 49, + ShieldedTransferContract: 51, + MarketSellAssetContract: 52, + MarketCancelOrderContract: 53, + FreezeBalanceV2Contract: 54, + UnfreezeBalanceV2Contract: 55, + WithdrawExpireUnfreezeContract: 56, + DelegateResourceContract: 57, + UnDelegateResourceContract: 58, + CancelAllUnfreezeV2Contract: 59, +} as const; + +export type ContractName = keyof typeof CONTRACT_TYPES; +const NAME_OF = new Map(Object.entries(CONTRACT_TYPES).map(([k, v]) => [v, k as ContractName])); +export const contractName = (type: number): ContractName | undefined => NAME_OF.get(type); +export const typeUrlOf = (name: ContractName) => `type.googleapis.com/protocol.${name}`; + +/** common.proto ResourceCode. */ +export const RESOURCES = ["BANDWIDTH", "ENERGY", "TRON_POWER"] as const; +export type Resource = (typeof RESOURCES)[number]; +export const resourceName = (code: bigint): Resource | null => RESOURCES[Number(code)] ?? null; + +export interface ContractEntry { + type: number; + name?: ContractName; + typeUrl: string; + value: Uint8Array; + permissionId: number; + /** provider / ContractName set (never set by TronWeb or java-tron's builders). */ + extra: boolean; +} + +export interface RawTx { + bytes: Uint8Array; + refBlockBytes: Uint8Array; + refBlockNum: bigint; + refBlockHash: Uint8Array; + expiration: bigint; + /** Transaction memo (raw.data). */ + data: Uint8Array; + contracts: ContractEntry[]; + timestamp: bigint; + feeLimit: bigint; + /** auths / scripts / unknown fields present: java-tron ignores them, but Clip doesn't explain them. */ + oddities: string[]; +} + +const RAW_FIELDS = [1, 3, 4, 8, 9, 10, 11, 12, 14, 18] as const; + +export function parseRaw(bytes: Uint8Array): RawTx { + const f = Fields.parse(bytes); + const oddities: string[] = []; + if (f.has(9)) oddities.push("auths"); + if (f.has(12)) oddities.push("scripts"); + for (const n of f.unknown(RAW_FIELDS)) oddities.push(`field ${n}`); + const contracts = f.repeated(11).map((c): ContractEntry => { + const cf = Fields.parse(c); + const any = Fields.parse(cf.bytes(2)); + const type = Number(cf.int(1)); + const name = contractName(type); + return { + type, + ...(name ? { name } : {}), + typeUrl: any.string(1), + value: any.bytes(2), + permissionId: Number(cf.int(5)), + extra: cf.has(3) || cf.has(4) || cf.unknown([1, 2, 3, 4, 5]).length > 0 || any.unknown([1, 2]).length > 0, + }; + }); + return { + bytes, + refBlockBytes: f.bytes(1), + refBlockNum: f.int(3), + refBlockHash: f.bytes(4), + expiration: f.int(8), + data: f.bytes(10), + contracts, + timestamp: f.int(14), + feeLimit: f.int(18), + oddities, + }; +} + +export const txIdOf = (raw: Uint8Array): Uint8Array => sha256(raw); + +/** Bytes of bandwidth a signed transaction uses (java-tron BandwidthProcessor: size without `ret`, + 64 per contract). */ +export function bandwidthBytes(rawLength: number, signatures = 1, contracts = 1): number { + return 1 + varintLength(rawLength) + rawLength + signatures * 67 + contracts * 64; +} + +/* ------------------------------------------------------------------ contract parameters */ + +const owner1 = (f: Fields) => f.bytes(1); + +export interface TransferParams { owner: Uint8Array; to: Uint8Array; amount: bigint } +export interface TransferAssetParams { assetName: string; owner: Uint8Array; to: Uint8Array; amount: bigint } +export interface TriggerParams { owner: Uint8Array; contract: Uint8Array; callValue: bigint; data: Uint8Array; callTokenValue: bigint; tokenId: bigint } +export interface ResourceParams { owner: Uint8Array; amount: bigint; resource: bigint } +export interface DelegateParams { owner: Uint8Array; resource: bigint; balance: bigint; receiver: Uint8Array; lock: boolean; lockPeriod: bigint } +export interface VoteParams { owner: Uint8Array; votes: { address: Uint8Array; count: bigint }[] } +export interface PermissionKey { address: Uint8Array; weight: bigint } +export interface PermissionParams { type: bigint; id: bigint; name: string; threshold: bigint; operations: Uint8Array; keys: PermissionKey[] } +export interface PermissionUpdateParams { owner: Uint8Array; ownerPermission: PermissionParams | null; witness: PermissionParams | null; actives: PermissionParams[] } + +export const parseTransfer = (v: Uint8Array): TransferParams => { + const f = Fields.parse(v); + return { owner: owner1(f), to: f.bytes(2), amount: f.int(3) }; +}; +export const parseTransferAsset = (v: Uint8Array): TransferAssetParams => { + const f = Fields.parse(v); + return { assetName: f.string(1), owner: f.bytes(2), to: f.bytes(3), amount: f.int(4) }; +}; +export const parseTrigger = (v: Uint8Array): TriggerParams => { + const f = Fields.parse(v); + return { owner: owner1(f), contract: f.bytes(2), callValue: f.int(3), data: f.bytes(4), callTokenValue: f.int(5), tokenId: f.int(6) }; +}; +/** FreezeBalanceV2 (frozen_balance = 2, resource = 3) and UnfreezeBalanceV2 (unfreeze_balance = 2, resource = 3). */ +export const parseResource = (v: Uint8Array): ResourceParams => { + const f = Fields.parse(v); + return { owner: owner1(f), amount: f.int(2), resource: f.int(3) }; +}; +export const parseDelegate = (v: Uint8Array): DelegateParams => { + const f = Fields.parse(v); + return { owner: owner1(f), resource: f.int(2), balance: f.int(3), receiver: f.bytes(4), lock: f.bool(5), lockPeriod: f.int(6) }; +}; +export const parseVote = (v: Uint8Array): VoteParams => { + const f = Fields.parse(v); + return { + owner: owner1(f), + votes: f.repeated(2).map((x) => { + const vf = Fields.parse(x); + return { address: vf.bytes(1), count: vf.int(2) }; + }), + }; +}; +export const parseOwnerOnly = (v: Uint8Array): { owner: Uint8Array } => ({ owner: owner1(Fields.parse(v)) }); + +function parsePermission(b: Uint8Array): PermissionParams { + const f = Fields.parse(b); + return { + type: f.int(1), + id: f.int(2), + name: f.string(3), + threshold: f.int(4), + operations: f.bytes(6), + keys: f.repeated(7).map((k) => { + const kf = Fields.parse(k); + return { address: kf.bytes(1), weight: kf.int(2) }; + }), + }; +} + +export const parsePermissionUpdate = (v: Uint8Array): PermissionUpdateParams => { + const f = Fields.parse(v); + return { + owner: owner1(f), + ownerPermission: f.has(2) ? parsePermission(f.bytes(2)) : null, + witness: f.has(3) ? parsePermission(f.bytes(3)) : null, + actives: f.repeated(4).map(parsePermission), + }; +}; + +/** The account a contract acts for (its owner_address), or null for a type Clip doesn't explain. */ +export function ownerOf(c: ContractEntry): Uint8Array | null { + if (!c.name || !VALUE_FIELDS[c.name]) return null; + return Fields.parse(c.value).bytes(VALUE_FIELDS[c.name]!.owner_address![0]); +} + +/* ------------------------------------------------------------------ building */ + +export interface RefBlock { + /** Block number. */ + number: bigint; + /** 32-byte block id (hex). */ + blockId: string; + /** Block timestamp (ms). */ + timestamp: bigint; +} + +export interface BuildSpec { + ref: RefBlock; + expiration: bigint; + timestamp: bigint; + feeLimit?: bigint; + memo?: string; + name: ContractName; + value: Uint8Array; +} + +/** ref_block_bytes = bytes 6..8 of the 8-byte big-endian block number; ref_block_hash = bytes 8..16 of the block id. */ +export function refBlockFields(ref: RefBlock): { refBlockBytes: Uint8Array; refBlockHash: Uint8Array } { + const n = BigInt.asUintN(64, ref.number); + const num = new Uint8Array(8); + for (let i = 0; i < 8; i++) num[7 - i] = Number((n >> BigInt(8 * i)) & 0xffn); + return { refBlockBytes: num.subarray(6, 8), refBlockHash: fromHex(ref.blockId).subarray(8, 16) }; +} + +export function encodeRaw(spec: BuildSpec): Uint8Array { + const { refBlockBytes, refBlockHash } = refBlockFields(spec.ref); + const any = new Writer().string(1, typeUrlOf(spec.name)).bytes(2, spec.value).finish(); + const contract = new Writer().int(1, CONTRACT_TYPES[spec.name]).message(2, any).finish(); + return new Writer() + .bytes(1, refBlockBytes) + .bytes(4, refBlockHash) + .int(8, spec.expiration) + .bytes(10, spec.memo ? utf8(spec.memo) : undefined) + .message(11, contract) + .int(14, spec.timestamp) + .int(18, spec.feeLimit ?? 0n) + .finish(); +} + +export const encodeTransfer = (owner: Uint8Array, to: Uint8Array, amount: bigint): Uint8Array => new Writer().bytes(1, owner).bytes(2, to).int(3, amount).finish(); +export const encodeTrigger = (owner: Uint8Array, contract: Uint8Array, data: Uint8Array, callValue = 0n): Uint8Array => + new Writer().bytes(1, owner).bytes(2, contract).int(3, callValue).bytes(4, data).finish(); + +/* ------------------------------------------------------------------ TronWeb transaction objects */ + +/** A TronWeb transaction object (what `tronWeb.transactionBuilder.*` and /wallet/createtransaction return). */ +export interface TronWebTx { + txID?: string; + raw_data?: Record; + raw_data_hex: string; + visible?: boolean; + signature?: string[]; +} + +export class TxShapeError extends Error {} + +/** + * The transaction a dapp handed over, from the Reown params ({ transaction }, or the legacy nested + * { transaction: { transaction } }), or a TronWeb object directly. + */ +export function txObjectOf(x: unknown): TronWebTx { + let t = x as Record | null; + for (let depth = 0; depth < 3 && t && typeof t === "object" && !("raw_data_hex" in t) && t.transaction && typeof t.transaction === "object"; depth++) { + t = t.transaction as Record; + } + if (!t || typeof t !== "object" || !isHex(t.raw_data_hex) || !(t.raw_data_hex as string).replace(/^0x/, "").length) { + throw new TxShapeError("Expected a TRON transaction with raw_data_hex."); + } + if (t.txID !== undefined && !(typeof t.txID === "string" && /^(0x)?[0-9a-f]{64}$/i.test(t.txID))) throw new TxShapeError("txID isn't a 32-byte hash."); + if (t.signature !== undefined && !(Array.isArray(t.signature) && t.signature.every((s) => isHex(s)))) throw new TxShapeError("signature isn't a list of hex strings."); + return t as unknown as TronWebTx; +} + +/** + * Checks the JSON a dapp sent against the bytes that get signed. Only the hex is trusted (it's what the txID covers); + * a JSON field that says something else means the request is lying about itself. Returns the mismatching fields. + */ +export function jsonMismatches(json: Record | undefined, raw: RawTx): string[] { + if (!json) return []; + const bad: string[] = []; + const eqHex = (k: string, want: Uint8Array) => { + const v = json[k]; + if (v === undefined) return; + if (!isHex(v) || !bytesEqual(fromHex(v), want)) bad.push(k); + }; + const eqInt = (obj: Record, k: string, want: bigint, label = k) => { + const v = obj[k]; + if (v === undefined || v === null) { + if (want !== 0n && k in obj) bad.push(label); + return; + } + if (typeof v === "boolean" ? (v ? 1n : 0n) !== want : !(typeof v === "number" || typeof v === "string") || !/^-?\d+$/.test(String(v)) || BigInt(String(v)) !== want) bad.push(label); + }; + eqHex("ref_block_bytes", raw.refBlockBytes); + eqHex("ref_block_hash", raw.refBlockHash); + eqHex("data", raw.data); + eqInt(json, "expiration", raw.expiration); + eqInt(json, "timestamp", raw.timestamp); + eqInt(json, "fee_limit", raw.feeLimit); + eqInt(json, "ref_block_num", raw.refBlockNum); + const list = json.contract; + if (list === undefined) return bad; + if (!Array.isArray(list) || list.length !== raw.contracts.length) return [...bad, "contract"]; + list.forEach((c, i) => { + const want = raw.contracts[i]!; + const cj = (c ?? {}) as Record; + if (cj.type !== undefined && cj.type !== want.name && cj.type !== want.type) bad.push(`contract[${i}].type`); + eqInt(cj, "Permission_id", BigInt(want.permissionId), `contract[${i}].Permission_id`); + const p = (cj.parameter ?? {}) as Record; + if (p.type_url !== undefined && p.type_url !== want.typeUrl) bad.push(`contract[${i}].type_url`); + const value = p.value; + if (value === undefined || !want.name) return; + if (!value || typeof value !== "object") { + bad.push(`contract[${i}].value`); + return; + } + for (const k of valueMismatches(want.name, want.value, value as Record)) bad.push(`contract[${i}].${k}`); + }); + return bad; +} + +/** Field name → (field number, kind) for the contract values Clip explains. */ +const VALUE_FIELDS: Partial>> = { + TransferContract: { owner_address: [1, "addr"], to_address: [2, "addr"], amount: [3, "int"] }, + TransferAssetContract: { asset_name: [1, "asset"], owner_address: [2, "addr"], to_address: [3, "addr"], amount: [4, "int"] }, + TriggerSmartContract: { owner_address: [1, "addr"], contract_address: [2, "addr"], call_value: [3, "int"], data: [4, "bytes"], call_token_value: [5, "int"], token_id: [6, "int"] }, + FreezeBalanceV2Contract: { owner_address: [1, "addr"], frozen_balance: [2, "int"], resource: [3, "resource"] }, + UnfreezeBalanceV2Contract: { owner_address: [1, "addr"], unfreeze_balance: [2, "int"], resource: [3, "resource"] }, + WithdrawExpireUnfreezeContract: { owner_address: [1, "addr"] }, + CancelAllUnfreezeV2Contract: { owner_address: [1, "addr"] }, + WithdrawBalanceContract: { owner_address: [1, "addr"] }, + DelegateResourceContract: { owner_address: [1, "addr"], resource: [2, "resource"], balance: [3, "int"], receiver_address: [4, "addr"], lock: [5, "bool"], lock_period: [6, "int"] }, + UnDelegateResourceContract: { owner_address: [1, "addr"], resource: [2, "resource"], balance: [3, "int"], receiver_address: [4, "addr"] }, + VoteWitnessContract: { owner_address: [1, "addr"] }, + AccountPermissionUpdateContract: { owner_address: [1, "addr"] }, +}; + +function valueMismatches(name: ContractName, value: Uint8Array, json: Record): string[] { + const spec = VALUE_FIELDS[name]; + if (!spec) return []; + const f = Fields.parse(value); + const bad: string[] = []; + for (const [k, [no, kind]] of Object.entries(spec)) { + const v = json[k]; + if (v === undefined || v === null) { + // Absent in JSON = proto3 default. Only a non-default value in the bytes is a mismatch. + const present = kind === "addr" || kind === "bytes" || kind === "asset" ? f.bytes(no).length > 0 : f.int(no) !== 0n; + if (present && k in json) bad.push(k); + continue; + } + switch (kind) { + case "addr": { + const b = typeof v === "string" ? addressBytes(v) : null; + if (!b || !bytesEqual(b, f.bytes(no))) bad.push(k); + break; + } + case "bytes": + if (!isHex(v) || !bytesEqual(fromHex(v), f.bytes(no))) bad.push(k); + break; + case "asset": { + // visible=true: the token id as text; visible=false: its hex. + const want = f.bytes(no); + if (typeof v !== "string" || !(bytesEqual(utf8(v), want) || (isHex(v) && bytesEqual(fromHex(v), want)))) bad.push(k); + break; + } + case "resource": { + const want = f.int(no); + if (!(v === resourceName(want) || v === Number(want))) bad.push(k); + break; + } + case "bool": + if (typeof v !== "boolean" || v !== f.bool(no)) bad.push(k); + break; + case "int": + if (!(typeof v === "number" || typeof v === "string") || !/^-?\d+$/.test(String(v)) || BigInt(String(v)) !== f.int(no)) bad.push(k); + break; + } + } + return bad; +} + +/** "T…" for 21 address bytes, or the hex if they aren't a TRON address. */ +export function showAddress(b: Uint8Array): string { + try { + return encodeAddress(b); + } catch { + return hex(b); + } +} + +export { ProtoError }; diff --git a/packages/chains-tron/src/util.ts b/packages/chains-tron/src/util.ts new file mode 100644 index 00000000..530c9df1 --- /dev/null +++ b/packages/chains-tron/src/util.ts @@ -0,0 +1,85 @@ +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.replace(/^0x/i, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export function isHex(h: unknown): h is string { + return typeof h === "string" && /^(0x)?([0-9a-f]{2})*$/i.test(h); +} + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function bytesEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean { + if (!a || !b || a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +export const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s); + +/** Printable UTF-8 text, or null. */ +export function textOf(bytes: Uint8Array): string | null { + if (!bytes.length) return null; + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + return /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s) ? null : s; + } catch { + return null; + } +} + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +export function short(address: string): string { + return address.length > 12 ? `${address.slice(0, 4)}…${address.slice(-4)}` : address; +} + +export function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +/** Number / bigint / decimal string from node JSON → bigint (anything else → 0). */ +export function big(x: unknown): bigint { + if (typeof x === "bigint") return x; + if (typeof x === "number" && Number.isSafeInteger(x)) return BigInt(x); + if (typeof x === "string" && /^-?\d+$/.test(x)) return BigInt(x); + return 0n; +} + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/packages/chains-tron/test/fixtures.ts b/packages/chains-tron/test/fixtures.ts new file mode 100644 index 00000000..3724adfd --- /dev/null +++ b/packages/chains-tron/test/fixtures.ts @@ -0,0 +1,255 @@ +/** + * Unsigned transactions captured from the Nile full node (POST /wallet/createtransaction, /wallet/triggersmartcontract, + * /wallet/freezebalancev2, /wallet/unfreezebalancev2, /wallet/delegateresource, /wallet/votewitnessaccount, + * /wallet/accountpermissionupdate) for the public "abandon … about" account TUEZ…GWYdH. Nothing here was signed or sent. + */ +export const NILE_TX = { + "transfer": { + "visible": true, + "txID": "a758dc1787a24ecfca5f9622e7ce36c663ce7328f6aedf166bae5bdc181e0a6d", + "raw_data": { + "ref_block_bytes": "4f15", + "ref_block_hash": "d5efcd3acb235c24", + "expiration": 1791285828000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "to_address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", + "amount": 1500000 + }, + "type_url": "type.googleapis.com/protocol.TransferContract" + }, + "type": "TransferContract" + } + ], + "timestamp": 1791285769056 + }, + "raw_data_hex": "0a024f152208d5efcd3acb235c2440a0b3d28791345a67080112630a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412320a1541c8599111f29c1e1e061265b4af93ea1f274ad78a121541b6e708a39781c96bd399c7657780ff9fe9f052a818e0c65b70e0e6ce879134" + }, + "trc20": { + "visible": true, + "txID": "3d0ae4ea180d71012c8b995c8af54d7db8f684df3fb6167d37765bb89eec532b", + "raw_data": { + "ref_block_bytes": "4f15", + "ref_block_hash": "d5efcd3acb235c24", + "expiration": 1791285828000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "contract_address": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + "data": "a9059cbb000000000000000000000000b6e708a39781c96bd399c7657780ff9fe9f052a800000000000000000000000000000000000000000000000000000000000f4240" + }, + "type_url": "type.googleapis.com/protocol.TriggerSmartContract" + }, + "type": "TriggerSmartContract" + } + ], + "timestamp": 1791285769893, + "fee_limit": 30000000 + }, + "raw_data_hex": "0a024f152208d5efcd3acb235c2440a0b3d28791345aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541c8599111f29c1e1e061265b4af93ea1f274ad78a121541eca9bc828a3005b9a3b909f2cc5c2a54794de05f2244a9059cbb000000000000000000000000b6e708a39781c96bd399c7657780ff9fe9f052a800000000000000000000000000000000000000000000000000000000000f424070a5edce87913490018087a70e" + }, + "approve": { + "visible": false, + "txID": "1d559e7e2710a0dc78fc22dc7220332a7295b74ae4c794cf16a0f14c82f0162e", + "raw_data": { + "ref_block_bytes": "4f1a", + "ref_block_hash": "e4f2a892a1e50164", + "expiration": 1791285843000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "41c8599111f29c1e1e061265b4af93ea1f274ad78a", + "contract_address": "41eca9bc828a3005b9a3b909f2cc5c2a54794de05f", + "data": "095ea7b3000000000000000000000000b6e708a39781c96bd399c7657780ff9fe9f052a8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "type_url": "type.googleapis.com/protocol.TriggerSmartContract" + }, + "type": "TriggerSmartContract" + } + ], + "timestamp": 1791285783222, + "fee_limit": 30000000 + }, + "raw_data_hex": "0a024f1a2208e4f2a892a1e5016440b8a8d38791345aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541c8599111f29c1e1e061265b4af93ea1f274ad78a121541eca9bc828a3005b9a3b909f2cc5c2a54794de05f2244095ea7b3000000000000000000000000b6e708a39781c96bd399c7657780ff9fe9f052a8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff70b6d5cf87913490018087a70e" + }, + "freeze": { + "visible": true, + "txID": "8a2b433a55ce9ea5dae1893d42d1b5001887ab268d2a22ea23accafbbe05a5de", + "raw_data": { + "ref_block_bytes": "4f16", + "ref_block_hash": "4f8a4770d0ae6bef", + "expiration": 1791285831000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "frozen_balance": 10000000, + "resource": "ENERGY" + }, + "type_url": "type.googleapis.com/protocol.FreezeBalanceV2Contract" + }, + "type": "FreezeBalanceV2Contract" + } + ], + "timestamp": 1791285771645 + }, + "raw_data_hex": "0a024f1622084f8a4770d0ae6bef40d8cad28791345a5a083612560a34747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e467265657a6542616c616e63655632436f6e7472616374121e0a1541c8599111f29c1e1e061265b4af93ea1f274ad78a1080ade204180170fdface879134" + }, + "unfreeze": { + "visible": true, + "txID": "ed52480ceebefe934cde1a1f4baec49fa10c15a808a97f6218d9b1a13c52281e", + "raw_data": { + "ref_block_bytes": "4f1e", + "ref_block_hash": "fa755f2179e7e510", + "expiration": 1791285855000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "unfreeze_balance": 1000000, + "resource": "ENERGY" + }, + "type_url": "type.googleapis.com/protocol.UnfreezeBalanceV2Contract" + }, + "type": "UnfreezeBalanceV2Contract" + } + ], + "timestamp": 1791285795990 + }, + "raw_data_hex": "0a024f1e2208fa755f2179e7e510409886d48791345a5b083712570a36747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e556e667265657a6542616c616e63655632436f6e7472616374121d0a1541c8599111f29c1e1e061265b4af93ea1f274ad78a10c0843d18017096b9d0879134" + }, + "delegate": { + "visible": true, + "txID": "beb147f9e5d599be38715bc855f16ece543542ac3a13e5ef5a9ffe1c45d1103e", + "raw_data": { + "ref_block_bytes": "4f16", + "ref_block_hash": "4f8a4770d0ae6bef", + "expiration": 1791285831000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "resource": "ENERGY", + "balance": 5000000, + "receiver_address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", + "lock": true, + "lock_period": 28800 + }, + "type_url": "type.googleapis.com/protocol.DelegateResourceContract" + }, + "type": "DelegateResourceContract" + } + ], + "timestamp": 1791285772380 + }, + "raw_data_hex": "0a024f1622084f8a4770d0ae6bef40d8cad28791345a78083912740a35747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e44656c65676174655265736f75726365436f6e7472616374123b0a1541c8599111f29c1e1e061265b4af93ea1f274ad78a100118c096b102221541b6e708a39781c96bd399c7657780ff9fe9f052a828013080e10170dc80cf879134" + }, + "vote": { + "visible": false, + "txID": "76b834c9f1b3c66d5ca39648dd0d823e0f8eb6df70bba1c7d469dd2dae5cf7c2", + "raw_data": { + "ref_block_bytes": "4f1a", + "ref_block_hash": "e4f2a892a1e50164", + "expiration": 1791285843000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "41c8599111f29c1e1e061265b4af93ea1f274ad78a", + "votes": [ + { + "vote_address": "41608e7e1c6f6dcc1679ea512503e41ca0254e0948", + "vote_count": 3 + } + ] + }, + "type_url": "type.googleapis.com/protocol.VoteWitnessContract" + }, + "type": "VoteWitnessContract" + } + ], + "timestamp": 1791285784060 + }, + "raw_data_hex": "0a024f1a2208e4f2a892a1e5016440b8a8d38791345a6a080412660a30747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e566f74655769746e657373436f6e747261637412320a1541c8599111f29c1e1e061265b4af93ea1f274ad78a12190a1541608e7e1c6f6dcc1679ea512503e41ca0254e0948100370fcdbcf879134" + }, + "perm": { + "visible": true, + "txID": "124b078aa8005371b7bca6f7969b2b97e43befd61adec8868edcb72b6fcb8333", + "raw_data": { + "ref_block_bytes": "4f1d", + "ref_block_hash": "6702af6ca3cd69f3", + "expiration": 1791285852000, + "contract": [ + { + "parameter": { + "value": { + "owner_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "owner": { + "permission_name": "owner", + "threshold": 1, + "keys": [ + { + "address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", + "weight": 1 + } + ] + }, + "actives": [ + { + "type": "Active", + "permission_name": "active0", + "threshold": 1, + "operations": "7fff1fc0033e0000000000000000000000000000000000000000000000000000", + "keys": [ + { + "address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", + "weight": 1 + } + ] + } + ] + }, + "type_url": "type.googleapis.com/protocol.AccountPermissionUpdateContract" + }, + "type": "AccountPermissionUpdateContract" + } + ], + "timestamp": 1791285794250 + }, + "raw_data_hex": "0a024f1d22086702af6ca3cd69f340e0eed38791345acf01082e12ca010a3c747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e4163636f756e745065726d697373696f6e557064617465436f6e74726163741289010a1541c8599111f29c1e1e061265b4af93ea1f274ad78a12241a056f776e657220013a190a1541b6e708a39781c96bd399c7657780ff9fe9f052a81001224a08021a0761637469766530200132207fff1fc0033e00000000000000000000000000000000000000000000000000003a190a1541b6e708a39781c96bd399c7657780ff9fe9f052a8100170caabd0879134" + }, + "memo": { + "visible": true, + "txID": "75eeed703e3e3c43213c4b2467b08236ff6786642a2f157401248eb514fcebf7", + "raw_data": { + "ref_block_bytes": "4f21", + "ref_block_hash": "da8c3d0d97723337", + "expiration": 1791285864000, + "data": "68656c6c6f", + "contract": [ + { + "parameter": { + "value": { + "owner_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "to_address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", + "amount": 1 + }, + "type_url": "type.googleapis.com/protocol.TransferContract" + }, + "type": "TransferContract" + } + ], + "timestamp": 1791285805958 + }, + "raw_data_hex": "0a024f212208da8c3d0d9772333740c0ccd4879134520568656c6c6f5a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541c8599111f29c1e1e061265b4af93ea1f274ad78a121541b6e708a39781c96bd399c7657780ff9fe9f052a81801708687d1879134" + } +} as const; diff --git a/packages/chains-tron/test/helpers.ts b/packages/chains-tron/test/helpers.ts new file mode 100644 index 00000000..87aae882 --- /dev/null +++ b/packages/chains-tron/test/helpers.ts @@ -0,0 +1,81 @@ +import type { Account, ChainContext, DappRequest, Network, SignablePayload, Signature } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { TRON_NILE } from "../src/index.js"; +import { fromHex } from "../src/util.js"; +import { ME, ME_PUB } from "./signatures.js"; + +/** Vault stand-in: answers with a precomputed signature that verifies for the payload (verification only). */ +export function fixtureSigner(signatures: readonly string[], pub = ME_PUB) { + const key = fromHex(pub); + const sigs = signatures.map((s) => fromHex(s)); + return { + sign(p: SignablePayload): Signature { + const s = sigs.find((x) => secp256k1.verify(x.subarray(0, 64), p.bytes, key, { prehash: false })); + if (!s) throw new Error("no fixture signature for this payload"); + return { scheme: "ecdsa-secp256k1", bytes: s.subarray(0, 64), recovery: s[64]! - 27, publicKey: pub }; + }, + }; +} + +export function makeAccount(address = ME, publicKey = ME_PUB): Account { + return { id: "tron:0", family: "tron", index: 0, curve: "secp256k1", derivationPath: "m/44'/195'/0'/0/0", publicKey, address }; +} + +type Handler = unknown | ((body: Record) => unknown); + +/** Mock java-tron HTTP API: path → JSON (or a function of the POSTed JSON). Unknown paths answer 404. */ +export function mockTron(routes: Record) { + const calls: { url: string; path: string; body: Record }[] = []; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const path = new URL(url).pathname; + const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; + calls.push({ url, path, body }); + if (!(path in routes)) return new Response("not found", { status: 404 }); + const h = routes[path]; + const v = typeof h === "function" ? (h as (b: Record) => unknown)(body) : h; + return new Response(JSON.stringify(v), { status: 200 }); + }) as typeof fetch; + return { fetch: f, calls }; +} + +export function ctxFor(fetchImpl: typeof fetch, network: Network = TRON_NILE, account: Account = makeAccount()): ChainContext { + return { network, account, fetch: fetchImpl }; +} + +export function req(method: string, params: unknown, origin = "https://app.example", networkId = TRON_NILE.id): DappRequest { + return { id: Math.random().toString(36).slice(2), origin, via: "injected", family: "tron", networkId, method, params }; +} + +/** A Nile-like account the key controls. */ +export const meAccount = (balance = 57_200_892_190) => ({ + address: ME, + balance, + create_time: 1626606534000, + owner_permission: { permission_name: "owner", threshold: 1, keys: [{ address: ME, weight: 1 }] }, + active_permission: [{ type: "Active", id: 2, permission_name: "active", threshold: 1, operations: "7fff1fc0033efb0f000000000000000000000000000000000000000000000000", keys: [{ address: ME, weight: 1 }] }], +}); + +/** Chain parameters as Nile and mainnet report them (POST /wallet/getchainparameters, 2026-10). */ +export const chainParameters = { + chainParameter: [ + { key: "getTransactionFee", value: 1000 }, + { key: "getEnergyFee", value: 100 }, + { key: "getCreateAccountFee", value: 100000 }, + { key: "getCreateNewAccountFeeInSystemContract", value: 1000000 }, + { key: "getMemoFee", value: 1000000 }, + { key: "getMaxFeeLimit", value: 15000000000 }, + { key: "getUnfreezeDelayDays", value: 14 }, + ], +}; + +/** /wallet/getblockbynum that answers ids whose bytes 8..16 are `refHash` (so any ref block checks out), or a fixed id. */ +export const blockByNum = (refHash: string) => (b: Record) => ({ + blockID: `${BigInt(b.num as number).toString(16).padStart(16, "0")}${refHash}${"00".repeat(16)}`, +}); + +/** A head block whose low 16 bits are near `refBytes`. */ +export const nowBlock = (number = 71585237, blockId = "0000000004444dd56ee3c6be8649ac0a384d36e2fb9f1724e30d40b7d8faf835", timestamp = 1791284757000) => ({ + blockID: blockId, + block_header: { raw_data: { number, timestamp } }, +}); diff --git a/packages/chains-tron/test/signatures.ts b/packages/chains-tron/test/signatures.ts new file mode 100644 index 00000000..35db38ea --- /dev/null +++ b/packages/chains-tron/test/signatures.ts @@ -0,0 +1,22 @@ +/** + * Signature fixtures, computed once OFFLINE (outside the repo) with the vault's own signer from the public BIP-39 test + * vector "abandon abandon … about" (empty passphrase), secp256k1 at m/44'/195'/0'/0/0 (TronLink / TronWeb.fromMnemonic): + * address TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH. Each is r ‖ s ‖ v (v = 27 + recovery), hex. Only public data lives here. + * + * - builtTrx: the raw transaction buildTransfer makes for 1.5 TRX me → bob on Nile, reference block 71585237 + * (0000000004444dd5…d8faf835, time 1791284757000), now = 1791284760000, expiration = block time + 10 min. + * - dappTransfer / dappTrc20: NILE_TX.transfer / NILE_TX.trc20 (txID). + * - message: TronWeb signMessageV2("Sign in to app.example"). + */ +export const ME = "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH"; +export const ME_PUB = "03ff21f8e64d3a3c0198edfbb7afdc79be959432e92e2f8a1984bb436a414b8edc"; +/** m/44'/195'/0'/0/1 of the same phrase. */ +export const BOB = "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK"; +export const BOB_PUB = "0209b9854ad6e016c5d72aee08f763821bd5e32489c12bcc078a935a14f29dbed6"; +export const SIG = { + "builtRaw": "0a024dd522086ee3c6be8649ac0a40c8d3b58791345a67080112630a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412320a1541c8599111f29c1e1e061265b4af93ea1f274ad78a121541b6e708a39781c96bd399c7657780ff9fe9f052a818e0c65b70c09b91879134", + "builtTrx": "16761d2008808f38988a645eed3e750728e7c3b97cdbc6dfb5035d275bcfd2ff1b3501a93e35a8c69caa322197502afe2935feabdfa674851256592d50b0ffed1c", + "dappTransfer": "9485a2c63119a6cf857a44c7deb8e67a53f886f14ca68d025b9bcb8e6119afcb1997334fa58fc7b998bc2dbc602d28101f5e0daabb9cf4ef78d6a551f83ff8011b", + "dappTrc20": "a3c0b03a398b4c258a0d84602ea548d8df92b789ec942f0eb5226d88684e32a63799d382aa7cc5e9c5c03fe4342260700bf7be2745d31600fc15f8c26aba4d261c", + "message": "fe7da50500197d661fb366ffe53bab38460868ebfb02e2f002507659f990aaa02b28a20d6201b014a6276f9b54689401f6c5dbd114753d056ec22bc6d67391b61c" +} as const; diff --git a/packages/chains-tron/test/tron.test.ts b/packages/chains-tron/test/tron.test.ts new file mode 100644 index 00000000..4450d4e4 --- /dev/null +++ b/packages/chains-tron/test/tron.test.ts @@ -0,0 +1,489 @@ +import { ClipError, type DappRequest, WALLET_ORIGIN } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { TronWeb, Trx, utils as twUtils } from "tronweb"; +import { describe, expect, it } from "vitest"; +import { + TRON_MAINNET, + TRON_METHODS, + TRON_NETS, + TRON_NETWORKS, + TRON_NILE, + TRON_SHASTA, + addressBytes, + controls, + createTronModule, + encodeRaw, + fromChainId, + jsonMismatches, + messageHash, + parseRaw, + toBase58, + txIdOf, + usdtAsset, +} from "../src/index.js"; +import { encodeTransfer, encodeTrigger } from "../src/tx.js"; +import { fromHex, hex, utf8 } from "../src/util.js"; +import { NILE_TX } from "./fixtures.js"; +import { blockByNum, chainParameters, ctxFor, fixtureSigner, meAccount, mockTron, nowBlock, req } from "./helpers.js"; +import { BOB, BOB_PUB, ME, ME_PUB, SIG } from "./signatures.js"; + +const NILE_USDT = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; +const DAPP_NOW = 1_791_285_780_000; // after the fixtures were made, before they expire +const BUILD_NOW = 1_791_284_760_000; +const signer = fixtureSigner([SIG.builtTrx, SIG.dappTransfer, SIG.dappTrc20, SIG.message]); +const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`; +const tw = { address: (TronWeb as unknown as { address: unknown }).address, utils: twUtils, Trx } as unknown as { + address: { fromHex(h: string): string; toHex(a: string): string }; + utils: { + crypto: { computeAddress(pub: number[] | Uint8Array): number[]; getBase58CheckAddress(b: number[]): string; ecRecover(data: string, sig: string): string }; + transaction: { txCheck(tx: unknown): boolean; txJsonToPb(tx: unknown): unknown; txPbToRawDataHex(pb: unknown): string }; + message: { hashMessage(m: string | Uint8Array): string }; + }; + Trx: { verifyMessageV2(message: string, signature: string): string }; +}; + +/** A java-tron mock with this account, Bob, chain parameters, a head block and blocks matching `refHash`. */ +function setup(over: Record = {}, refHash: string = NILE_TX.transfer.raw_data.ref_block_hash, now = DAPP_NOW) { + const mock = mockTron({ + "/wallet/getaccount": (b: Record) => (b.address === ME ? meAccount() : b.address === BOB ? { address: BOB, balance: 1 } : {}), + "/wallet/getaccountresource": { freeNetLimit: 600, freeNetUsed: 600 }, + "/wallet/getchainparameters": chainParameters, + "/wallet/getnowblock": nowBlock(), + "/wallet/getblockbynum": blockByNum(refHash), + ...over, + }); + return { m: createTronModule({ now: () => now, sleep: async () => {}, confirmPollMs: 0 }), ctx: ctxFor(mock.fetch), calls: mock.calls }; +} + +const signTx = (tx: unknown, extra: Record = {}, origin?: string): DappRequest => req(TRON_METHODS.signTransaction, { address: ME, transaction: tx, ...extra }, origin); +const rawOf = (r: DappRequest) => (r.params as { transaction: { raw_data_hex: string } }).transaction.raw_data_hex; + +describe("accounts and addresses", () => { + const m = createTronModule(); + it("derives like TronLink / TronWeb.fromMnemonic and encodes T… addresses", () => { + expect(m.family).toBe("tron"); + expect(m.curve).toBe("secp256k1"); + expect(m.derivationPath(0)).toBe("m/44'/195'/0'/0/0"); + expect(m.derivationPath(3)).toBe("m/44'/195'/0'/0/3"); + expect(m.addressFromPublicKey(fromHex(ME_PUB), TRON_NILE)).toBe(ME); + expect(m.addressFromPublicKey(fromHex(BOB_PUB), TRON_MAINNET)).toBe(BOB); + }); + + it("matches TronWeb's own address maths", () => { + // TronWeb computeAddress takes the 65-byte uncompressed key. + for (const [pub, addr] of [[ME_PUB, ME], [BOB_PUB, BOB]] as const) { + const uncompressed = secp256k1.Point.fromBytes(fromHex(pub)).toBytes(false); + expect(tw.utils.crypto.getBase58CheckAddress(tw.utils.crypto.computeAddress(uncompressed))).toBe(addr); + expect(tw.address.fromHex(hex(addressBytes(addr)!))).toBe(addr); + } + }); + + it("checks base58check addresses", () => { + expect(m.isAddress(ME)).toBe(true); + expect(m.isAddress(` ${BOB} `)).toBe(true); + expect(m.isAddress(`${ME.slice(0, -1)}${ME.endsWith("H") ? "J" : "H"}`)).toBe(false); // checksum + expect(m.isAddress("41c8599111f29c1e1e061265b4af93ea1f274ad78a")).toBe(false); // hex is for dapps, not for typing + expect(m.isAddress("0xc8599111f29c1e1e061265b4af93ea1f274ad78a")).toBe(false); + expect(m.isAddress("T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb")).toBe(true); // the zero address (black hole) + expect(toBase58("41c8599111f29c1e1e061265b4af93ea1f274ad78a")).toBe(ME); + expect(m.networksForAddress(ME, [...TRON_NETWORKS, { ...TRON_NILE, id: "eip155:1", family: "evm" }]).map((n) => n.id)).toEqual(TRON_NETWORKS.map((n) => n.id)); + expect(m.networksForAddress("nope", TRON_NETWORKS)).toEqual([]); + }); +}); + +describe("networks", () => { + it("uses the WalletConnect / Reown ids and accepts other spellings", () => { + expect(TRON_NETWORKS.map((n) => n.id)).toEqual(["tron:0xcd8690dc", "tron:0x94a9059e", "tron:0x2b6653dc"]); + expect(TRON_NILE.testnet && TRON_SHASTA.testnet && !TRON_MAINNET.testnet).toBe(true); + expect(fromChainId("0x2b6653dc")).toBe(TRON_MAINNET.id); + expect(fromChainId("tron:728126428")).toBe(TRON_MAINNET.id); // ChainAgnostic draft (decimal) + expect(fromChainId(3448148188)).toBe(TRON_NILE.id); + expect(fromChainId(TRON_NETS.shasta.genesisBlockId)).toBe(TRON_SHASTA.id); + expect(fromChainId("eip155:1")).toBeNull(); + for (const n of Object.values(TRON_NETS)) expect(n.genesisBlockId.endsWith(n.chainId.slice(2))).toBe(true); + }); + + it("keys Tether USDT as usdt on mainnet only", () => { + expect(usdtAsset(TRON_MAINNET.id)).toMatchObject({ key: "usdt", address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", decimals: 6 }); + expect(usdtAsset(TRON_NILE.id)).toMatchObject({ key: `trc20:${NILE_USDT}`, symbol: "USDT" }); + expect(usdtAsset(TRON_SHASTA.id)).toBeNull(); + }); +}); + +describe("transaction bytes", () => { + it("parses what java-tron builds: txID = sha256(raw_data), JSON agrees with the hex (as TronWeb's txCheck says)", () => { + for (const [name, tx] of Object.entries(NILE_TX)) { + const raw = parseRaw(fromHex(tx.raw_data_hex)); + expect(hex(txIdOf(raw.bytes)), name).toBe(tx.txID); + expect(raw.contracts).toHaveLength(1); + expect(raw.oddities, name).toEqual([]); + expect(jsonMismatches(tx.raw_data as Record, raw), name).toEqual([]); + expect(tw.utils.transaction.txCheck(JSON.parse(JSON.stringify(tx))), name).toBe(true); + } + }); + + it("spots JSON that says something other than the hex", () => { + const tx = JSON.parse(JSON.stringify(NILE_TX.transfer)); + tx.raw_data.contract[0].parameter.value.amount = 1; + expect(jsonMismatches(tx.raw_data, parseRaw(fromHex(tx.raw_data_hex)))).toEqual(["contract[0].amount"]); + expect(tw.utils.transaction.txCheck(tx)).toBe(false); + const t2 = JSON.parse(JSON.stringify(NILE_TX.trc20)); + t2.raw_data.contract[0].parameter.value.contract_address = BOB; + expect(jsonMismatches(t2.raw_data, parseRaw(fromHex(t2.raw_data_hex)))).toEqual(["contract[0].contract_address"]); + }); + + it("encodes byte-for-byte what TronWeb encodes from the same JSON", () => { + const ref = { number: 71585237n, blockId: "0000000004444dd56ee3c6be8649ac0a384d36e2fb9f1724e30d40b7d8faf835", timestamp: 1791284757000n }; + const me = addressBytes(ME)!; + const bob = addressBytes(BOB)!; + const usdt = addressBytes(NILE_USDT)!; + const data = fromHex(`a9059cbb000000000000000000000000${hex(bob).slice(2)}${(1_000_000).toString(16).padStart(64, "0")}`); + const cases = [ + { raw: encodeRaw({ ref, timestamp: 1791284760000n, expiration: 1791285357000n, name: "TransferContract", value: encodeTransfer(me, bob, 1_500_000n) }), type: "TransferContract", value: { owner_address: hex(me), to_address: hex(bob), amount: 1_500_000 } }, + { raw: encodeRaw({ ref, timestamp: 1791284760000n, expiration: 1791285357000n, feeLimit: 2_900_000n, name: "TriggerSmartContract", value: encodeTrigger(me, usdt, data) }), type: "TriggerSmartContract", value: { owner_address: hex(me), contract_address: hex(usdt), data: hex(data) } }, + { raw: encodeRaw({ ref, timestamp: 1791284760000n, expiration: 1791285357000n, memo: "hello", name: "TransferContract", value: encodeTransfer(me, bob, 1n) }), type: "TransferContract", value: { owner_address: hex(me), to_address: hex(bob), amount: 1 } }, + ]; + for (const c of cases) { + const r = parseRaw(c.raw); + const json = { + visible: false, + txID: hex(txIdOf(c.raw)), + raw_data_hex: hex(c.raw), + raw_data: { + ref_block_bytes: hex(r.refBlockBytes), + ref_block_hash: hex(r.refBlockHash), + expiration: Number(r.expiration), + timestamp: Number(r.timestamp), + ...(r.feeLimit ? { fee_limit: Number(r.feeLimit) } : {}), + ...(r.data.length ? { data: hex(r.data) } : {}), + contract: [{ type: c.type, parameter: { type_url: `type.googleapis.com/protocol.${c.type}`, value: c.value } }], + }, + }; + expect(tw.utils.transaction.txPbToRawDataHex(tw.utils.transaction.txJsonToPb(json)).toLowerCase()).toBe(hex(c.raw)); + expect(jsonMismatches(json.raw_data, r)).toEqual([]); + } + }); +}); + +describe("decode", () => { + it("explains a dapp's TRX transfer with the bandwidth it burns", async () => { + const { m, ctx } = setup(); + const d = await m.decode(signTx(NILE_TX.transfer), ctx); + expect(d.title).toBe(`Send 1.5 TRX to ${short(BOB)}`); + expect(d.titleMsg).toMatchObject({ id: "bg.req.sendTo", values: { amount: "1.5 TRX", to: short(BOB) } }); + expect(d.blind).toBe(false); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "trx" }), delta: "-1500000" }]); + // 1 + 2 (length varint) + 133 raw + 67 signature + 64 result bytes = 267 bytes × 1000 sun (no free bandwidth left) + expect(d.fee).toEqual({ asset: expect.objectContaining({ key: "trx" }), amount: "267000" }); + expect(d.lines).toEqual( + expect.arrayContaining([ + { label: "To", value: BOB }, + { label: "Network fee at most", value: "0.267 TRX" }, + expect.objectContaining({ label: "Sent by", value: "app.example (it gets the signed transaction)" }), + ]), + ); + }); + + it("is free when the daily free bandwidth covers it, and says when the recipient's account must be opened", async () => { + const free = setup({ "/wallet/getaccountresource": { freeNetLimit: 600, freeNetUsed: 0 } }); + expect((await free.m.decode(signTx(NILE_TX.transfer), free.ctx)).fee?.amount).toBe("0"); + + const fresh = setup({ "/wallet/getaccount": (b: Record) => (b.address === ME ? meAccount() : {}) }); + const d = await fresh.m.decode(signTx(NILE_TX.transfer), fresh.ctx); + expect(d.fee?.amount).toBe("1100000"); // 1 TRX system fee + 0.1 TRX (no staked bandwidth); free bandwidth can't pay it + expect(d.warnings).toContainEqual(expect.objectContaining({ code: "new-recipient", msg: expect.objectContaining({ id: "bg.tron.newAccountFee", values: { amount: "1.1 TRX" } }) })); + }); + + it("explains a TRC-20 transfer with the energy it burns", async () => { + const { m, ctx } = setup({ + "/wallet/getaccountresource": { freeNetLimit: 600, freeNetUsed: 0, EnergyLimit: 1975, EnergyUsed: 0 }, + "/wallet/triggerconstantcontract": { result: { result: true }, energy_used: 14650, constant_result: ["0".repeat(63) + "1"] }, + "/wallet/estimateenergy": { result: { result: true }, energy_required: 21975 }, + }, NILE_TX.trc20.raw_data.ref_block_hash); + const d = await m.decode(signTx(NILE_TX.trc20), ctx); + expect(d.title).toBe(`Send 1 USDT to ${short(BOB)}`); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: `trc20:${NILE_USDT}`, symbol: "USDT" }), delta: "-1000000" }]); + expect(d.fee?.amount).toBe("2000000"); // (21975 − 1975 available) × 100 sun; bandwidth is free + expect(d.simulated).toBe(true); + expect(d.lines).toContainEqual({ label: "Token", value: `USDT (${NILE_USDT})` }); + }); + + it("warns when a contract call is expected to fail", async () => { + const { m, ctx } = setup({ + "/wallet/triggerconstantcontract": { result: { result: false, code: "CONTRACT_EXE_ERROR", message: hex(utf8("REVERT opcode executed")) } }, + "/wallet/estimateenergy": { result: { result: false } }, + }, NILE_TX.trc20.raw_data.ref_block_hash); + const d = await m.decode(signTx(NILE_TX.trc20), ctx); + expect(d.warnings).toContainEqual(expect.objectContaining({ code: "simulation-failed", msg: expect.objectContaining({ id: "bg.warn.expectedToFailReason", values: { reason: "REVERT opcode executed" } }) })); + expect(d.fee?.amount).toBe(String(30_000_000 + (1 + 2 + 211 + 67 + 64) * 1000)); // the whole fee limit + bandwidth + }); + + it("flags an unlimited approval (hex addresses, visible: false)", async () => { + const { m, ctx } = setup({ "/wallet/triggerconstantcontract": { result: { result: true }, energy_used: 20000 } }, NILE_TX.approve.raw_data.ref_block_hash); + const d = await m.decode(signTx(NILE_TX.approve), ctx); + expect(d.title).toBe(`Allow ${short(BOB)} to spend unlimited USDT`); + expect(d.warnings[0]).toMatchObject({ level: "danger", code: "unlimited-approval", msg: { id: "bg.warn.letsTakeAll" } }); + expect(d.blind).toBe(false); + }); + + it("explains staking, unstaking, lending energy, voting and permission changes in plain words", async () => { + const run = async (tx: (typeof NILE_TX)[keyof typeof NILE_TX]) => { + const { m, ctx } = setup({}, tx.raw_data.ref_block_hash); + return m.decode(signTx(tx), ctx); + }; + const freeze = await run(NILE_TX.freeze); + expect(freeze.title).toBe("Stake 10 TRX for energy"); + expect(freeze.titleMsg?.values?.resource).toMatchObject({ id: "bg.tron.energy" }); + expect(freeze.lines).toContainEqual(expect.objectContaining({ label: "Unstaking takes", value: "14 days" })); + expect(freeze.balanceChanges[0]?.delta).toBe("-10000000"); + + expect((await run(NILE_TX.unfreeze)).title).toBe("Unstake 1 TRX"); + + const lend = await run(NILE_TX.delegate); + expect(lend.title).toBe(`Lend the energy of 5 TRX you've staked to ${short(BOB)}`); + expect(lend.lines).toContainEqual(expect.objectContaining({ label: "Locked", value: "About 24 hours. You can't take it back before then." })); + + const vote = await run(NILE_TX.vote); + expect(vote.title).toBe("Vote for 1 Super Representatives"); + expect(vote.lines).toEqual(expect.arrayContaining([{ label: "Validator", value: `${toBase58("41608e7e1c6f6dcc1679ea512503e41ca0254e0948")}: 3` }])); + + const perm = await run(NILE_TX.perm); + expect(perm.title).toBe("Change who controls your TRON account"); + expect(perm.warnings).toContainEqual(expect.objectContaining({ level: "danger", code: "account-takeover" })); + expect(perm.lines[0]).toEqual({ label: "Owner", value: `${BOB} ×1 (needs 1)` }); + + const memo = await run(NILE_TX.memo); + expect(memo.lines).toContainEqual({ label: "Memo", value: "hello" }); + expect(memo.fee?.amount).toBe(String(1_000_000 + (1 + 2 + 138 + 67 + 64) * 1000)); // the memo fee + bandwidth + }); + + it("is blind for calldata it can't read, unknown contract types and bytes that don't parse", async () => { + const me = addressBytes(ME)!; + const ref = { number: 71585237n, blockId: "0000000004444dd56ee3c6be8649ac0a384d36e2fb9f1724e30d40b7d8faf835", timestamp: 1791284757000n }; + const call = encodeRaw({ ref, timestamp: 1791284760000n, expiration: 1791286000000n, feeLimit: 10_000_000n, name: "TriggerSmartContract", value: encodeTrigger(me, addressBytes(NILE_USDT)!, fromHex("12345678"), 5n) }); + const { m, ctx } = setup({ "/wallet/triggerconstantcontract": { result: { result: true }, energy_used: 500 } }, "6ee3c6be8649ac0a"); + const d = await m.decode(signTx({ raw_data_hex: hex(call) }), ctx); + expect(d.blind).toBe(true); + expect(d.title).toBe(`Use 0x12345678 on contract ${short(NILE_USDT)}`); + expect(d.warnings[0]).toMatchObject({ code: "blind-signing", level: "danger" }); + expect(d.balanceChanges).toEqual([{ asset: expect.objectContaining({ key: "trx" }), delta: "-5" }]); + + const proposal = encodeRaw({ ref, timestamp: 1791284760000n, expiration: 1791286000000n, name: "ProposalCreateContract", value: encodeTransfer(me, me, 0n) }); + expect((await m.decode(signTx({ raw_data_hex: hex(proposal) }), ctx)).blind).toBe(true); + + const garbage = await m.decode(signTx({ raw_data_hex: "0aff" }), ctx); + expect(garbage.blind).toBe(true); + expect(garbage.titleMsg?.id).toBe("bg.req.unreadableFrom"); + }); + + it("explains a message", async () => { + const { m, ctx } = setup(); + const d = await m.decode(req(TRON_METHODS.signMessage, { address: ME, message: "Sign in to app.example" }), ctx); + expect(d).toMatchObject({ title: "Sign a message for app.example", blind: false, lines: [{ label: "Message", value: "Sign in to app.example" }] }); + const bin = await m.decode(req(TRON_METHODS.signMessage, { message: "00ff", encoding: "hex" }), ctx); + expect(bin.lines).toEqual([{ label: "Message (not text)", value: "0x00ff" }]); + }); + + it("refuses the wrong network, the wrong account, a lying txID or JSON, an expired or foreign transaction", async () => { + const code = async (p: Promise) => { + try { + await p; + } catch (e) { + expect(e).toBeInstanceOf(ClipError); + return (e as ClipError).code; + } + return "no error"; + }; + const { m, ctx } = setup(); + expect(await code(m.decode({ ...signTx(NILE_TX.transfer), networkId: TRON_MAINNET.id }, ctx))).toBe("tron/network-mismatch"); + // A transaction whose reference block isn't on this network (TAPOS) is refused too. + const other = setup({}, "0000000000000000"); + expect(await code(other.m.decode(signTx(NILE_TX.transfer), other.ctx))).toBe("tron/network-mismatch"); + expect(await code(m.decode(signTx(NILE_TX.transfer, { address: BOB }), ctx))).toBe("tron/wrong-account"); + expect(await code(m.decode(signTx({ ...NILE_TX.transfer, txID: NILE_TX.trc20.txID }), ctx))).toBe("tron/txid-mismatch"); + const lying = JSON.parse(JSON.stringify(NILE_TX.transfer)); + lying.raw_data.contract[0].parameter.value.to_address = ME; + expect(await code(m.decode(signTx(lying), ctx))).toBe("tron/json-mismatch"); + expect(await code(m.decode(signTx({ nope: 1 }), ctx))).toBe("tron/malformed"); + const late = setup({}, NILE_TX.transfer.raw_data.ref_block_hash, NILE_TX.transfer.raw_data.expiration + 1); + expect(await code(late.m.decode(signTx(NILE_TX.transfer), late.ctx))).toBe("tron/expired"); + // Bob's transaction: this account isn't its owner. + const ref = { number: 71585237n, blockId: "0000000004444dd56ee3c6be8649ac0a384d36e2fb9f1724e30d40b7d8faf835", timestamp: 1791284757000n }; + const bobs = encodeRaw({ ref, timestamp: 1n, expiration: 1791286000000n, name: "TransferContract", value: encodeTransfer(addressBytes(BOB)!, addressBytes(ME)!, 1n) }); + expect(await code(m.decode(signTx({ raw_data_hex: hex(bobs) }), ctx))).toBe("tron/not-a-signer"); + // An account whose owner permission was handed to another key (what happened to this public test account on Nile). + const taken = setup({ "/wallet/getaccount": { ...meAccount(), owner_permission: { threshold: 1, keys: [{ address: "TBvYV4gvmB73zNhGdMW12bxxmPkpibcrRt", weight: 1 }] } } }); + const e = await taken.m.decode(signTx(NILE_TX.transfer), taken.ctx).catch((x: ClipError) => x); + expect(e).toMatchObject({ code: "tron/not-controlled", msg: { id: "bg.tron.notControlled" } }); + expect(await code(m.decode(req("tron_signTypedData", {}), ctx))).toBe("tron/unsupported-method"); + }); + + it("controls(): owner, active permissions and multi-signature thresholds", () => { + expect(controls({}, ME, 0)).toBe(true); + expect(controls({}, ME, 2)).toBe(false); + expect(controls(meAccount(), ME, 0)).toBe(true); + expect(controls(meAccount(), ME, 2)).toBe(true); + expect(controls(meAccount(), ME, 3)).toBe(false); + expect(controls({ address: ME, owner_permission: { threshold: 2, keys: [{ address: ME, weight: 1 }, { address: BOB, weight: 1 }] } }, ME, 0)).toBe(false); + }); +}); + +describe("prepare and finalize", () => { + it("signs sha256(raw_data) and returns the transaction with an r‖s‖v signature TronWeb recovers", async () => { + const { m, ctx, calls } = setup(); + const r = signTx(NILE_TX.transfer); + const payloads = await m.prepare(r, ctx, "a1"); + expect(payloads).toEqual([{ accountId: "tron:0", scheme: "ecdsa-secp256k1", bytes: fromHex(NILE_TX.transfer.txID), approvalId: "a1" }]); + const out = (await m.finalize(r, payloads.map((p) => signer.sign(p)), ctx)) as { txID: string; signature: string[]; raw_data_hex: string }; + expect(out.txID).toBe(NILE_TX.transfer.txID); + expect(out.signature).toEqual([SIG.dappTransfer]); + expect(out.raw_data_hex).toBe(NILE_TX.transfer.raw_data_hex); + expect(tw.address.fromHex(tw.utils.crypto.ecRecover(out.txID, out.signature[0]!))).toBe(ME); + expect(calls.filter((c) => c.path.includes("broadcast"))).toEqual([]); + }); + + it("signs messages like TronWeb's signMessageV2 (verifyMessageV2 recovers the account)", async () => { + const { m, ctx } = setup(); + const r = req(TRON_METHODS.signMessage, { address: ME, message: "Sign in to app.example" }); + const [p] = await m.prepare(r, ctx, "a2"); + expect(hex(p!.bytes)).toBe(tw.utils.message.hashMessage("Sign in to app.example").replace(/^0x/, "")); + expect(hex(messageHash(utf8("Sign in to app.example")))).toBe(hex(p!.bytes)); + const out = (await m.finalize(r, [signer.sign(p!)], ctx)) as { signature: string }; + expect(out.signature).toBe(`0x${SIG.message}`); + expect(tw.Trx.verifyMessageV2("Sign in to app.example", out.signature)).toBe(ME); + }); + + it("refuses a bad signature before broadcasting", async () => { + const { m, ctx, calls } = setup({ "/wallet/broadcasthex": { result: true } }); + const r = { ...signTx(NILE_TX.transfer), method: TRON_METHODS.signAndSendTransaction }; + const [p] = await m.prepare(r, ctx, "a3"); + const good = signer.sign(p!); + const flipped = good.bytes.slice(); + flipped[10] = flipped[10]! ^ 1; + await expect(m.finalize(r, [{ ...good, bytes: flipped }], ctx)).rejects.toMatchObject({ code: "tron/bad-signature" }); + // A real signature, but over another transaction. + const other = fixtureSigner([SIG.dappTrc20]).sign({ ...p!, bytes: fromHex(NILE_TX.trc20.txID) }); + await expect(m.finalize(r, [other], ctx)).rejects.toMatchObject({ code: "tron/bad-signature" }); + await expect(m.finalize(r, [], ctx)).rejects.toMatchObject({ code: "tron/bad-signature" }); + expect(calls.filter((c) => c.path.includes("broadcast"))).toEqual([]); + }); + + it("fixes a wrong recovery id instead of sending a signature java-tron would reject", async () => { + const { m, ctx } = setup(); + const r = signTx(NILE_TX.transfer); + const [p] = await m.prepare(r, ctx, "a4"); + const s = signer.sign(p!); + const out = (await m.finalize(r, [{ ...s, recovery: 1 - (s.recovery ?? 0) }], ctx)) as { signature: string[] }; + expect(out.signature).toEqual([SIG.dappTransfer]); + }); +}); + +describe("buildTransfer", () => { + const head = { blockId: "6ee3c6be8649ac0a" }; + const build = (over: Record = {}) => setup(over, head.blockId, BUILD_NOW); + + it("builds, decodes, signs, broadcasts and confirms a TRX send", async () => { + let sent: Record | undefined; + const { m, ctx, calls } = build({ + "/wallet/broadcasthex": (b: Record) => ((sent = b), { result: true, txid: "x" }), + "/wallet/gettransactioninfobyid": (b: Record) => ({ id: b.value, blockNumber: 71585240, receipt: {} }), + }); + const r = await m.buildTransfer({ asset: TRON_NILE.nativeAsset, to: BOB, amount: "1500000" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, family: "tron", networkId: TRON_NILE.id, method: TRON_METHODS.signAndSendTransaction }); + expect(rawOf(r)).toBe(SIG.builtRaw); + const d = await m.decode(r, ctx); + expect(d.title).toBe(`Send 1.5 TRX to ${short(BOB)}`); + expect(d.lines.some((l) => l.label === "Sent by")).toBe(false); + const payloads = await m.prepare(r, ctx, "b1"); + const out = (await m.finalize(r, payloads.map((p) => signer.sign(p)), ctx)) as { txID: string; result: boolean }; + const txID = hex(txIdOf(fromHex(SIG.builtRaw))); + expect(out).toMatchObject({ txID, result: true }); + // Transaction { raw_data = 1; signature = 2 } + expect(SIG.builtRaw.length / 2).toBe(133); // length varint 0x85 0x01 + expect(sent?.transaction).toBe(`0a8501${SIG.builtRaw}1241${SIG.builtTrx}`); + expect(calls.some((c) => c.path === "/wallet/gettransactioninfobyid")).toBe(true); + }); + + it("refuses in plain words", async () => { + const code = (p: Promise) => p.then(() => "no error", (e: ClipError) => e.code); + const { m, ctx } = build(); + const trx = TRON_NILE.nativeAsset; + expect(await code(m.buildTransfer({ asset: trx, to: ME, amount: "1" }, ctx))).toBe("tron/self-transfer"); + expect(await code(m.buildTransfer({ asset: trx, to: "TBad", amount: "1" }, ctx))).toBe("tron/bad-address"); + expect(await code(m.buildTransfer({ asset: trx, to: BOB, amount: "0" }, ctx))).toBe("tron/bad-amount"); + expect(await code(m.buildTransfer({ asset: trx, to: BOB, amount: "57200892190" }, ctx))).toBe("tron/insufficient-funds"); + const fresh = build({ "/wallet/getaccount": {} }); + const e = await fresh.m.buildTransfer({ asset: trx, to: BOB, amount: "1" }, fresh.ctx).catch((x: ClipError) => x); + expect(e).toMatchObject({ code: "tron/not-open", msg: { id: "bg.tron.notOpen" } }); + const toContract = build({ "/wallet/getaccount": (b: Record) => (b.address === ME ? meAccount() : { address: NILE_USDT, type: "Contract" }) }); + expect(await code(toContract.m.buildTransfer({ asset: trx, to: NILE_USDT, amount: "1" }, toContract.ctx))).toBe("tron/contract-recipient"); + }); + + it("says why a broadcast failed", async () => { + const { m, ctx } = build({ "/wallet/broadcasthex": { result: false, code: "BANDWITH_ERROR", message: hex(utf8("Account resource insufficient error.")) } }); + const r = await m.buildTransfer({ asset: TRON_NILE.nativeAsset, to: BOB, amount: "1500000" }, ctx); + const payloads = await m.prepare(r, ctx, "b2"); + const e = await m.finalize(r, payloads.map((p) => signer.sign(p)), ctx).catch((x: ClipError) => x); + expect(e).toMatchObject({ code: "tron/send-failed", userMessage: "You need a little TRX to pay for this transaction's bandwidth. Nothing was sent." }); + }); + + it("builds a TRC-20 (USDT) send with a fee limit from the energy estimate", async () => { + const usdt = usdtAsset(TRON_NILE.id)!; + const trigger = (b: Record) => + String(b.data).startsWith("70a08231") + ? { result: { result: true }, constant_result: [(5_000_000).toString(16).padStart(64, "0")] } + : { result: { result: true }, energy_used: 14650, constant_result: ["0".repeat(63) + "1"] }; + const { m, ctx } = build({ "/wallet/triggerconstantcontract": trigger, "/wallet/estimateenergy": { result: { result: true }, energy_required: 21975 } }); + const r = await m.buildTransfer({ asset: usdt, to: BOB, amount: "1000000" }, ctx); + const raw = parseRaw(fromHex(rawOf(r))); + expect(raw.contracts[0]?.name).toBe("TriggerSmartContract"); + expect(raw.feeLimit).toBe(2_900_000n); // 21975 energy × 100 sun × 1.3, rounded up to 0.1 TRX + const d = await m.decode(r, ctx); + expect(d.title).toBe(`Send 1 USDT to ${short(BOB)}`); + expect(d.fee?.amount).toBe(String(2_197_500 + (1 + 2 + parseRaw(fromHex(rawOf(r))).bytes.length + 67 + 64) * 1000)); // energy + bandwidth + expect(await m.buildTransfer({ asset: usdt, to: BOB, amount: "6000000" }, ctx).then(() => "", (e: ClipError) => e.code)).toBe("tron/insufficient-token"); + }); +}); + +describe("balances", () => { + it("reads TRX and USDT, and staking separately", async () => { + const { m, ctx } = setup({ + "/wallet/getaccount": { + ...meAccount(), + frozenV2: [{ amount: 1_000_000 }, { type: "ENERGY", amount: 2_000_000 }, { type: "TRON_POWER" }], + unfrozenV2: [{ type: "ENERGY", unfreeze_amount: 300, unfreeze_expire_time: DAPP_NOW + 1 }, { unfreeze_amount: 40, unfreeze_expire_time: DAPP_NOW - 1 }], + }, + "/wallet/triggerconstantcontract": (b: Record) => { + expect(b).toMatchObject({ owner_address: ME, contract_address: NILE_USDT, data: `70a08231000000000000000000000000${hex(addressBytes(ME)!).slice(2)}` }); + return { result: { result: true }, constant_result: [(25_000_000).toString(16).padStart(64, "0")] }; + }, + "/wallet/getaccountresource": { EnergyLimit: 100, EnergyUsed: 1, NetLimit: 10, NetUsed: 2, freeNetLimit: 600, freeNetUsed: 5 }, + }); + const b = await m.getBalances(ctx); + expect(b.map((x) => [x.asset.key, x.amount])).toEqual([ + ["trx", "57200892190"], + [`trc20:${NILE_USDT}`, "25000000"], + ]); + expect(await m.staking(ctx)).toEqual({ + staked: "3000000", + unstaking: "300", + withdrawable: "40", + energy: { used: "1", limit: "100" }, + bandwidth: { used: "2", limit: "10", freeUsed: "5", freeLimit: "600" }, + }); + expect(await m.getNfts(ctx)).toEqual([]); + const empty = setup({ "/wallet/getaccount": {}, "/wallet/triggerconstantcontract": { result: { result: true }, constant_result: ["0".repeat(64)] } }); + expect((await empty.m.getBalances(empty.ctx)).map((x) => x.amount)).toEqual(["0", "0"]); + }); + + it("uses the next endpoint when one is down, and says so in plain words when none answers", async () => { + const ctx = ctxFor((async (u: string) => (String(u).startsWith("https://nile.trongrid.io") ? new Response("busy", { status: 503 }) : new Response(JSON.stringify(meAccount())))) as typeof fetch); + const m = createTronModule(); + expect((await m.getBalances({ ...ctx, network: { ...TRON_SHASTA, rpcUrls: ["https://nile.trongrid.io", "https://api.nileex.io"] } }))[0]?.amount).toBe("57200892190"); + const down = ctxFor((async () => { + throw new TypeError("fetch failed"); + }) as typeof fetch); + await expect(m.getBalances(down)).rejects.toMatchObject({ code: "tron/offline" }); + }); +}); + diff --git a/packages/chains-tron/tsconfig.json b/packages/chains-tron/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-tron/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/chains-xrpl/LICENSE b/packages/chains-xrpl/LICENSE new file mode 100644 index 00000000..fd2e97a7 --- /dev/null +++ b/packages/chains-xrpl/LICENSE @@ -0,0 +1,202 @@ + + 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. + + Copyright 2026 ColdAI (https://coldai.org) + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/chains-xrpl/NOTICE b/packages/chains-xrpl/NOTICE new file mode 100644 index 00000000..d8e118d6 --- /dev/null +++ b/packages/chains-xrpl/NOTICE @@ -0,0 +1,8 @@ +Clip Wallet: @clip-wallet/chains-xrpl +Copyright 2026 ColdAI (https://coldai.org) + +This product includes software developed by ColdAI (https://coldai.org). +Licensed under the Apache License, Version 2.0 (see LICENSE). + +"Clip Wallet", "1Mask" and the Clip Wallet logo are trademarks of ColdAI. The Apache +License 2.0 grants no rights to use them (Section 6). diff --git a/packages/chains-xrpl/README.md b/packages/chains-xrpl/README.md new file mode 100644 index 00000000..083e8976 --- /dev/null +++ b/packages/chains-xrpl/README.md @@ -0,0 +1,111 @@ +# @clip-wallet/chains-xrpl + +XRP Ledger `ChainModule` for Clip Wallet. It builds, decodes and checks transactions in plain words and never +touches keys: `prepare()` returns the 32-byte digest for the vault to sign (`ecdsa-secp256k1`), and `finalize()` +checks the signature against the account's key before anything is assembled or sent. + +No SDK at runtime. The binary codec subset (`src/codec.ts`), addresses (`src/address.ts`) and JSON-RPC client +(`src/rpc.ts`) are hand-written on `@noble/hashes`, `@noble/curves` (verification only) and `@scure/base`. Tests +encode the same JSON with `ripple-binary-codec` (xrpl.js) and require identical bytes, verify the signatures with +`ripple-keypairs` and compute transaction ids with `xrpl`'s `hashes.hashSignedTx` (all devDependencies). + +## Networks + +| network | NetworkId (CAIP-2) | JSON-RPC (public, no key) | explorer | +|---|---|---|---| +| Testnet | `xrpl:1` | `https://s.altnet.rippletest.net:51234/`, `https://testnet.xrpl-labs.com/` | testnet.xrpl.org | +| Mainnet | `xrpl:0` | `https://xrplcluster.com/`, `https://s1.ripple.com:51234/`, `https://s2.ripple.com:51234/` | livenet.xrpl.org | +| Devnet | `xrpl:2` | `https://s.devnet.rippletest.net:51234/` | devnet.xrpl.org | + +- Ids: ChainAgnostic namespaces `xrpl/caip2.md` (`xrpl:`, from `server_info`). XLS-72d uses the same ids + and the aliases `xrpl:mainnet` / `xrpl:testnet` / `xrpl:devnet` (`fromChainId` accepts both). +- Every endpoint was checked with `server_info` (network_id, validated ledger, reserves) in October 2026. The reserves + then were 1 XRP base and 0.2 XRP per owned object; the module always reads them live. +- `NetworkID` is dropped from a transaction for networks ≤ 1024 and required above (rippled rules), so a request + naming another network's id is refused. + +## Accounts + +`curve: "secp256k1"`, `derivationPath(i) = m/44'/144'/i'/0/0` (xrpl.js `Wallet.fromMnemonic` for i = 0, Ledger Live, +GemWallet). Address = base58 (Ripple alphabet) check of `0x00 ‖ RIPEMD-160(SHA-256(compressed key))`; the public +"abandon … about" account is `rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3` (checked against `ripple-keypairs.deriveAddress`). +`isAddress` accepts classic addresses and X-addresses (checksums verified); `networksForAddress` keeps an X-address to +mainnet or the test networks. `buildTransfer` accepts an X-address and turns its tag into `DestinationTag`. + +## Assets + +- XRP: key `xrp`, 6 decimals (drops). `spendable(ctx)` returns balance, locked (base reserve + owner reserve × + owned objects, from `server_info` and `account_info`) and spendable XRP. +- Trust-line tokens from `account_lines`. Known: Ripple USD **RLUSD** (key `rlusd`, currency + `524C555344…`; issuer `rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De` mainnet, `rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV` + testnet) and Circle **USDC** (key `usdc`, currency `5553444300…`; issuer `rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE` + mainnet, `rHuGNhqTG32mfmAvWA8hUyWRLV3tCSwKQt` testnet, developers.circle.com). Both issuers were checked with + `gateway_balances`. Known stablecoins use 6 decimals, other tokens 15 (amounts are truncated, never rounded up). +- Any other issuer's token that calls itself USD/USDC/RLUSD/USDT/XRP is `spam: true`. Negative lines (tokens this + account issued) are not holdings and are skipped. +- `getNfts` returns `[]`: XLS-20 NFTs need an `Nft.standard` value core doesn't have yet (`"xls20"`). +- `buildTrustLine({ asset })` adds a token (TrustSet, high limit, `tfSetNoRipple`; locks one owner reserve). + +## Requests (XLS-72d) + +| method | params | result | +|---|---|---| +| `xrpl:signTransaction` | `{ tx_json, account: "r…", network: "xrpl:1" \| "xrpl:testnet", options?: { autofill?, multisig? } }` | `{ signed_tx_blob }` (upper-case hex) | +| `xrpl:signAndSubmitTransaction` | same | `{ tx_hash, tx_json }`: `submit`, then `tx` until validated (`tx_json` is that answer) | + +`buildTransfer` (XRP or a trust-line token) uses `xrpl:signAndSubmitTransaction` with `origin: WALLET_ORIGIN` and a +fully filled transaction. + +- **Autofill**: a missing `Sequence` (from `account_info`; 0 with `TicketSequence`), `Fee` (`fee` command: + max(base, open-ledger fee), capped at 0.1 XRP; `AccountDelete` pays the owner reserve) and + `LastLedgerSequence` (current + 20) are filled once in `decode()` and cached per request id, so `prepare()` and + `finalize()` sign exactly what was shown. `SigningPubKey` is always the account's key. +- **Refused**: another network (request, `network` param or `NetworkID`), another account (`Account`, `account`, + `SigningPubKey`), multi-signing (`options.multisig` or an empty `SigningPubKey`), already signed (`TxnSignature`, + `Signers`), a master key that is turned off (`lsfDisableMaster`). +- **Signing**: digest = SHA-512Half(`53545800` ‖ signing fields). The vault's low-S r ‖ s is verified with + `secp256k1.verify(…, { prehash: false })`, then DER-encoded into `TxnSignature`. The id is + SHA-512Half(`54584E00` ‖ blob). `tem`/`tef`/`tel`/`ter` (except `terQUEUED`) answers mean nothing was sent; + a validated `tec` result throws `xrpl/failed` ("… The network fee was still charged."). Engine results are put in + plain words by `plainXrplError`. + +### decode() + +- **Payment**: "Send 1.5 XRP to rPT1…pAYe" / "Send 2.5 RLUSD to …"; cross-currency: "Pay up to … so … gets …". + `tfPartialPayment` → **danger** (the recipient may get far less; `DeliverMin` shown). Destination tag shown. + Recipient checks against the ledger: not activated and less than the base reserve → danger (it fails); activated by + this payment → info; `lsfRequireDestTag` without a tag → danger `memo-required`; `lsfDisallowXRP` → caution; + `lsfDepositAuth` → caution; token without the recipient's trust line → danger. XRP to yourself → danger (rippled + refuses it). Spending into the reserve → danger. +- **TrustSet**: "Add RLUSD to your account" (the reserve it locks) / "Remove … from your account"; look-alike tokens + → `known-scam`. +- **OfferCreate** "Trade 10 XRP for 5 RLUSD" (kind: standing / immediate-or-cancel / fill-or-kill), **OfferCancel**. +- **AccountSet**: every `SetFlag`/`ClearFlag` (and the legacy tf flags) in words; Domain, transfer fee, NFT minter + (caution), message key. `asfDisableMaster` → **danger `account-takeover`**. Unknown flags → blind. +- **SetRegularKey** "Give … control of your account" and **SignerListSet** → **danger `account-takeover`**. +- **AccountDelete** → **danger `account-closure`** (everything left goes to the destination; the fee is burned). +- **NFTokenMint / Burn / CreateOffer / AcceptOffer / CancelOffer**: the accepted offer is read with `ledger_entry` + so the title says "Buy an NFT for 3 XRP" / "Sell your NFT for …"; a free sell offer to anyone → danger. +- **EscrowCreate / Finish / Cancel** (times in UTC from the Ripple epoch), **AMMDeposit / AMMWithdraw**. +- Anything else (checks, payment channels, MPTs, XChain, AMMCreate/Bid/Vote, Batch …), and any field the codec + doesn't know (e.g. `Delegate`), is **blind**, and `prepare()` refuses it: Clip Wallet never signs bytes it can't + serialise itself. + +## Dapp connectivity (1Mask) + +XLS-72d "Browser Wallet Standard" (https://github.com/XRPLF/XRPL-Standards/discussions/206; reference types +`@xrpl-wallet-standard/core`, https://github.com/tequdev/xrpl-wallet-standard) is Wallet Standard with two features, +`xrpl:signTransaction` and `xrpl:signAndSubmitTransaction`, and chains `xrpl:`. A wallet registers itself +with `registerWallet`, under its own name and icon. 1Mask implements it (`packages/1mask/src/inpage/xrpl.ts`, +`background/xrpl.ts`). The XLS discussion was closed for inactivity in March 2026 and has no message-signing feature, +so there is no XRPL sign-in/sign-message in 1Mask. + +Not implemented, on purpose: GemWallet's API (`@gemwallet/api`) and the Crossmark SDK talk to those specific +extensions by their own message names; answering them would mean pretending to be GemWallet or Crossmark. +xrpl-connect (XRPL Commons) ships one adapter per wallet (Xaman, Crossmark, GemWallet, WalletConnect) and doesn't +discover Wallet Standard wallets. + +## Testnet funds + +`POST https://faucet.altnet.rippletest.net/accounts` with `{ "destination": "r…" }` sends 100 test XRP to that +address (no captcha, no account; checked October 2026). Devnet: `https://faucet.devnet.rippletest.net/accounts`. diff --git a/packages/chains-xrpl/package.json b/packages/chains-xrpl/package.json new file mode 100644 index 00000000..b334d7fd --- /dev/null +++ b/packages/chains-xrpl/package.json @@ -0,0 +1,81 @@ +{ + "name": "@clip-wallet/chains-xrpl", + "version": "0.1.0", + "description": "XRP Ledger ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + "keywords": [ + "clip-wallet", + "wallet", + "xrpl", + "chain-module" + ], + "license": "Apache-2.0", + "author": "ColdAI (https://coldai.org)", + "homepage": "https://github.com/ColdAI-org/clip-wallet/tree/main/packages/chains-xrpl#readme", + "bugs": { + "url": "https://github.com/ColdAI-org/clip-wallet/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ColdAI-org/clip-wallet.git", + "directory": "packages/chains-xrpl" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "development": "./src/networks.ts", + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./networks": { + "types": "./dist/networks.d.ts", + "import": "./dist/networks.js", + "default": "./dist/networks.js" + } + } + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node ../../tools/release/build-package.mjs", + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clip-wallet/core": "workspace:*", + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "devDependencies": { + "ripple-binary-codec": "^2.11.0", + "ripple-keypairs": "^3.1.0", + "xrpl": "^5.3.0" + } +} diff --git a/packages/chains-xrpl/src/address.ts b/packages/chains-xrpl/src/address.ts new file mode 100644 index 00000000..3b7baa32 --- /dev/null +++ b/packages/chains-xrpl/src/address.ts @@ -0,0 +1,92 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { ripemd160 } from "@noble/hashes/legacy.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { base58xrp } from "@scure/base"; +import { bytesEqual, concat } from "./util.js"; + +/** + * XRPL addresses (https://xrpl.org/docs/concepts/accounts/addresses): base58 with the Ripple alphabet over + * payload ‖ SHA-256(SHA-256(payload))[0..4]. Classic "r…" payload = 0x00 ‖ AccountID (RIPEMD-160(SHA-256(key))). + * X-addresses (https://xrpaddress.info, ripple-address-codec `encodeXAddress`) = prefix ‖ AccountID ‖ flag ‖ tag: + * prefix 0x05 0x44 ("X…", mainnet) or 0x04 0x93 ("T…", test networks), flag 0 (no tag) or 1 (32-bit tag, written + * little-endian in 8 bytes). + */ + +function checked(payload: Uint8Array): string { + return base58xrp.encode(concat(payload, sha256(sha256(payload)).subarray(0, 4))); +} + +function unchecked(text: string): Uint8Array | null { + let raw: Uint8Array; + try { + raw = base58xrp.decode(text); + } catch { + return null; + } + if (raw.length < 5) return null; + const payload = raw.subarray(0, raw.length - 4); + return bytesEqual(sha256(sha256(payload)).subarray(0, 4), raw.subarray(raw.length - 4)) ? payload : null; +} + +/** AccountID (20 bytes) of a secp256k1 public key. */ +export function accountIdOf(publicKey: Uint8Array): Uint8Array { + if (publicKey.length !== 33 && publicKey.length !== 65) throw new Error("XRPL needs a secp256k1 public key"); + return ripemd160(sha256(secp256k1.Point.fromBytes(publicKey).toBytes(true))); +} + +export function encodeClassic(accountId: Uint8Array): string { + if (accountId.length !== 20) throw new Error("AccountID is 20 bytes"); + return checked(concat(new Uint8Array([0x00]), accountId)); +} + +/** 20-byte AccountID of a classic address, or null when it isn't one (checksum included). */ +export function decodeClassic(address: string): Uint8Array | null { + if (!/^r[1-9A-HJ-NP-Za-km-z]{24,34}$/.test(address)) return null; + const p = unchecked(address); + return p && p.length === 21 && p[0] === 0x00 ? p.slice(1) : null; +} + +export function isClassicAddress(value: string): boolean { + return decodeClassic(value) !== null; +} + +export interface XAddress { + classic: string; + tag: number | null; + test: boolean; +} + +export function decodeXAddress(value: string): XAddress | null { + if (!/^[XT][1-9A-HJ-NP-Za-km-z]{45,47}$/.test(value)) return null; + const p = unchecked(value); + if (!p || p.length !== 31) return null; + const test = p[0] === 0x04 && p[1] === 0x93; + if (!test && !(p[0] === 0x05 && p[1] === 0x44)) return null; + const flag = p[22]; + const t = p.subarray(23, 31); + if (flag === 0) { + if (t.some((b) => b !== 0)) return null; + return { classic: encodeClassic(p.slice(2, 22)), tag: null, test }; + } + if (flag !== 1 || t[4] || t[5] || t[6] || t[7]) return null; // 64-bit tags aren't valid on the XRPL + const tag = (t[0]! | (t[1]! << 8) | (t[2]! << 16)) + t[3]! * 0x1000000; + return { classic: encodeClassic(p.slice(2, 22)), tag, test }; +} + +export function encodeXAddress(classic: string, tag: number | null, test: boolean): string { + const id = decodeClassic(classic); + if (!id) throw new Error("not a classic address"); + const t = new Uint8Array(8); + if (tag !== null) { + if (!Number.isInteger(tag) || tag < 0 || tag > 0xffffffff) throw new Error("bad destination tag"); + t[0] = tag & 0xff; + t[1] = (tag >>> 8) & 0xff; + t[2] = (tag >>> 16) & 0xff; + t[3] = (tag >>> 24) & 0xff; + } + return checked(concat(new Uint8Array(test ? [0x04, 0x93] : [0x05, 0x44]), id, new Uint8Array([tag === null ? 0 : 1]), t)); +} + +export function addressOfPublicKey(publicKey: Uint8Array): string { + return encodeClassic(accountIdOf(publicKey)); +} diff --git a/packages/chains-xrpl/src/codec.ts b/packages/chains-xrpl/src/codec.ts new file mode 100644 index 00000000..5edfe0df --- /dev/null +++ b/packages/chains-xrpl/src/codec.ts @@ -0,0 +1,416 @@ +import { sha512 } from "@noble/hashes/sha2.js"; +import { decodeClassic } from "./address.js"; +import { concat, fromHex, hex, isObj } from "./util.js"; + +/** + * A hand-written subset of the XRPL binary format (https://xrpl.org/docs/references/protocol/binary-format), enough + * for the transactions Clip Wallet reads and signs. Field type codes, field codes and the signing-field set are + * copied from ripple-binary-codec's definitions.json (rippled's SField.cpp); test/codec.test.ts encodes the same JSON + * with ripple-binary-codec and checks the bytes are identical. + * + * - Fields are sorted by (type code, field code). Field ids are 1–3 bytes. + * - Blob, AccountID and Vector256 are length-prefixed (VL: 1–3 bytes). + * - Amount: XRP = 8 bytes (bit 62 "positive", drops in the low 62 bits); issued = 8 bytes (bit 63, sign bit 62, + * exponent + 97 in 8 bits, 54-bit mantissa normalised to [10^15, 10^16)) ‖ currency (20) ‖ issuer (20). + * - STObject ends with 0xE1, STArray with 0xF1. PathSet steps are flagged 0x01/0x10/0x20, paths split by 0xFF, + * the set ends with 0x00. + * Anything else (unknown fields, MPT amounts, XChain types) throws `UnsupportedField`, so nothing is ever signed + * that this file can't fully serialise. + */ + +type FieldType = "UInt8" | "UInt16" | "UInt32" | "Hash128" | "Hash256" | "Amount" | "Blob" | "AccountID" | "STObject" | "STArray" | "PathSet" | "Vector256" | "Issue"; + +const TYPE_CODE: Record = { + UInt16: 1, + UInt32: 2, + Hash128: 4, + Hash256: 5, + Amount: 6, + Blob: 7, + AccountID: 8, + STObject: 14, + STArray: 15, + UInt8: 16, + PathSet: 18, + Vector256: 19, + Issue: 24, +}; + +interface FieldDef { + type: FieldType; + nth: number; + /** Part of the single-signing payload (TxnSignature and Signers aren't). */ + signing: boolean; +} + +const f = (type: FieldType, nth: number, signing = true): FieldDef => ({ type, nth, signing }); + +/** name → definition (ripple-binary-codec definitions.json, FIELDS). */ +export const FIELDS: Record = { + TransactionType: f("UInt16", 2), + SignerWeight: f("UInt16", 3), + TransferFee: f("UInt16", 4), + TradingFee: f("UInt16", 5), + NetworkID: f("UInt32", 1), + Flags: f("UInt32", 2), + SourceTag: f("UInt32", 3), + Sequence: f("UInt32", 4), + Expiration: f("UInt32", 10), + TransferRate: f("UInt32", 11), + DestinationTag: f("UInt32", 14), + QualityIn: f("UInt32", 20), + QualityOut: f("UInt32", 21), + OfferSequence: f("UInt32", 25), + LastLedgerSequence: f("UInt32", 27), + OperationLimit: f("UInt32", 29), + SetFlag: f("UInt32", 33), + ClearFlag: f("UInt32", 34), + SignerQuorum: f("UInt32", 35), + CancelAfter: f("UInt32", 36), + FinishAfter: f("UInt32", 37), + TicketSequence: f("UInt32", 41), + NFTokenTaxon: f("UInt32", 42), + EmailHash: f("Hash128", 1), + WalletLocator: f("Hash256", 7), + AccountTxnID: f("Hash256", 9), + NFTokenID: f("Hash256", 10), + InvoiceID: f("Hash256", 17), + NFTokenBuyOffer: f("Hash256", 28), + NFTokenSellOffer: f("Hash256", 29), + Amount: f("Amount", 1), + LimitAmount: f("Amount", 3), + TakerPays: f("Amount", 4), + TakerGets: f("Amount", 5), + Fee: f("Amount", 8), + SendMax: f("Amount", 9), + DeliverMin: f("Amount", 10), + Amount2: f("Amount", 11), + NFTokenBrokerFee: f("Amount", 19), + LPTokenOut: f("Amount", 25), + LPTokenIn: f("Amount", 26), + EPrice: f("Amount", 27), + MessageKey: f("Blob", 2), + SigningPubKey: f("Blob", 3), + TxnSignature: f("Blob", 4, false), + URI: f("Blob", 5), + Domain: f("Blob", 7), + MemoType: f("Blob", 12), + MemoData: f("Blob", 13), + MemoFormat: f("Blob", 14), + Fulfillment: f("Blob", 16), + Condition: f("Blob", 17), + Account: f("AccountID", 1), + Owner: f("AccountID", 2), + Destination: f("AccountID", 3), + Issuer: f("AccountID", 4), + RegularKey: f("AccountID", 8), + NFTokenMinter: f("AccountID", 9), + Memo: f("STObject", 10), + SignerEntry: f("STObject", 11), + SignerEntries: f("STArray", 4), + Memos: f("STArray", 9), + TickSize: f("UInt8", 16), + Paths: f("PathSet", 1), + NFTokenOffers: f("Vector256", 4), + Asset: f("Issue", 3), + Asset2: f("Issue", 4), +}; + +/** TRANSACTION_TYPES (definitions.json). Only these can be serialised; decode() reads a subset of them. */ +export const TX_TYPES: Record = { + Payment: 0, + EscrowCreate: 1, + EscrowFinish: 2, + AccountSet: 3, + EscrowCancel: 4, + SetRegularKey: 5, + OfferCreate: 7, + OfferCancel: 8, + TicketCreate: 10, + SignerListSet: 12, + TrustSet: 20, + AccountDelete: 21, + NFTokenMint: 25, + NFTokenBurn: 26, + NFTokenCreateOffer: 27, + NFTokenCancelOffer: 28, + NFTokenAcceptOffer: 29, + AMMDeposit: 36, + AMMWithdraw: 37, +}; + +export class UnsupportedField extends Error { + constructor(public readonly field: string, why = "isn't supported") { + super(`${field} ${why}`); + this.name = "UnsupportedField"; + } +} + +/* ------------------------------------------------------------------ primitives */ + +function fieldId(type: number, nth: number): Uint8Array { + if (type < 16 && nth < 16) return new Uint8Array([(type << 4) | nth]); + if (type < 16) return new Uint8Array([type << 4, nth]); + if (nth < 16) return new Uint8Array([nth, type]); + return new Uint8Array([0, type, nth]); +} + +export function vlPrefix(len: number): Uint8Array { + if (len <= 192) return new Uint8Array([len]); + if (len <= 12480) { + const l = len - 193; + return new Uint8Array([193 + (l >>> 8), l & 0xff]); + } + if (len <= 918744) { + const l = len - 12481; + return new Uint8Array([241 + (l >>> 16), (l >>> 8) & 0xff, l & 0xff]); + } + throw new Error("VL too long"); +} + +function uint(value: unknown, bytes: number, name: string): Uint8Array { + const n = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : NaN; + if (!Number.isInteger(n) || n < 0 || n >= 2 ** (bytes * 8)) throw new UnsupportedField(name, "isn't a valid number"); + const out = new Uint8Array(bytes); + let v = n; + for (let i = bytes - 1; i >= 0; i--) { + out[i] = v % 256; + v = Math.floor(v / 256); + } + return out; +} + +function hashOf(value: unknown, bytes: number, name: string): Uint8Array { + if (typeof value !== "string" || !new RegExp(`^[0-9a-fA-F]{${bytes * 2}}$`).test(value)) throw new UnsupportedField(name, "isn't a valid hash"); + return fromHex(value); +} + +function blob(value: unknown, name: string): Uint8Array { + if (typeof value !== "string" || !/^([0-9a-fA-F]{2})*$/.test(value)) throw new UnsupportedField(name, "isn't hex"); + return fromHex(value); +} + +export function accountId(value: unknown, name = "Account"): Uint8Array { + const id = typeof value === "string" ? decodeClassic(value) : null; + if (!id) throw new UnsupportedField(name, "isn't a valid XRPL address"); + return id; +} + +const STANDARD_CODE = /^[A-Za-z0-9?!@#$%^&*<>(){}[\]|]{3}$/; + +/** 20-byte currency code: "XRP" (all zero, only where XRP is allowed), a 3-character code, or 40 hex digits. */ +export function currencyCode(code: unknown, allowXrp: boolean, name: string): Uint8Array { + if (code === "XRP") { + if (!allowXrp) throw new UnsupportedField(name, "can't use XRP as an issued currency"); + return new Uint8Array(20); + } + if (typeof code === "string" && STANDARD_CODE.test(code)) { + const out = new Uint8Array(20); + for (let i = 0; i < 3; i++) out[12 + i] = code.charCodeAt(i); + return out; + } + if (typeof code === "string" && /^[0-9a-fA-F]{40}$/.test(code)) { + const b = fromHex(code); + if (b.every((x) => x === 0)) throw new UnsupportedField(name, "has an invalid currency code"); + return b; + } + throw new UnsupportedField(name, "has an invalid currency code"); +} + +const MIN_MANTISSA = 10n ** 15n; +const MAX_MANTISSA = 10n ** 16n; + +/** Issued-currency value → 8 bytes. Throws when it can't be represented exactly (more than 16 significant digits). */ +export function iouValue(value: unknown, name: string): Uint8Array { + const m = typeof value === "string" ? /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(value.trim()) : null; + if (!m || (!m[2] && !m[3])) throw new UnsupportedField(name, "isn't a valid amount"); + let mantissa = BigInt(`${m[2] ?? ""}${m[3] ?? ""}` || "0"); + let exp = Number(m[4] ?? 0) - (m[3]?.length ?? 0); + if (mantissa === 0n) return fromHex("8000000000000000"); + while (mantissa < MIN_MANTISSA) { + mantissa *= 10n; + exp -= 1; + } + while (mantissa >= MAX_MANTISSA) { + if (mantissa % 10n !== 0n) throw new UnsupportedField(name, "has too many significant digits"); + mantissa /= 10n; + exp += 1; + } + if (exp < -96 || exp > 80) throw new UnsupportedField(name, "is out of range"); + let bits = (1n << 63n) | (BigInt(exp + 97) << 54n) | mantissa; + if (m[1] !== "-") bits |= 1n << 62n; + return u64(bits); +} + +function u64(v: bigint): Uint8Array { + const out = new Uint8Array(8); + for (let i = 7; i >= 0; i--, v >>= 8n) out[i] = Number(v & 0xffn); + return out; +} + +const MAX_DROPS = 10n ** 17n; + +export function amount(value: unknown, name: string): Uint8Array { + if (typeof value === "string") { + if (!/^\d+$/.test(value) || BigInt(value) > MAX_DROPS) throw new UnsupportedField(name, "isn't a valid XRP amount in drops"); + return u64((1n << 62n) | BigInt(value)); + } + if (isObj(value) && typeof value.currency === "string" && typeof value.issuer === "string" && typeof value.value === "string") { + const extra = Object.keys(value).filter((k) => k !== "currency" && k !== "issuer" && k !== "value"); + if (extra.length) throw new UnsupportedField(`${name}.${extra[0]}`); + return concat(iouValue(value.value, name), currencyCode(value.currency, false, name), accountId(value.issuer, name)); + } + // MPT amounts ({ mpt_issuance_id, value }) and anything else. + throw new UnsupportedField(name, "isn't an XRP or issued-currency amount"); +} + +function issue(value: unknown, name: string): Uint8Array { + if (!isObj(value)) throw new UnsupportedField(name); + if (value.currency === "XRP" && value.issuer === undefined && Object.keys(value).length === 1) return new Uint8Array(20); + if (typeof value.issuer === "string" && Object.keys(value).length === 2) return concat(currencyCode(value.currency, false, name), accountId(value.issuer, name)); + throw new UnsupportedField(name); +} + +function pathSet(value: unknown, name: string): Uint8Array { + if (!Array.isArray(value) || !value.length) throw new UnsupportedField(name); + const parts: Uint8Array[] = []; + value.forEach((path, i) => { + if (!Array.isArray(path) || !path.length) throw new UnsupportedField(name); + if (i > 0) parts.push(new Uint8Array([0xff])); + for (const step of path) { + if (!isObj(step)) throw new UnsupportedField(name); + const keys = Object.keys(step).filter((k) => k !== "type" && k !== "type_hex"); + if (keys.some((k) => k !== "account" && k !== "currency" && k !== "issuer") || !keys.length) throw new UnsupportedField(name); + let type = 0; + const body: Uint8Array[] = []; + if (step.account !== undefined) { + type |= 0x01; + body.push(accountId(step.account, name)); + } + if (step.currency !== undefined) { + type |= 0x10; + body.push(currencyCode(step.currency, true, name)); + } + if (step.issuer !== undefined) { + type |= 0x20; + body.push(accountId(step.issuer, name)); + } + parts.push(new Uint8Array([type]), ...body); + } + }); + parts.push(new Uint8Array([0x00])); + return concat(...parts); +} + +function vector256(value: unknown, name: string): Uint8Array { + if (!Array.isArray(value)) throw new UnsupportedField(name); + const body = concat(...value.map((h) => hashOf(h, 32, name))); + return concat(vlPrefix(body.length), body); +} + +/* ------------------------------------------------------------------ objects */ + +function fieldValue(name: string, def: FieldDef, value: unknown, signingOnly: boolean): Uint8Array { + switch (def.type) { + case "UInt8": + return uint(value, 1, name); + case "UInt16": + if (name === "TransactionType") { + const code = typeof value === "string" ? TX_TYPES[value] : undefined; + if (code === undefined) throw new UnsupportedField(`TransactionType ${String(value)}`); + return uint(code, 2, name); + } + return uint(value, 2, name); + case "UInt32": + return uint(value, 4, name); + case "Hash128": + return hashOf(value, 16, name); + case "Hash256": + return hashOf(value, 32, name); + case "Amount": + return amount(value, name); + case "Blob": { + const b = blob(value, name); + return concat(vlPrefix(b.length), b); + } + case "AccountID": { + const id = accountId(value, name); + return concat(vlPrefix(20), id); + } + case "Issue": + return issue(value, name); + case "PathSet": + return pathSet(value, name); + case "Vector256": + return vector256(value, name); + case "STObject": + if (!isObj(value)) throw new UnsupportedField(name); + return concat(serializeObject(value, signingOnly), new Uint8Array([0xe1])); + case "STArray": { + if (!Array.isArray(value)) throw new UnsupportedField(name); + const parts: Uint8Array[] = []; + for (const el of value) { + const keys = isObj(el) ? Object.keys(el) : []; + const inner = keys[0]; + const innerDef = inner ? FIELDS[inner] : undefined; + if (keys.length !== 1 || !inner || !innerDef || innerDef.type !== "STObject") throw new UnsupportedField(name); + parts.push(fieldId(TYPE_CODE.STObject, innerDef.nth), fieldValue(inner, innerDef, (el as Record)[inner], signingOnly)); + } + parts.push(new Uint8Array([0xf1])); + return concat(...parts); + } + } +} + +/** JSON keys that aren't fields of the transaction (answers carry them; they are never serialised). */ +const NOT_SERIALIZED = new Set(["hash", "ctid", "date", "inLedger", "ledger_index", "meta", "validated"]); + +export function serializeObject(obj: Record, signingOnly = false): Uint8Array { + const entries: { name: string; def: FieldDef }[] = []; + for (const name of Object.keys(obj)) { + if (NOT_SERIALIZED.has(name) || obj[name] === undefined) continue; + const def = FIELDS[name]; + if (!def) throw new UnsupportedField(name); + if (signingOnly && !def.signing) continue; + entries.push({ name, def }); + } + entries.sort((a, b) => TYPE_CODE[a.def.type] - TYPE_CODE[b.def.type] || a.def.nth - b.def.nth); + return concat(...entries.flatMap(({ name, def }) => [fieldId(TYPE_CODE[def.type], def.nth), fieldValue(name, def, obj[name], signingOnly)])); +} + +/* ------------------------------------------------------------------ hashing */ + +export const sha512Half = (b: Uint8Array): Uint8Array => sha512(b).subarray(0, 32); + +/** HashPrefix::txSign ("STX\0") and HashPrefix::transactionID ("TXN\0") (rippled HashPrefix.h). */ +const PREFIX_SIGN = new Uint8Array([0x53, 0x54, 0x58, 0x00]); +const PREFIX_TXID = new Uint8Array([0x54, 0x58, 0x4e, 0x00]); + +/** The single-signing payload (ripple-binary-codec `encodeForSigning`). */ +export function signingPayload(tx: Record): Uint8Array { + return concat(PREFIX_SIGN, serializeObject(tx, true)); +} + +/** What the account key signs: SHA-512Half(0x53545800 ‖ signing fields). */ +export function signingDigest(tx: Record): Uint8Array { + return sha512Half(signingPayload(tx)); +} + +/** The transaction's id: SHA-512Half(0x54584E00 ‖ signed blob), upper-case hex. */ +export function txHash(blobBytes: Uint8Array): string { + return hex(sha512Half(concat(PREFIX_TXID, blobBytes))).toUpperCase(); +} + +/** r ‖ s (64 bytes, low-S) → DER, the form TxnSignature carries. */ +export function derSignature(rs: Uint8Array): Uint8Array { + if (rs.length !== 64) throw new Error("expected r||s"); + const int = (b: Uint8Array) => { + let i = 0; + while (i < b.length - 1 && b[i] === 0 && !(b[i + 1]! & 0x80)) i++; + const v = b.subarray(i); + return v[0]! & 0x80 ? concat(new Uint8Array([0]), v) : v; + }; + const r = int(rs.subarray(0, 32)); + const s = int(rs.subarray(32)); + return concat(new Uint8Array([0x30, 4 + r.length + s.length, 0x02, r.length]), r, new Uint8Array([0x02, s.length]), s); +} diff --git a/packages/chains-xrpl/src/describe.ts b/packages/chains-xrpl/src/describe.ts new file mode 100644 index 00000000..a4b1c059 --- /dev/null +++ b/packages/chains-xrpl/src/describe.ts @@ -0,0 +1,471 @@ +import { type AssetRef, type BalanceChange, type NetworkId, type Warning, say } from "@clip-wallet/core"; +import { currencyName, tokenAsset, xrpAsset } from "./networks.js"; +import type { AccountRoot, Reserves, Rpc } from "./rpc.js"; +import { decimalToUnits, formatUnits, isObj, short, textOfHex } from "./util.js"; + +export interface Line { + label: string; + value: string; +} + +export interface Described { + title: string; + lines: Line[]; + balanceChanges: BalanceChange[]; + warnings: Warning[]; + blind: boolean; +} + +export interface DescribeContext { + networkId: NetworkId; + me: string; + /** null in unit tests that don't look up the ledger. */ + rpc: Rpc | null; + reserves: Reserves | null; + /** This account as the ledger has it (null = not activated). undefined = unknown. */ + account?: AccountRoot | null; +} + +/* ------------------------------------------------------------------ flags (definitions.json) */ + +export const TF = { + partialPayment: 0x00020000, + sellNft: 0x00000001, + burnable: 0x00000001, + onlyXrp: 0x00000002, + transferable: 0x00000008, + mutable: 0x00000010, + offerPassive: 0x00010000, + offerIoc: 0x00020000, + offerFok: 0x00040000, + offerSell: 0x00080000, + setNoRipple: 0x00020000, + setFreeze: 0x00100000, + setDeepFreeze: 0x00400000, +} as const; + +/** AccountRoot flags (lsf*). */ +export const LSF = { + requireDestTag: 0x00020000, + disallowXrp: 0x00080000, + disableMaster: 0x00100000, + depositAuth: 0x01000000, +} as const; + +/** AccountSet SetFlag / ClearFlag values (asf*) → what they do, in words. */ +export const ASF: Record string; takeover?: boolean; caution?: boolean }> = { + 1: { name: "asfRequireDest", words: () => say("bg.xrpl.asf.requireDest") }, + 2: { name: "asfRequireAuth", words: () => say("bg.xrpl.asf.requireAuth") }, + 3: { name: "asfDisallowXRP", words: () => say("bg.xrpl.asf.disallowXrp") }, + 4: { name: "asfDisableMaster", words: () => say("bg.xrpl.asf.disableMaster"), takeover: true }, + 5: { name: "asfAccountTxnID", words: () => say("bg.xrpl.asf.accountTxnId") }, + 6: { name: "asfNoFreeze", words: () => say("bg.xrpl.asf.noFreeze"), caution: true }, + 7: { name: "asfGlobalFreeze", words: () => say("bg.xrpl.asf.globalFreeze"), caution: true }, + 8: { name: "asfDefaultRipple", words: () => say("bg.xrpl.asf.defaultRipple") }, + 9: { name: "asfDepositAuth", words: () => say("bg.xrpl.asf.depositAuth") }, + 10: { name: "asfAuthorizedNFTokenMinter", words: () => say("bg.xrpl.asf.nftMinter"), caution: true }, + 12: { name: "asfDisallowIncomingNFTokenOffer", words: () => say("bg.xrpl.asf.noNftOffers") }, + 13: { name: "asfDisallowIncomingCheck", words: () => say("bg.xrpl.asf.noChecks") }, + 14: { name: "asfDisallowIncomingPayChan", words: () => say("bg.xrpl.asf.noPayChannels") }, + 15: { name: "asfDisallowIncomingTrustline", words: () => say("bg.xrpl.asf.noTrustLines") }, + 16: { name: "asfAllowTrustLineClawback", words: () => say("bg.xrpl.asf.clawback"), caution: true }, + 17: { name: "asfAllowTrustLineLocking", words: () => say("bg.xrpl.asf.lockTokens") }, +}; + +/** AccountSet tf* flags: [bit, turns on?, the asf value it stands for]. */ +const ACCOUNTSET_TF: [number, boolean, number][] = [ + [0x00010000, true, 1], + [0x00020000, false, 1], + [0x00040000, true, 2], + [0x00080000, false, 2], + [0x00100000, true, 3], + [0x00200000, false, 3], +]; + +/** Seconds between the Unix epoch and the Ripple epoch (2000-01-01T00:00:00Z). */ +export const RIPPLE_EPOCH = 946684800; + +export function rippleTime(t: unknown): string { + return typeof t === "number" ? new Date((t + RIPPLE_EPOCH) * 1000).toISOString().replace(".000Z", " UTC").replace("T", " ") : String(t); +} + +/* ------------------------------------------------------------------ amounts */ + +export interface Amt { + asset: AssetRef; + /** Base units (drops, or the asset's decimals). */ + units: bigint; + xrp: boolean; + /** Issued tokens: the currency and issuer as the ledger writes them. */ + currency?: string; + issuer?: string; +} + +export function amountOf(networkId: NetworkId, v: unknown): Amt | null { + if (typeof v === "string" && /^\d+$/.test(v)) return { asset: xrpAsset(networkId), units: BigInt(v), xrp: true }; + if (isObj(v) && typeof v.currency === "string" && typeof v.issuer === "string" && typeof v.value === "string") { + const asset = tokenAsset(networkId, v.currency, v.issuer); + try { + return { asset, units: decimalToUnits(v.value, asset.decimals), xrp: false, currency: v.currency, issuer: v.issuer }; + } catch { + return null; + } + } + return null; +} + +export function amountText(a: Amt): string { + return `${formatUnits(a.units < 0n ? -a.units : a.units, a.asset.decimals)} ${a.asset.symbol}`; +} + +const xrpText = (drops: bigint) => `${formatUnits(drops, 6)} XRP`; + +function tokenLabel(a: Amt): string { + return a.xrp ? "XRP" : `${a.asset.symbol} · ${a.issuer}`; +} + +const danger = (code: Warning["code"], message: string): Warning => ({ level: "danger", code, message }); +const caution = (code: Warning["code"], message: string): Warning => ({ level: "caution", code, message }); +const info = (code: Warning["code"], message: string): Warning => ({ level: "info", code, message }); + +const spamWarning = (a: Amt): Warning[] => (a.asset.spam ? [caution("known-scam", say("bg.warn.spamToken", { symbol: a.asset.symbol }))] : []); + +function memoLines(tx: Record): Line[] { + if (!Array.isArray(tx.Memos)) return []; + const out: Line[] = []; + for (const m of tx.Memos) { + const memo = isObj(m) && isObj(m.Memo) ? m.Memo : null; + if (!memo) continue; + const data = typeof memo.MemoData === "string" ? memo.MemoData : ""; + const text = textOfHex(data); + if (text !== null) out.push({ label: "Memo", value: text.length > 200 ? `${text.slice(0, 197)}…` : text }); + else if (data) out.push({ label: "Data", value: `0x${data.slice(0, 64).toLowerCase()}${data.length > 64 ? "…" : ""}` }); + } + return out; +} + +/* ------------------------------------------------------------------ the transaction */ + +const CHANGES = (...c: (BalanceChange | null)[]) => c.filter((x): x is BalanceChange => !!x); +const change = (a: Amt | null, sign: 1n | -1n): BalanceChange | null => (a && a.units !== 0n ? { asset: a.asset, delta: (a.units * sign).toString() } : null); + +/** Reads what this transaction does. `blind` when it's a type Clip Wallet can't explain. */ +export async function describeTx(tx: Record, dc: DescribeContext): Promise { + const type = String(tx.TransactionType); + const flags = typeof tx.Flags === "number" ? tx.Flags : 0; + const lines: Line[] = []; + const warnings: Warning[] = []; + let changes: BalanceChange[] = []; + let title: string; + const amt = (v: unknown) => amountOf(dc.networkId, v); + + switch (type) { + case "Payment": { + const a = amt(tx.Amount); + const dest = String(tx.Destination); + if (!a) return blindOf(tx); + const sendMax = tx.SendMax !== undefined ? amt(tx.SendMax) : null; + const deliverMin = tx.DeliverMin !== undefined ? amt(tx.DeliverMin) : null; + const partial = (flags & TF.partialPayment) !== 0; + const cross = sendMax && sendMax.asset.key !== a.asset.key; + lines.push({ label: "To", value: dest }); + if (tx.DestinationTag !== undefined) lines.push({ label: say("bg.xrpl.label.destinationTag"), value: String(tx.DestinationTag) }); + if (!a.xrp) lines.push({ label: "Token", value: tokenLabel(a) }); + if (cross && sendMax) { + title = say("bg.xrpl.payUpTo", { amount: amountText(sendMax), to: short(dest), get: amountText(a) }); + lines.push({ label: "You pay", value: say("bg.xrpl.atMost", { amount: amountText(sendMax) }) }); + } else { + title = say("bg.req.sendTo", { amount: amountText(a), to: short(dest) }); + } + if (partial) { + lines.push({ label: say("bg.xrpl.label.deliversAtLeast"), value: deliverMin ? amountText(deliverMin) : say("bg.xrpl.anyAmount") }); + warnings.push(danger("simulation-failed", say("bg.xrpl.partialPayment", { amount: amountText(a) }))); + } + const out = cross || partial ? (sendMax ?? a) : a; + if (dest !== dc.me) changes = CHANGES(change(out, -1n)); + else if (!cross && !partial && a.xrp) warnings.push(danger("simulation-failed", say("bg.xrpl.xrpToSelf"))); + warnings.push(...spamWarning(a)); + if (dest !== dc.me) warnings.push(...(await destinationWarnings(dest, a, tx.DestinationTag !== undefined, dc))); + break; + } + case "TrustSet": { + const limit = isObj(tx.LimitAmount) ? amt(tx.LimitAmount) : null; + if (!limit || limit.xrp) return blindOf(tx); + lines.push({ label: "Token", value: limit.asset.symbol }, { label: say("bg.xrpl.label.issuer"), value: String(limit.issuer) }); + if (limit.units === 0n) { + title = say("bg.req.removeFromYourAccount", { symbol: limit.asset.symbol }); + lines.push({ label: "Also", value: say("bg.xrpl.frees", { amount: dc.reserves ? xrpText(dc.reserves.inc) : "0.2 XRP" }) }); + } else { + title = say("bg.req.addToYourAccount", { symbol: limit.asset.symbol }); + lines.push({ label: "Limit", value: amountText(limit) }); + lines.push({ label: "Locks", value: say("bg.xrpl.locksReserve", { amount: dc.reserves ? xrpText(dc.reserves.inc) : "0.2 XRP" }) }); + warnings.push(...spamWarning(limit)); + } + if (flags & (TF.setFreeze | TF.setDeepFreeze)) warnings.push(caution("unknown-call", say("bg.xrpl.freezesLine"))); + break; + } + case "OfferCreate": { + const give = amt(tx.TakerGets); + const get = amt(tx.TakerPays); + if (!give || !get) return blindOf(tx); + title = say("bg.req.trade", { give: amountText(give), get: amountText(get) }); + lines.push({ label: "You pay", value: say("bg.xrpl.atMost", { amount: amountText(give) }) }, { label: "You get", value: amountText(get) }); + if (tx.OfferSequence !== undefined) lines.push({ label: say("bg.xrpl.label.replacesOffer"), value: `#${tx.OfferSequence}` }); + if (tx.Expiration !== undefined) lines.push({ label: "Expires", value: rippleTime(tx.Expiration) }); + const kind = flags & TF.offerFok ? say("bg.xrpl.offer.fillOrKill") : flags & TF.offerIoc ? say("bg.xrpl.offer.immediate") : say("bg.xrpl.offer.standing"); + lines.push({ label: "Kind", value: kind }); + changes = CHANGES(change(give, -1n), change(get, 1n)); + warnings.push(...spamWarning(give), ...spamWarning(get)); + break; + } + case "OfferCancel": + title = say("bg.xrpl.cancelOffer"); + lines.push({ label: "Offer", value: `#${String(tx.OfferSequence)}` }); + break; + case "AccountSet": { + title = say("bg.xrpl.changeSettings"); + for (const [key, label] of [ + ["SetFlag", say("bg.xrpl.label.turnsOn")], + ["ClearFlag", say("bg.xrpl.label.turnsOff")], + ] as const) { + const v = tx[key]; + if (typeof v !== "number") continue; + const def = ASF[v]; + if (!def) return blindOf(tx); + lines.push({ label, value: def.words() }); + if (key === "SetFlag" && def.takeover) warnings.push(danger("account-takeover", say("bg.xrpl.disableMasterWarning"))); + if (key === "SetFlag" && def.caution) warnings.push(caution("unknown-call", say("bg.xrpl.issuerSettingWarning"))); + } + if (typeof tx.Domain === "string") lines.push({ label: say("bg.xrpl.label.domain"), value: textOfHex(tx.Domain) ?? (tx.Domain ? `0x${tx.Domain}` : say("bg.xrpl.cleared")) }); + if (tx.TransferRate !== undefined) { + const r = Number(tx.TransferRate); + lines.push({ label: say("bg.xrpl.label.transferFee"), value: r === 0 || r === 1e9 ? "0%" : `${((r - 1e9) / 1e7).toFixed(4).replace(/\.?0+$/, "")}%` }); + } + if (typeof tx.NFTokenMinter === "string") { + lines.push({ label: say("bg.xrpl.label.nftMinter"), value: tx.NFTokenMinter }); + warnings.push(caution("account-takeover", say("bg.xrpl.nftMinterWarning", { who: short(tx.NFTokenMinter) }))); + } + if (typeof tx.MessageKey === "string") lines.push({ label: "Key", value: tx.MessageKey ? `0x${tx.MessageKey.slice(0, 16)}…` : say("bg.xrpl.cleared") }); + if (tx.TickSize !== undefined) lines.push({ label: "Details", value: `TickSize ${tx.TickSize}` }); + // Legacy AccountSet transaction flags (tfRequireDestTag … tfAllowXRP) do the same as SetFlag/ClearFlag 1–3. + for (const [bit, on, asf] of ACCOUNTSET_TF) { + if (!(flags & bit)) continue; + lines.push({ label: on ? say("bg.xrpl.label.turnsOn") : say("bg.xrpl.label.turnsOff"), value: ASF[asf]!.words() }); + } + if (flags & ~ACCOUNTSET_TF.reduce((m, [b]) => m | b, 0x80000000) & 0xffffffff) return blindOf(tx); + break; + } + case "SetRegularKey": + if (typeof tx.RegularKey === "string") { + title = say("bg.req.giveControl", { who: short(tx.RegularKey) }); + lines.push({ label: "Key", value: tx.RegularKey }); + warnings.push(danger("account-takeover", say("bg.xrpl.regularKeyWarning", { who: short(tx.RegularKey) }))); + } else { + title = say("bg.xrpl.removeRegularKey"); + } + break; + case "SignerListSet": { + const entries = Array.isArray(tx.SignerEntries) ? tx.SignerEntries.map((e) => (isObj(e) && isObj(e.SignerEntry) ? e.SignerEntry : {})) : []; + if (!tx.SignerQuorum) { + title = say("bg.xrpl.removeSigners"); + break; + } + title = say("bg.xrpl.setSigners", { count: entries.length }); + for (const e of entries) lines.push({ label: "Account", value: `${String(e.Account)} (${String(e.SignerWeight)})` }); + lines.push({ label: say("bg.xrpl.label.quorum"), value: String(tx.SignerQuorum) }); + warnings.push(danger("account-takeover", say("bg.xrpl.signersWarning"))); + break; + } + case "AccountDelete": { + const dest = String(tx.Destination); + title = say("bg.xrpl.closeAccount", { to: short(dest) }); + lines.push({ label: "To", value: dest }); + if (tx.DestinationTag !== undefined) lines.push({ label: say("bg.xrpl.label.destinationTag"), value: String(tx.DestinationTag) }); + const fee = BigInt(typeof tx.Fee === "string" && /^\d+$/.test(tx.Fee) ? tx.Fee : "0"); + warnings.push(danger("account-closure", say("bg.xrpl.closeWarning", { to: short(dest), fee: xrpText(fee) }))); + if (dc.account) { + const left = BigInt(dc.account.Balance) - fee; + if (left > 0n) changes = [{ asset: xrpAsset(dc.networkId), delta: (-left).toString() }]; + } + warnings.push(...(await destinationWarnings(dest, null, tx.DestinationTag !== undefined, dc))); + break; + } + case "NFTokenMint": { + title = say("bg.xrpl.mintNft"); + const uri = typeof tx.URI === "string" ? (textOfHex(tx.URI) ?? `0x${tx.URI}`) : null; + if (uri) lines.push({ label: say("bg.xrpl.label.link"), value: uri }); + lines.push({ label: "Collection", value: say("bg.xrpl.taxon", { taxon: String(tx.NFTokenTaxon ?? 0) }) }); + if (tx.TransferFee !== undefined) lines.push({ label: say("bg.xrpl.label.royalty"), value: `${Number(tx.TransferFee) / 1000}%` }); + const props = [ + flags & TF.transferable ? say("bg.xrpl.nft.transferable") : say("bg.xrpl.nft.notTransferable"), + ...(flags & TF.burnable ? [say("bg.xrpl.nft.issuerCanBurn")] : []), + ...(flags & TF.mutable ? [say("bg.xrpl.nft.mutable")] : []), + ]; + lines.push({ label: "Details", value: props.join("; ") }); + if (typeof tx.Issuer === "string" && tx.Issuer !== dc.me) lines.push({ label: say("bg.xrpl.label.issuer"), value: tx.Issuer }); + if (tx.Amount !== undefined) { + const a = amt(tx.Amount); + if (!a) return blindOf(tx); + lines.push({ label: "Offer", value: amountText(a) }); + } + break; + } + case "NFTokenBurn": + title = say("bg.xrpl.burnNft"); + lines.push({ label: "NFT", value: String(tx.NFTokenID) }); + if (typeof tx.Owner === "string" && tx.Owner !== dc.me) lines.push({ label: "Owner", value: tx.Owner }); + warnings.push(caution("unknown-call", say("bg.warn.burnForGood"))); + break; + case "NFTokenCreateOffer": { + const a = amt(tx.Amount); + if (!a) return blindOf(tx); + const nft = String(tx.NFTokenID); + lines.push({ label: "NFT", value: nft }); + if (flags & TF.sellNft) { + if (a.units === 0n) { + title = typeof tx.Destination === "string" ? say("bg.xrpl.giveNft", { to: short(tx.Destination) }) : say("bg.xrpl.sellNft", { amount: amountText(a) }); + if (typeof tx.Destination !== "string") warnings.push(danger("new-recipient", say("bg.xrpl.freeForAnyone"))); + } else { + title = say("bg.xrpl.sellNft", { amount: amountText(a) }); + } + if (typeof tx.Destination === "string") lines.push({ label: "To", value: tx.Destination }); + } else { + title = say("bg.xrpl.buyNft", { amount: amountText(a) }); + if (typeof tx.Owner === "string") lines.push({ label: "Owner", value: tx.Owner }); + lines.push({ label: "Locks", value: say("bg.xrpl.offerHolds") }); + } + if (tx.Expiration !== undefined) lines.push({ label: "Expires", value: rippleTime(tx.Expiration) }); + warnings.push(...spamWarning(a)); + break; + } + case "NFTokenAcceptOffer": { + const ids = [tx.NFTokenSellOffer, tx.NFTokenBuyOffer].filter((x): x is string => typeof x === "string"); + title = say("bg.xrpl.acceptNftOffer"); + if (ids.length === 2) { + lines.push({ label: "Details", value: say("bg.xrpl.brokered") }); + if (tx.NFTokenBrokerFee !== undefined) { + const fee = amt(tx.NFTokenBrokerFee); + if (fee) lines.push({ label: "You get", value: amountText(fee) }); + } + break; + } + const offer = dc.rpc && ids[0] ? await nftOffer(dc.rpc, ids[0]) : null; + if (!offer) { + lines.push({ label: "Offer", value: ids[0] ?? "?" }); + warnings.push(caution("simulation-failed", say("bg.xrpl.offerUnknown"))); + break; + } + const a = amt(offer.Amount); + if (!a) return blindOf(tx); + lines.push({ label: "NFT", value: offer.NFTokenID }, { label: "From", value: offer.Owner }); + if (typeof tx.NFTokenSellOffer === "string") { + title = say("bg.xrpl.acceptNftSell", { amount: amountText(a) }); + changes = CHANGES(change(a, -1n)); + } else { + title = say("bg.xrpl.acceptNftBuy", { amount: amountText(a) }); + changes = CHANGES(change(a, 1n)); + } + warnings.push(...spamWarning(a)); + break; + } + case "NFTokenCancelOffer": { + const offers = Array.isArray(tx.NFTokenOffers) ? tx.NFTokenOffers : []; + title = say("bg.xrpl.cancelNftOffers", { count: offers.length }); + break; + } + case "EscrowCreate": { + const a = amt(tx.Amount); + if (!a) return blindOf(tx); + const dest = String(tx.Destination); + title = say("bg.xrpl.escrowCreate", { amount: amountText(a), to: short(dest) }); + lines.push({ label: "To", value: dest }); + if (tx.FinishAfter !== undefined) lines.push({ label: say("bg.xrpl.label.releaseAfter"), value: rippleTime(tx.FinishAfter) }); + if (tx.CancelAfter !== undefined) lines.push({ label: say("bg.xrpl.label.cancelAfter"), value: rippleTime(tx.CancelAfter) }); + if (tx.Condition !== undefined) lines.push({ label: "Details", value: say("bg.xrpl.escrowCondition") }); + warnings.push(caution("durable-nonce", say("bg.xrpl.escrowLocked"))); + if (dest !== dc.me) changes = CHANGES(change(a, -1n)); + break; + } + case "EscrowFinish": + case "EscrowCancel": + title = say(type === "EscrowFinish" ? "bg.xrpl.escrowFinish" : "bg.xrpl.escrowCancel"); + lines.push({ label: "Owner", value: String(tx.Owner) }, { label: "Details", value: `#${String(tx.OfferSequence)}` }); + break; + case "AMMDeposit": + case "AMMWithdraw": { + const pool = [tx.Asset, tx.Asset2].map((x) => (isObj(x) ? (x.currency === "XRP" ? "XRP" : currencyName(String(x.currency))) : "?")).join(" / "); + const a = tx.Amount !== undefined ? amt(tx.Amount) : null; + const b = tx.Amount2 !== undefined ? amt(tx.Amount2) : null; + if ((tx.Amount !== undefined && !a) || (tx.Amount2 !== undefined && !b)) return blindOf(tx); + lines.push({ label: "Pool", value: pool }); + if (type === "AMMDeposit") { + title = say("bg.xrpl.ammDeposit"); + for (const x of [a, b]) if (x) lines.push({ label: "You pay", value: say("bg.xrpl.atMost", { amount: amountText(x) }) }); + changes = CHANGES(change(a, -1n), change(b, -1n)); + } else { + title = say("bg.xrpl.ammWithdraw"); + for (const x of [a, b]) if (x) lines.push({ label: "You get", value: amountText(x) }); + if (!a && !b) lines.push({ label: "You get", value: say("bg.xrpl.yourShare") }); + } + warnings.push(caution("simulation-failed", say("bg.xrpl.poolAmounts"))); + break; + } + default: + return blindOf(tx); + } + + lines.push(...memoLines(tx)); + if (typeof tx.SourceTag === "number") lines.push({ label: say("bg.xrpl.label.sourceTag"), value: String(tx.SourceTag) }); + return { title, lines, balanceChanges: changes, warnings, blind: false }; +} + +export function blindOf(tx: Record): Described { + return { + title: say("bg.xrpl.unknownTx", { type: String(tx.TransactionType) }), + lines: [{ label: "Type", value: String(tx.TransactionType) }, ...memoLines(tx)], + balanceChanges: [], + warnings: [{ level: "danger", code: "blind-signing", message: "Clip Wallet can't read this transaction. Only sign it if you trust the app." }], + blind: true, + }; +} + +async function nftOffer(rpc: Rpc, index: string): Promise<{ Amount: unknown; NFTokenID: string; Owner: string; Flags: number } | null> { + try { + const r = await rpc.call<{ node?: { LedgerEntryType?: string; Amount: unknown; NFTokenID: string; Owner: string; Flags: number } }>("ledger_entry", { index, ledger_index: "validated" }); + return r.node?.LedgerEntryType === "NFTokenOffer" ? r.node : null; + } catch { + return null; + } +} + +/** What the ledger knows about the recipient: not activated, needs a tag, refuses XRP, has no trust line. */ +async function destinationWarnings(dest: string, a: Amt | null, hasTag: boolean, dc: DescribeContext): Promise { + if (!dc.rpc) return []; + const out: Warning[] = []; + let acct: AccountRoot | null; + try { + acct = await dc.rpc.accountInfo(dest); + } catch { + return [caution("simulation-failed", say("bg.xrpl.recipientUnchecked"))]; + } + const base = dc.reserves ? xrpText(dc.reserves.base) : "1 XRP"; + if (!acct) { + if (a && a.xrp && dc.reserves && a.units < dc.reserves.base) out.push(danger("simulation-failed", say("bg.xrpl.newAccountTooSmall", { reserve: base }))); + else if (a && a.xrp) out.push(info("new-recipient", say("bg.xrpl.opensAccount", { reserve: base }))); + else out.push(danger("simulation-failed", say("bg.xrpl.noRecipientAccount"))); + return out; + } + if (acct.Flags & LSF.requireDestTag && !hasTag) out.push(danger("memo-required", say("bg.xrpl.destinationTagNeeded"))); + if (a?.xrp && acct.Flags & LSF.disallowXrp) out.push(caution("new-recipient", say("bg.xrpl.disallowsXrp"))); + if (acct.Flags & LSF.depositAuth) out.push(caution("simulation-failed", say("bg.xrpl.depositAuth"))); + if (a && !a.xrp && a.issuer !== dest) { + try { + const lines = await dc.rpc.lines(dest, a.issuer); + const want = (a.currency ?? "").toUpperCase(); + if (!lines.some((l) => l.currency.toUpperCase() === want && l.account === a.issuer)) out.push(danger("simulation-failed", say("bg.xrpl.noTrustLine", { symbol: a.asset.symbol }))); + } catch { + /* unknown: no warning beyond the network's own answer */ + } + } + return out; +} diff --git a/packages/chains-xrpl/src/index.ts b/packages/chains-xrpl/src/index.ts new file mode 100644 index 00000000..b022b310 --- /dev/null +++ b/packages/chains-xrpl/src/index.ts @@ -0,0 +1,28 @@ +export { XRPL_METHODS, type XrplModuleOptions, type XrpSpendable, createXrplModule, normalize } from "./module.js"; +export { + RLUSD_CODE, + USDC_CODE, + XRPL_DEVNET, + XRPL_MAINNET, + XRPL_NETS, + XRPL_NETWORKS, + XRPL_TESTNET, + type XrplNet, + type XrplNetSpec, + currencyName, + explorerTxUrl, + fromChainId, + knownTokens, + netOf, + tokenAsset, + xrpAsset, +} from "./networks.js"; +export { addressOfPublicKey, decodeXAddress, encodeXAddress, isClassicAddress } from "./address.js"; +export { UnsupportedField, derSignature, serializeObject, signingDigest, signingPayload, txHash } from "./codec.js"; +export { ASF, RIPPLE_EPOCH, describeTx } from "./describe.js"; +export { XrplRpcError, plainXrplError } from "./rpc.js"; + +import { createXrplModule } from "./module.js"; + +/** Default instance. */ +export const xrplModule = createXrplModule(); diff --git a/packages/chains-xrpl/src/module.ts b/packages/chains-xrpl/src/module.ts new file mode 100644 index 00000000..baec3614 --- /dev/null +++ b/packages/chains-xrpl/src/module.ts @@ -0,0 +1,485 @@ +import { + type AssetRef, + type ChainContext, + type ChainModule, + ClipError, + type DappRequest, + type DecodedRequest, + type Network, + type Nft, + type Signature, + type SignablePayload, + type TokenBalance, + type Warning, + WALLET_ORIGIN, + msg, + say, +} from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { addressOfPublicKey, decodeClassic, decodeXAddress, isClassicAddress } from "./address.js"; +import { TX_TYPES, UnsupportedField, derSignature, serializeObject, signingDigest, txHash } from "./codec.js"; +import { type DescribeContext, LSF, blindOf, describeTx } from "./describe.js"; +import { fromChainId, specFor, tokenAsset, xrpAsset } from "./networks.js"; +import { type AccountRoot, type Reserves, type Rpc, XrplRpcError, plainXrplError, rpcFor } from "./rpc.js"; +import { decimalToUnits, formatUnits, fromHex, hex, hostOf, isObj, randomId, sleep } from "./util.js"; + +/** + * Request methods: XLS-72d (Browser Wallet Standard, https://github.com/XRPLF/XRPL-Standards/discussions/206; types + * in @xrpl-wallet-standard/core, https://github.com/tequdev/xrpl-wallet-standard). Params are the feature's input + * minus the WalletAccount object: { tx_json, account: "r…", network: "xrpl:1" | "xrpl:testnet", options? }. + * - xrpl:signTransaction → { signed_tx_blob } + * - xrpl:signAndSubmitTransaction → { tx_hash, tx_json } (tx_json = the `tx` answer once validated) + * buildTransfer uses xrpl:signAndSubmitTransaction with WALLET_ORIGIN. + */ +export const XRPL_METHODS = { + signTransaction: "xrpl:signTransaction", + signAndSubmitTransaction: "xrpl:signAndSubmitTransaction", +} as const; + +export interface XrplModuleOptions { + /** Delay between `tx` polls after submitting (default 1000 ms). */ + confirmPollMs?: number; + /** `tx` polls before giving up waiting for validation (default 20; the transaction is already submitted by then). */ + confirmAttempts?: number; + /** Highest fee autofill will set, in drops (default 100,000 = 0.1 XRP; AccountDelete always pays the owner reserve). */ + maxAutoFeeDrops?: bigint; +} + +/** XRP split into the part the reserve locks and the part you can spend (drops). */ +export interface XrpSpendable { + balance: string; + locked: string; + spendable: string; + /** Base reserve and per-object reserve, drops. */ + baseReserve: string; + ownerReserve: string; + ownerCount: number; +} + +interface Normalized { + tx: Record; + submit: boolean; + /** The dapp left Sequence / Fee / LastLedgerSequence for the wallet to fill. */ + needsFill: boolean; +} + +/** Fields only the signer adds (a request carrying them is already signed, or multi-signed). */ +const SIGNED_FIELDS = ["TxnSignature", "Signers"]; +/** LastLedgerSequence = current + this many ledgers (~1 minute), as xrpl.js autofill. */ +const LEDGER_OFFSET = 20; + +function paramsOf(request: DappRequest): { tx_json: Record; account?: unknown; network?: unknown; options?: Record } { + const p = request.params; + if (!isObj(p) || !isObj(p.tx_json)) throw new ClipError("This request from the app is malformed, so we stopped it.", "xrpl/bad-params"); + return p as { tx_json: Record }; +} + +export function normalize(request: DappRequest, ctx: ChainContext): Normalized { + if (request.method !== XRPL_METHODS.signTransaction && request.method !== XRPL_METHODS.signAndSubmitTransaction) { + throw new ClipError("Clip Wallet doesn't support this kind of request yet.", "xrpl/unsupported-method"); + } + const spec = specFor(request.networkId); + const p = paramsOf(request); + if (!spec || request.networkId !== ctx.network.id || (p.network !== undefined && fromChainId(String(p.network)) !== ctx.network.id)) { + throw new ClipError(msg("bg.xrpl.err.wrongNetwork"), "xrpl/network-mismatch"); + } + if (isObj(p.options) && p.options.multisig) throw new ClipError(msg("bg.xrpl.err.multisig"), "xrpl/unsupported"); + const me = ctx.account.address; + const named = isObj(p.account) ? p.account.address : p.account; + const tx: Record = { ...p.tx_json }; + if (tx.Account === undefined) tx.Account = me; + if ((named !== undefined && named !== me) || tx.Account !== me) { + throw new ClipError("This request is for a different account than the one you're using.", "xrpl/wrong-account"); + } + if (SIGNED_FIELDS.some((k) => tx[k] !== undefined)) throw new ClipError(msg("bg.xrpl.err.alreadySigned"), "xrpl/bad-params"); + const pub = ctx.account.publicKey.toUpperCase(); + if (typeof tx.SigningPubKey === "string" && tx.SigningPubKey !== "" && tx.SigningPubKey.toUpperCase() !== pub) { + throw new ClipError("This request is for a different account than the one you're using.", "xrpl/wrong-account"); + } + if (tx.SigningPubKey === "") throw new ClipError(msg("bg.xrpl.err.multisig"), "xrpl/unsupported"); + tx.SigningPubKey = pub; + // NetworkID must be present above network 1024 and absent at or below it (rippled telREQUIRES_NETWORK_ID / + // telNETWORK_ID_MAKES_TX_NON_CANONICAL). + if (tx.NetworkID !== undefined) { + if (Number(tx.NetworkID) !== spec.networkId) throw new ClipError(msg("bg.xrpl.err.wrongNetwork"), "xrpl/network-mismatch"); + if (spec.networkId <= 1024) delete tx.NetworkID; + } else if (spec.networkId > 1024) { + tx.NetworkID = spec.networkId; + } + // API v2 spells Payment.Amount "DeliverMax" in JSON; the binary field is Amount. + if (tx.DeliverMax !== undefined) { + if (tx.Amount !== undefined && JSON.stringify(tx.Amount) !== JSON.stringify(tx.DeliverMax)) throw new ClipError("This request from the app is malformed, so we stopped it.", "xrpl/bad-params"); + tx.Amount = tx.DeliverMax; + delete tx.DeliverMax; + } + const needsFill = tx.Sequence === undefined || tx.Fee === undefined || tx.LastLedgerSequence === undefined; + return { tx, submit: request.method === XRPL_METHODS.signAndSubmitTransaction, needsFill }; +} + +function unsupported(e: unknown): never { + if (e instanceof UnsupportedField) throw new ClipError(msg("bg.xrpl.err.cantRead", { field: e.field }), "xrpl/unreadable-operation", e); + throw e; +} + +export function createXrplModule(options: XrplModuleOptions = {}): ChainModule & { + normalize: typeof normalize; + spendable(ctx: ChainContext): Promise; + buildTrustLine(p: { asset: AssetRef }, ctx: ChainContext): Promise; +} { + const pollMs = options.confirmPollMs ?? 1000; + const attempts = options.confirmAttempts ?? 20; + const maxAutoFee = options.maxAutoFeeDrops ?? 100_000n; + + /** + * The transaction as signed, per request: autofill reads the ledger (sequence, fee, ledger index), so decode, + * prepare and finalize must all use the one decode filled in, or the signature wouldn't match what was shown. + */ + const filled = new Map>(); + const remember = (id: string, tx: Record) => { + filled.set(id, tx); + if (filled.size > 64) filled.delete(filled.keys().next().value!); + }; + + async function fill(n: Normalized, rpc: Rpc, account: AccountRoot | null, reserves: Reserves | null): Promise> { + const tx = { ...n.tx }; + if (!n.needsFill) return tx; + if (tx.Sequence === undefined) { + if (tx.TicketSequence !== undefined) tx.Sequence = 0; + else if (!account) throw notActivated(reserves); + else tx.Sequence = account.Sequence; + } + if (tx.Fee === undefined || tx.LastLedgerSequence === undefined) { + const fee = await rpc.fee(); + if (tx.Fee === undefined) { + if (tx.TransactionType === "AccountDelete") tx.Fee = (reserves ?? (await rpc.reserves())).inc.toString(); + else { + const want = fee.openLedger > fee.base ? fee.openLedger : fee.base; + tx.Fee = (want > maxAutoFee ? maxAutoFee : want).toString(); + } + } + if (tx.LastLedgerSequence === undefined) tx.LastLedgerSequence = fee.ledger + LEDGER_OFFSET; + } + return tx; + } + + function notActivated(reserves: Reserves | null): ClipError { + return new ClipError(msg("bg.xrpl.err.notActivated", { reserve: formatUnits(reserves?.base ?? 1_000_000n, 6) }), "xrpl/not-activated"); + } + + async function decode(req: DappRequest, ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const rpc = rpcFor(ctx); + const me = ctx.account.address; + const xrp = xrpAsset(ctx.network.id); + const [account, reserves] = await Promise.all([rpc.accountInfo(me).catch(() => undefined), rpc.reserves().catch(() => null)]); + const warnings: Warning[] = []; + let tx = n.tx; + try { + tx = await fill(n, rpc, account ?? null, reserves); + remember(req.id, tx); + } catch (e) { + if (!(e instanceof ClipError)) throw e; + warnings.push({ level: "danger", code: "simulation-failed", message: e.userMessage, ...(e.msg ? { msg: e.msg } : {}) }); + } + // A type or field this codec can't serialise can't be signed either: blind, and the reason says so. + let unreadable: string | null = TX_TYPES[String(tx.TransactionType)] === undefined ? String(tx.TransactionType) : null; + if (!unreadable) { + try { + serializeObject(tx); + } catch (e) { + if (!(e instanceof UnsupportedField)) throw e; + unreadable = e.field; + } + } + if (unreadable) { + const m = msg("bg.xrpl.err.cantRead", { field: unreadable }); + warnings.push({ level: "danger", code: "blind-signing", message: m.fallback, msg: m }); + } + if (account && account.Flags & LSF.disableMaster) { + throw new ClipError(msg("bg.xrpl.err.otherKey"), "xrpl/master-disabled"); + } + const dc: DescribeContext = { networkId: ctx.network.id, me, rpc, reserves, account: account ?? null }; + const d = unreadable ? blindOf(tx) : await describeTx(tx, dc); + const fee = typeof tx.Fee === "string" && /^\d+$/.test(tx.Fee) ? BigInt(tx.Fee) : 0n; + const lines = [...d.lines]; + if (tx.Fee !== undefined) lines.push({ label: "Network fee", value: `${formatUnits(fee, 6)} XRP` }); + if (fee > maxAutoFee && tx.TransactionType !== "AccountDelete") warnings.push({ level: "caution", code: "high-fee", message: say("bg.xrpl.highFee", { fee: `${formatUnits(fee, 6)} XRP` }) }); + if (account && reserves) { + const locked = reserves.base + reserves.inc * BigInt(account.OwnerCount); + const out = d.balanceChanges.filter((c) => c.asset.key === "xrp").reduce((s, c) => s - BigInt(c.delta), 0n); + const balance = BigInt(account.Balance); + if (tx.TransactionType !== "AccountDelete" && out + fee > 0n && out + fee > balance - locked) { + warnings.push({ level: "danger", code: "simulation-failed", message: say("bg.xrpl.notEnoughXrp", { reserve: `${formatUnits(locked, 6)} XRP` }) }); + } + } + if (!n.submit) lines.push({ label: "Sent by", value: say("bg.xrpl.appSends", { host: hostOf(req.origin) }) }); + return { + requestId: req.id, + networkId: req.networkId, + title: d.title, + lines, + balanceChanges: d.balanceChanges, + ...(tx.Fee !== undefined ? { fee: { asset: xrp, amount: fee.toString() } } : {}), + simulated: false, + blind: d.blind, + warnings: [...d.warnings, ...warnings], + }; + } + + async function txFor(req: DappRequest, ctx: ChainContext): Promise> { + const n = normalize(req, ctx); + const cached = filled.get(req.id); + if (cached) return cached; + if (n.needsFill) { + const rpc = rpcFor(ctx); + const tx = await fill(n, rpc, await rpc.accountInfo(ctx.account.address), null); + remember(req.id, tx); + return tx; + } + return n.tx; + } + + async function prepare(req: DappRequest, ctx: ChainContext, approvalId: string): Promise { + const tx = await txFor(req, ctx); + let bytes: Uint8Array; + try { + bytes = signingDigest(tx); + } catch (e) { + unsupported(e); + } + return [{ accountId: ctx.account.id, scheme: "ecdsa-secp256k1", bytes, approvalId }]; + } + + async function confirm(rpc: Rpc, hash: string): Promise | null> { + let last: Record | null = null; + for (let k = 0; k < attempts; k++) { + try { + last = await rpc.call>("tx", { transaction: hash, binary: false, api_version: 2 }); + if (last.validated === true) return last; + } catch (e) { + if (!(e instanceof XrplRpcError) || e.error !== "txnNotFound") throw e; + } + if (pollMs > 0) await sleep(pollMs); + } + return last; + } + + async function finalize(req: DappRequest, signatures: Signature[], ctx: ChainContext): Promise { + const n = normalize(req, ctx); + const tx = filled.get(req.id) ?? (n.needsFill ? null : n.tx); + if (!tx) throw new ClipError("This request expired. Please try again from the app.", "xrpl/not-prepared"); + const sig = signatures[0]; + const digest = signingDigest(tx); + const pub = fromHex(ctx.account.publicKey); + if (signatures.length !== 1 || !sig || sig.scheme !== "ecdsa-secp256k1" || sig.bytes.length !== 64 || !secp256k1.verify(sig.bytes, digest, pub, { prehash: false })) { + throw new ClipError("The signature didn't match. Nothing was sent.", "xrpl/bad-signature"); + } + const signed = { ...tx, TxnSignature: hex(derSignature(sig.bytes)).toUpperCase() }; + const blob = serializeObject(signed); + const blobHex = hex(blob).toUpperCase(); + const hash = txHash(blob); + filled.delete(req.id); + if (!n.submit) return { signed_tx_blob: blobHex }; + + const rpc = rpcFor(ctx); + let engine: string; + try { + const r = await rpc.call<{ engine_result: string; engine_result_message?: string; accepted?: boolean }>("submit", { tx_blob: blobHex }); + engine = r.engine_result; + } catch (e) { + if (e instanceof ClipError) throw e; + throw new ClipError("The network rejected this. Nothing was sent.", "xrpl/send-failed", e); + } + // tes/tec/terQUEUED: in the ledger (or queued for it); tem/tef/tel/ter: not applied, nothing sent. + if (!(engine === "tesSUCCESS" || engine === "terQUEUED" || engine.startsWith("tec"))) { + throw new ClipError(plainXrplError(engine), "xrpl/send-failed"); + } + const result = await confirm(rpc, hash); + const meta = result && isObj(result.meta) ? result.meta : null; + const code = meta && typeof meta.TransactionResult === "string" ? meta.TransactionResult : null; + if (result?.validated === true && code && code !== "tesSUCCESS") { + throw new ClipError(`${plainXrplError(code)} ${say("bg.xrpl.feeCharged")}`, "xrpl/failed"); + } + return { tx_hash: hash, tx_json: result ?? { hash, validated: false, tx_json: signed } }; + } + + async function spendableOf(rpc: Rpc, me: string): Promise<{ account: AccountRoot | null; reserves: Reserves; balance: bigint; locked: bigint; spendable: bigint }> { + const [account, reserves] = await Promise.all([rpc.accountInfo(me), rpc.reserves()]); + const balance = account ? BigInt(account.Balance) : 0n; + const locked = account ? reserves.base + reserves.inc * BigInt(account.OwnerCount) : 0n; + return { account, reserves, balance, locked, spendable: balance > locked ? balance - locked : 0n }; + } + + async function getBalances(ctx: ChainContext): Promise { + const rpc = rpcFor(ctx); + const me = ctx.account.address; + const account = await rpc.accountInfo(me); + const out: TokenBalance[] = [{ asset: xrpAsset(ctx.network.id), amount: account ? account.Balance : "0" }]; + if (!account) return out; + for (const l of await rpc.lines(me)) { + const asset = tokenAsset(ctx.network.id, l.currency, l.account); + let units: bigint; + try { + units = decimalToUnits(l.balance, asset.decimals); + } catch { + continue; + } + if (units < 0n) continue; // we issued it: an obligation, not a holding + out.push({ asset, amount: units.toString() }); + } + return out; + } + + /** NFTs (XLS-20) need an `Nft.standard` value core doesn't have yet ("xls20"), so none are listed. */ + async function getNfts(_ctx: ChainContext): Promise { + return []; + } + + function parseRecipient(to: string, net: Network): { address: string; tag: number | null } { + const t = to.trim(); + if (isClassicAddress(t)) return { address: t, tag: null }; + const x = decodeXAddress(t); + if (x) { + if (x.test !== net.testnet) throw new ClipError(msg("bg.xrpl.err.xAddressNetwork"), "xrpl/network-mismatch"); + return { address: x.classic, tag: x.tag }; + } + throw new ClipError(msg("bg.xrpl.err.badAddress"), "xrpl/bad-address"); + } + + function request(ctx: ChainContext, tx: Record): DappRequest { + return { + id: randomId(), + origin: WALLET_ORIGIN, + via: "injected", + family: "xrpl", + networkId: ctx.network.id, + method: XRPL_METHODS.signAndSubmitTransaction, + params: { tx_json: tx, account: ctx.account.address, network: ctx.network.id }, + }; + } + + async function base(rpc: Rpc, ctx: ChainContext, mine: { account: AccountRoot | null; reserves: Reserves }, type: string) { + if (!mine.account) throw notActivated(mine.reserves); + if (mine.account.Flags & LSF.disableMaster) throw new ClipError(msg("bg.xrpl.err.otherKey"), "xrpl/master-disabled"); + const fee = await rpc.fee(); + const want = fee.openLedger > fee.base ? fee.openLedger : fee.base; + return { + TransactionType: type, + Account: ctx.account.address, + Fee: (want > maxAutoFee ? maxAutoFee : want).toString(), + Sequence: mine.account.Sequence, + LastLedgerSequence: fee.ledger + LEDGER_OFFSET, + SigningPubKey: ctx.account.publicKey.toUpperCase(), + Flags: 0, + }; + } + + function tokenOf(asset: AssetRef): { currency: string; issuer: string } { + const [currency, issuer] = (asset.address ?? "").split("."); + if (!currency || !issuer || !decodeClassic(issuer)) throw new ClipError(msg("bg.xrpl.err.notToken"), "xrpl/bad-asset"); + return { currency, issuer }; + } + + async function buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise { + const me = ctx.account.address; + const { address: to, tag } = parseRecipient(p.to, ctx.network); + if (to === me) throw new ClipError("That's your own address.", "xrpl/self-transfer"); + if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) throw new ClipError("Enter an amount greater than zero.", "xrpl/bad-amount"); + const amount = BigInt(p.amount); + const rpc = rpcFor(ctx); + const mine = await spendableOf(rpc, me); + const tx: Record = await base(rpc, ctx, mine, "Payment"); + const fee = BigInt(tx.Fee as string); + const dest = await rpc.accountInfo(to); + const reserve = formatUnits(mine.reserves.base, 6); + if (dest && dest.Flags & LSF.requireDestTag && tag === null) throw new ClipError(msg("bg.xrpl.err.tagNeeded"), "xrpl/memo-required"); + + if (!p.asset.address) { + if (amount + fee > mine.spendable) { + throw new ClipError(msg("bg.xrpl.err.notEnoughXrp", { reserve: formatUnits(mine.locked, 6), fee: formatUnits(fee, 6) }), "xrpl/insufficient-funds"); + } + if (!dest && amount < mine.reserves.base) throw new ClipError(msg("bg.xrpl.err.newAccount", { reserve }), "xrpl/too-small"); + tx.Destination = to; + tx.Amount = amount.toString(); + } else { + const { currency, issuer } = tokenOf(p.asset); + const ref = tokenAsset(ctx.network.id, currency, issuer); + const value = formatUnits(amount, ref.decimals); + if (fee > mine.spendable) throw new ClipError(msg("bg.xrpl.err.feeOnly"), "xrpl/insufficient-funds"); + if (issuer !== me) { + const held = (await rpc.lines(me, issuer)).find((l) => l.currency.toUpperCase() === currency.toUpperCase()); + if (!held || decimalToUnits(held.balance, ref.decimals) < amount) throw new ClipError(msg("bg.err.notEnough", { symbol: ref.symbol }), "xrpl/insufficient-token"); + } + if (!dest) throw new ClipError(msg("bg.xrpl.noRecipientAccount"), "xrpl/bad-recipient"); + if (to !== issuer) { + const theirs = (await rpc.lines(to, issuer)).find((l) => l.currency.toUpperCase() === currency.toUpperCase()); + if (!theirs) throw new ClipError(msg("bg.xrpl.err.noTrustLine", { symbol: ref.symbol }), "xrpl/no-trust-line"); + } + tx.Destination = to; + tx.Amount = { currency, issuer, value }; + // An issuer's transfer fee (TransferRate, 1e9 = none) comes out of the sender: allow for it with SendMax. + if (issuer !== me && to !== issuer) { + const iss = await rpc.accountInfo(issuer); + const rate = BigInt((iss as { TransferRate?: number } | null)?.TransferRate ?? 0); + if (rate > 1_000_000_000n) tx.SendMax = { currency, issuer, value: formatUnits((amount * rate + 999_999_999n) / 1_000_000_000n, ref.decimals) }; + } + } + if (tag !== null) tx.DestinationTag = tag; + return request(ctx, tx); + } + + /** Adds RLUSD, USDC or another token to the account (TrustSet with a high limit; locks one owner reserve). */ + async function buildTrustLine(p: { asset: AssetRef }, ctx: ChainContext): Promise { + const { currency, issuer } = tokenOf(p.asset); + const rpc = rpcFor(ctx); + const mine = await spendableOf(rpc, ctx.account.address); + const tx: Record = await base(rpc, ctx, mine, "TrustSet"); + if (mine.spendable < mine.reserves.inc + BigInt(tx.Fee as string)) { + throw new ClipError(msg("bg.xrpl.err.reserveForToken", { amount: formatUnits(mine.reserves.inc, 6) }), "xrpl/insufficient-funds"); + } + tx.LimitAmount = { currency, issuer, value: "1000000000000" }; + tx.Flags = 0x00020000; // tfSetNoRipple, as wallets set it for holders + return request(ctx, tx); + } + + async function spendable(ctx: ChainContext): Promise { + const s = await spendableOf(rpcFor(ctx), ctx.account.address); + return { + balance: s.balance.toString(), + locked: s.locked.toString(), + spendable: s.spendable.toString(), + baseReserve: s.reserves.base.toString(), + ownerReserve: s.reserves.inc.toString(), + ownerCount: s.account?.OwnerCount ?? 0, + }; + } + + return { + family: "xrpl", + curve: "secp256k1", + /** xrpl.js Wallet.fromMnemonic (i = 0), Ledger Live, GemWallet: account i at m/44'/144'/i'/0/0. */ + derivationPath: (index: number) => `m/44'/144'/${index}'/0/0`, + addressFromPublicKey(publicKey: Uint8Array, _network: Network): string { + return addressOfPublicKey(publicKey); + }, + isAddress: (value: string) => isClassicAddress(value.trim()) || decodeXAddress(value.trim()) !== null, + /** Classic addresses work on every XRPL network; X-addresses say whether they are for mainnet or a test network. */ + networksForAddress(value: string, candidates: Network[]): Network[] { + const v = value.trim(); + const mine = candidates.filter((c) => c.family === "xrpl"); + if (isClassicAddress(v)) return mine; + const x = decodeXAddress(v); + return x ? mine.filter((c) => c.testnet === x.test) : []; + }, + getBalances, + getNfts, + decode, + prepare, + finalize, + buildTransfer, + buildTrustLine, + spendable, + normalize, + }; +} + diff --git a/packages/chains-xrpl/src/networks.ts b/packages/chains-xrpl/src/networks.ts new file mode 100644 index 00000000..e0fa23ba --- /dev/null +++ b/packages/chains-xrpl/src/networks.ts @@ -0,0 +1,159 @@ +import type { AssetRef, Network, NetworkId } from "@clip-wallet/core"; + +/** + * XRP Ledger networks. + * - CAIP-2 (ChainAgnostic namespaces, xrpl/caip2.md): "xrpl:" + the server's `network_id` — `xrpl:0` mainnet, + * `xrpl:1` testnet, `xrpl:2` devnet. XLS-72d (Browser Wallet Standard) uses the same ids and the aliases + * `xrpl:mainnet` / `xrpl:testnet` / `xrpl:devnet`. + * - Public JSON-RPC (no key), checked with `server_info` (network_id, validated ledger, reserves) in Oct 2026: + * mainnet xrplcluster.com (XRPL Labs), s1/s2.ripple.com:51234 (Ripple); testnet s.altnet.rippletest.net:51234 + * (Ripple), testnet.xrpl-labs.com; devnet s.devnet.rippletest.net:51234. + * - Reserves then: 1 XRP base, 0.2 XRP per owned object (read live from `server_info`, never hard-coded in sends). + */ +export type XrplNet = "mainnet" | "testnet" | "devnet"; + +export interface XrplNetSpec { + caip2: NetworkId; + networkId: number; + rpc: string[]; + explorer: string; + /** Faucet that funds an address (test networks only). */ + faucet?: string; + /** Ripple USD (RLUSD) issuer: docs.ripple.com / ripple.com/solutions/stablecoin, checked with gateway_balances. */ + rlusd?: string; + /** Circle USDC issuer (developers.circle.com "USDC contract addresses", XRPL), checked with gateway_balances. */ + usdc?: string; +} + +/** Currency codes as the ledger stores them (40 hex digits: the ASCII name, zero-padded). */ +export const RLUSD_CODE = "524C555344000000000000000000000000000000"; +export const USDC_CODE = "5553444300000000000000000000000000000000"; + +export const XRPL_NETS: Record = { + testnet: { + caip2: "xrpl:1", + networkId: 1, + // testnet.xrpl-labs.com first: it answers CORS preflights (pages, mobile WebViews); the rippled host does not. + rpc: ["https://testnet.xrpl-labs.com/", "https://s.altnet.rippletest.net:51234/"], + explorer: "https://testnet.xrpl.org", + faucet: "https://faucet.altnet.rippletest.net/accounts", + rlusd: "rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV", + usdc: "rHuGNhqTG32mfmAvWA8hUyWRLV3tCSwKQt", + }, + mainnet: { + caip2: "xrpl:0", + networkId: 0, + rpc: ["https://xrplcluster.com/", "https://s1.ripple.com:51234/", "https://s2.ripple.com:51234/"], + explorer: "https://livenet.xrpl.org", + rlusd: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", + usdc: "rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE", + }, + devnet: { + caip2: "xrpl:2", + networkId: 2, + rpc: ["https://s.devnet.rippletest.net:51234/"], + explorer: "https://devnet.xrpl.org", + faucet: "https://faucet.devnet.rippletest.net/accounts", + }, +}; + +/** 1 XRP = 1,000,000 drops. */ +export const XRP_DECIMALS = 6; +/** Issued tokens: RLUSD and USDC are shown with 6 decimals, other tokens with 15 (the ledger keeps 16 significant digits). */ +export const STABLE_DECIMALS = 6; +export const TOKEN_DECIMALS = 15; + +export function xrpAsset(networkId: NetworkId): AssetRef { + return { key: "xrp", symbol: "XRP", name: "XRP", decimals: XRP_DECIMALS, networkId }; +} + +function network(net: XrplNet): Network { + const c = XRPL_NETS[net]; + return { + id: c.caip2, + family: "xrpl", + name: net === "mainnet" ? "XRP Ledger" : net === "testnet" ? "XRP Ledger Testnet" : "XRP Ledger Devnet", + nativeAsset: xrpAsset(c.caip2), + testnet: net !== "mainnet", + rpcUrls: c.rpc, + explorerUrl: c.explorer, + }; +} + +export const XRPL_TESTNET = network("testnet"); +export const XRPL_MAINNET = network("mainnet"); +export const XRPL_DEVNET = network("devnet"); +export const XRPL_NETWORKS: Network[] = [XRPL_TESTNET, XRPL_MAINNET, XRPL_DEVNET]; + +const ALIASES: Record = { "xrpl:mainnet": "mainnet", "xrpl:testnet": "testnet", "xrpl:devnet": "devnet" }; + +/** "xrpl:1", "xrpl:testnet" (XLS-72d alias), "1" or 1 → the network, else null. */ +export function netOf(chain: string | number): XrplNet | null { + if (typeof chain === "string" && ALIASES[chain]) return ALIASES[chain]!; + const id = typeof chain === "number" ? chain : /^(?:xrpl:)?(\d+)$/.exec(chain)?.[1]; + for (const [name, c] of Object.entries(XRPL_NETS) as [XrplNet, XrplNetSpec][]) if (String(c.networkId) === String(id)) return name; + return null; +} + +/** The CAIP-2 id for any XLS-72d chain identifier this wallet knows, else null. */ +export function fromChainId(chain: string | number): NetworkId | null { + const n = netOf(chain); + return n ? XRPL_NETS[n].caip2 : null; +} + +export function specFor(networkId: NetworkId): XrplNetSpec | null { + const n = netOf(networkId); + return n ? XRPL_NETS[n] : null; +} + +/** Readable name of a ledger currency code ("USD", "RLUSD"; 40-hex ASCII names decoded; others shortened). */ +export function currencyName(code: string): string { + if (code.length !== 40) return code; + if (/^03/i.test(code)) return "LP token"; // XLS-30 AMM LP tokens + const bytes = code.match(/../g)!.map((h) => parseInt(h, 16)); + const end = bytes.findIndex((b, i) => b === 0 && bytes.slice(i).every((x) => x === 0)); + const text = bytes.slice(0, end < 0 ? 20 : end); + if (text.length && text.every((b) => b >= 0x20 && b < 0x7f)) return String.fromCharCode(...text); + return `${code.slice(0, 6)}…`; +} + +function looksLikeStable(name: string): boolean { + const c = name.toUpperCase().replace(/[^A-Z0-9]/g, ""); + return /^(US|U5)D/.test(c) || c.includes("USDC") || c.includes("RLUSD") || c.includes("USDT") || c === "XRP"; +} + +/** + * AssetRef for an issued token. `address` is "." (CAIP-19 xrpl `token:` reference). Ripple's RLUSD + * → key "rlusd", Circle's USDC → key "usdc"; anything else is `xrpl:.`, and an unknown issuer's + * token that looks like a dollar stablecoin or XRP is marked spam. + */ +export function tokenAsset(networkId: NetworkId, currency: string, issuer: string): AssetRef { + const spec = specFor(networkId); + const upper = currency.length === 40 ? currency.toUpperCase() : currency; + const isRlusd = !!spec?.rlusd && issuer === spec.rlusd && upper === RLUSD_CODE; + const isUsdc = !!spec?.usdc && issuer === spec.usdc && upper === USDC_CODE; + const symbol = isRlusd ? "RLUSD" : isUsdc ? "USDC" : currencyName(currency); + const asset: AssetRef = { + key: isRlusd ? "rlusd" : isUsdc ? "usdc" : `xrpl:${upper}.${issuer}`, + symbol, + name: isRlusd ? "Ripple USD" : isUsdc ? "USD Coin" : symbol, + decimals: isRlusd || isUsdc ? STABLE_DECIMALS : TOKEN_DECIMALS, + networkId, + address: `${upper}.${issuer}`, + }; + if (!isRlusd && !isUsdc && looksLikeStable(symbol)) asset.spam = true; + return asset; +} + +/** The known tokens on a network (for sends and the token list). */ +export function knownTokens(networkId: NetworkId): AssetRef[] { + const s = specFor(networkId); + const out: AssetRef[] = []; + if (s?.rlusd) out.push(tokenAsset(networkId, RLUSD_CODE, s.rlusd)); + if (s?.usdc) out.push(tokenAsset(networkId, USDC_CODE, s.usdc)); + return out; +} + +export function explorerTxUrl(net: Network, hash: string): string { + return `${net.explorerUrl}/transactions/${hash}`; +} diff --git a/packages/chains-xrpl/src/rpc.ts b/packages/chains-xrpl/src/rpc.ts new file mode 100644 index 00000000..ef8804e8 --- /dev/null +++ b/packages/chains-xrpl/src/rpc.ts @@ -0,0 +1,199 @@ +import { type ChainContext, ClipError } from "@clip-wallet/core"; + +/** + * rippled JSON-RPC over `ctx.fetch` (https://xrpl.org/docs/references/http-websocket-apis): POST { method, + * params: [ { … } ] } to each of the network's public servers in turn. A server that can't be reached or answers + * "slowDown"/"tooBusy"/"noNetwork" is skipped; an answer with `status: "error"` is the ledger's answer and is thrown + * as an XrplRpcError (its `error` code, e.g. "actNotFound"). + */ +export class XrplRpcError extends Error { + constructor( + public readonly error: string, + message?: string, + ) { + super(message || error); + this.name = "XrplRpcError"; + } +} + +const RETRY_ERRORS = new Set(["slowDown", "tooBusy", "noNetwork", "noCurrent", "noClosed", "amendmentBlocked", "notSynced", "highFee"]); + +export interface AccountRoot { + Account: string; + Balance: string; + Flags: number; + OwnerCount: number; + Sequence: number; + RegularKey?: string; +} + +export interface Reserves { + /** Drops. */ + base: bigint; + /** Drops per owned object. */ + inc: bigint; + /** Validated ledger index. */ + ledger: number; +} + +export interface TrustLine { + account: string; + balance: string; + currency: string; + limit: string; +} + +export interface Rpc { + call>(method: string, params?: Record): Promise; + /** null when the account doesn't exist (never funded, or deleted). */ + accountInfo(address: string): Promise; + reserves(): Promise; + /** Fee to get into the open ledger now, in drops (the `fee` command). */ + fee(): Promise<{ openLedger: bigint; base: bigint; ledger: number }>; + lines(address: string, peer?: string): Promise; +} + +const xrpToDrops = (x: unknown): bigint => { + const s = typeof x === "number" ? String(x) : typeof x === "string" ? x : ""; + const m = /^(\d+)(?:\.(\d{1,6}))?$/.exec(s); + if (!m) return 0n; + return BigInt(m[1]!) * 1_000_000n + BigInt((m[2] ?? "").padEnd(6, "0")); +}; + +const big = (x: unknown): bigint => (typeof x === "string" && /^\d+$/.test(x) ? BigInt(x) : typeof x === "number" && Number.isInteger(x) ? BigInt(x) : 0n); + +export function rpcFor(ctx: ChainContext): Rpc { + const urls = ctx.network.rpcUrls; + + async function call(method: string, params: Record = {}): Promise { + let last: unknown; + for (const url of urls) { + let body: { result?: Record }; + try { + const res = await ctx.fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method, params: [params] }) }); + if (!res.ok) { + last = new Error(`HTTP ${res.status}`); + continue; + } + body = (await res.json()) as typeof body; + } catch (e) { + last = e; + continue; + } + const r = body.result; + if (!r || typeof r !== "object") { + last = new Error("bad answer"); + continue; + } + if (r.status === "error") { + const code = String(r.error ?? "unknown"); + if (RETRY_ERRORS.has(code)) { + last = new XrplRpcError(code); + continue; + } + throw new XrplRpcError(code, typeof r.error_message === "string" ? r.error_message : undefined); + } + return r as T; + } + throw new ClipError("Couldn't reach the XRP Ledger. Check your connection and try again.", "xrpl/offline", last); + } + + return { + call, + async accountInfo(address) { + try { + const r = await call<{ account_data: AccountRoot }>("account_info", { account: address, ledger_index: "current" }); + return r.account_data; + } catch (e) { + if (e instanceof XrplRpcError && e.error === "actNotFound") return null; + throw e; + } + }, + async reserves() { + const r = await call<{ info: { validated_ledger?: { reserve_base_xrp?: unknown; reserve_inc_xrp?: unknown; seq?: number } } }>("server_info"); + const v = r.info.validated_ledger; + if (!v) throw new ClipError("The XRP Ledger server isn't in sync right now. Try again in a minute.", "xrpl/unavailable"); + return { base: xrpToDrops(v.reserve_base_xrp), inc: xrpToDrops(v.reserve_inc_xrp), ledger: Number(v.seq ?? 0) }; + }, + async fee() { + const r = await call<{ drops: { open_ledger_fee?: string; base_fee?: string; minimum_fee?: string }; ledger_current_index?: number }>("fee"); + const base = big(r.drops.base_fee) || 10n; + return { openLedger: big(r.drops.open_ledger_fee) || base, base, ledger: Number(r.ledger_current_index ?? 0) }; + }, + async lines(address, peer) { + const out: TrustLine[] = []; + let marker: unknown; + for (let page = 0; page < 10; page++) { + try { + const r = await call<{ lines: TrustLine[]; marker?: unknown }>("account_lines", { + account: address, + ledger_index: "validated", + limit: 400, + ...(peer ? { peer } : {}), + ...(marker ? { marker } : {}), + }); + out.push(...r.lines); + marker = r.marker; + } catch (e) { + if (e instanceof XrplRpcError && e.error === "actNotFound") return out; + throw e; + } + if (!marker) break; + } + return out; + }, + }; +} + +/** Engine results in plain words (https://xrpl.org/docs/references/protocol/transactions/transaction-results). */ +export function plainXrplError(code: string, reserve?: string): string { + switch (code) { + case "tecNO_DST": + case "tecNO_DST_INSUF_XRP": + return `The recipient's XRP Ledger account doesn't exist yet. Send at least ${reserve ?? "the base reserve"} XRP to open it.`; + case "tecUNFUNDED_PAYMENT": + case "tecUNFUNDED": + case "tecUNFUNDED_OFFER": + case "terINSUF_FEE_B": + return "You don't have enough for this. Part of your XRP has to stay in your account as a reserve."; + case "tecINSUFFICIENT_RESERVE": + case "tecINSUF_RESERVE_LINE": + case "tecINSUF_RESERVE_OFFER": + return "Your account needs more XRP in reserve for this. Each token, offer or NFT page locks a little XRP."; + case "tecDST_TAG_NEEDED": + return "The recipient needs a destination tag. Ask them for it (or for an X-address) and try again."; + case "tecPATH_DRY": + case "tecPATH_PARTIAL": + case "tecNO_LINE": + case "tecNO_LINE_INSUF_RESERVE": + return "The recipient can't receive this token: they haven't added it to their account (no trust line), or there's no way to deliver it."; + case "tecNO_PERMISSION": + case "tecNO_AUTH": + return "The recipient doesn't accept this from you (deposit authorization or token authorization is on)."; + case "tecFROZEN": + return "This token is frozen by its issuer, so it can't move right now."; + case "tecHAS_OBLIGATIONS": + case "tecTOO_SOON": + return "This account can't be closed yet: it still owns objects, or it was used too recently."; + case "tefPAST_SEQ": + case "tefMAX_LEDGER": + case "tefALREADY": + return "This transaction is out of date or was already sent. Check Activity before you try again."; + case "telINSUF_FEE_P": + case "terQUEUED": + return "The XRP Ledger is busy right now. Try again in a minute."; + case "temREDUNDANT": + return "This transaction doesn't change anything (for example, sending XRP to yourself)."; + case "temBAD_FEE": + case "temBAD_AMOUNT": + case "temBAD_CURRENCY": + case "temDST_IS_SRC": + return "The XRP Ledger says this transaction is malformed. Nothing was sent."; + case "tefBAD_AUTH": + case "tefBAD_AUTH_MASTER": + case "tefMASTER_DISABLED": + return "This account is controlled by another key, so Clip Wallet's signature isn't accepted."; + default: + return `The XRP Ledger rejected this (${code}). Nothing was sent.`; + } +} diff --git a/packages/chains-xrpl/src/util.ts b/packages/chains-xrpl/src/util.ts new file mode 100644 index 00000000..d618fdfe --- /dev/null +++ b/packages/chains-xrpl/src/util.ts @@ -0,0 +1,89 @@ +export function hex(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +} + +export function fromHex(h: string): Uint8Array { + const clean = h.replace(/^0x/, ""); + if (clean.length % 2 || /[^0-9a-f]/i.test(clean)) throw new Error("bad hex"); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); + return out; +} + +export function concat(...parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + let v = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = v / base; + v %= base; + const frac = decimals > 0 ? v.toString().padStart(decimals, "0").replace(/0+$/, "") : ""; + const out = frac ? `${whole}.${frac}` : `${whole}`; + return neg ? `-${out}` : out; +} + +/** + * A decimal string as XRPL JSON writes it ("1.5", "-0.25", "1e-7", "1234567890123456e-3") → base units with + * `decimals` places, truncated toward zero (never rounds up what someone holds). + */ +export function decimalToUnits(value: string, decimals: number): bigint { + const m = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(value.trim()); + if (!m || (!m[2] && !m[3])) throw new Error(`not a number: ${value}`); + const neg = m[1] === "-"; + const digits = `${m[2] ?? ""}${m[3] ?? ""}`.replace(/^0+(?=\d)/, "") || "0"; + const exp = Number(m[4] ?? 0) - (m[3]?.length ?? 0) + decimals; + let n = BigInt(digits); + if (exp >= 0) n *= 10n ** BigInt(exp); + else n /= 10n ** BigInt(-exp); + return neg ? -n : n; +} + +export function short(address: string): string { + return address.length > 12 ? `${address.slice(0, 4)}…${address.slice(-4)}` : address; +} + +export function randomId(): string { + const b = new Uint8Array(12); + crypto.getRandomValues(b); + return hex(b); +} + +export function hostOf(origin: string): string { + try { + return new URL(origin).host || origin; + } catch { + return origin; + } +} + +/** Printable UTF-8 text of hex bytes, or null. */ +export function textOfHex(h: string | undefined): string | null { + if (!h || !/^([0-9a-f]{2})+$/i.test(h)) return null; + try { + const s = new TextDecoder("utf-8", { fatal: true }).decode(fromHex(h)); + return /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(s) ? null : s; + } catch { + return null; + } +} + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export const isObj = (v: unknown): v is Record => !!v && typeof v === "object" && !Array.isArray(v); diff --git a/packages/chains-xrpl/test/codec.test.ts b/packages/chains-xrpl/test/codec.test.ts new file mode 100644 index 00000000..1d3f6ad4 --- /dev/null +++ b/packages/chains-xrpl/test/codec.test.ts @@ -0,0 +1,90 @@ +import { decode, encode, encodeForSigning } from "ripple-binary-codec"; +import { hashes } from "xrpl"; +import { describe, expect, it } from "vitest"; +import { UnsupportedField, derSignature, serializeObject, signingPayload, txHash, vlPrefix } from "../src/codec.js"; +import { decodeXAddress, encodeXAddress, isClassicAddress } from "../src/address.js"; +import { fromHex, hex } from "../src/util.js"; + +const ME = "rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"; +const BOB = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"; +const RLUSD = "rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV"; +const PUB = "031D68BC1A142E6766B2BDFB006CCFE135EF2E0E2E94ABB5CF5C9AB6104776FBAE"; +const base = { Account: ME, Fee: "12", Sequence: 7, LastLedgerSequence: 21322700, SigningPubKey: PUB, Flags: 0 }; + +/** The same JSON both ways: ripple-binary-codec (xrpl.js) and this package must produce identical bytes. */ +const CASES: Record>[] = [ + { xrpPayment: { ...base, TransactionType: "Payment", Destination: BOB, Amount: "1500000", DestinationTag: 42 } }, + { iouPayment: { ...base, TransactionType: "Payment", Destination: BOB, Amount: { currency: "524C555344000000000000000000000000000000", issuer: RLUSD, value: "2.5" }, Memos: [{ Memo: { MemoType: "746578742F706C61696E", MemoData: "68656C6C6F" } }, { Memo: { MemoData: "00" } }] } }, + { partial: { ...base, TransactionType: "Payment", Flags: 131072, Destination: BOB, Amount: { currency: "USD", issuer: RLUSD, value: "1000000" }, SendMax: "1000000000", DeliverMin: { currency: "USD", issuer: RLUSD, value: "1e-7" }, Paths: [[{ currency: "USD", issuer: RLUSD }], [{ account: BOB }, { currency: "XRP" }]] } }, + { trust: { ...base, TransactionType: "TrustSet", Flags: 131072, LimitAmount: { currency: "USD", issuer: RLUSD, value: "1000000000" }, QualityIn: 0 } }, + { offer: { ...base, TransactionType: "OfferCreate", TakerGets: "15000000", TakerPays: { currency: "USD", issuer: RLUSD, value: "-0.001234567890123456" }, Expiration: 800000000 } }, + { cancel: { ...base, TransactionType: "OfferCancel", OfferSequence: 5 } }, + { accountSet: { ...base, TransactionType: "AccountSet", SetFlag: 8, Domain: "6578616D706C652E636F6D", TransferRate: 1002000000, TickSize: 5, EmailHash: "98B4375E1D753E5B91627516F6D70977" } }, + { regularKey: { ...base, TransactionType: "SetRegularKey", RegularKey: BOB } }, + { signerList: { ...base, TransactionType: "SignerListSet", SignerQuorum: 2, SignerEntries: [{ SignerEntry: { Account: BOB, SignerWeight: 1 } }, { SignerEntry: { Account: RLUSD, SignerWeight: 1 } }] } }, + { del: { ...base, Fee: "200000", TransactionType: "AccountDelete", Destination: BOB, DestinationTag: 13 } }, + { mint: { ...base, TransactionType: "NFTokenMint", NFTokenTaxon: 0, Flags: 8, TransferFee: 500, URI: "697066733A2F2F62616679" } }, + { nftOffer: { ...base, TransactionType: "NFTokenCreateOffer", NFTokenID: "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65", Amount: "1000000", Flags: 1 } }, + { nftAccept: { ...base, TransactionType: "NFTokenAcceptOffer", NFTokenSellOffer: "68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77" } }, + { nftCancel: { ...base, TransactionType: "NFTokenCancelOffer", NFTokenOffers: ["68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"] } }, + { escrow: { ...base, TransactionType: "EscrowCreate", Destination: BOB, Amount: "10000", FinishAfter: 800000000, CancelAfter: 800086400, Condition: "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100" } }, + { amm: { ...base, TransactionType: "AMMDeposit", Flags: 1048576, Asset: { currency: "XRP" }, Asset2: { currency: "USD", issuer: RLUSD }, Amount: "1000", Amount2: { currency: "USD", issuer: RLUSD, value: "0.5" } } }, + { signed: { ...base, TransactionType: "Payment", Destination: BOB, Amount: "1", TxnSignature: "3045022100AB", NetworkID: 21338 } }, +]; + +describe("binary codec subset", () => { + for (const c of CASES) { + const [name, tx] = Object.entries(c)[0]!; + it(`matches ripple-binary-codec: ${name}`, () => { + expect(hex(serializeObject(tx)).toUpperCase()).toBe(encode(tx as never)); + expect(hex(signingPayload(tx)).toUpperCase()).toBe(encodeForSigning(tx as never)); + }); + } + + it("hashes signed blobs like xrpl.js", () => { + const blob = encode({ ...base, TransactionType: "Payment", Destination: BOB, Amount: "1", TxnSignature: "3045022100AB" } as never); + expect(txHash(fromHex(blob))).toBe(hashes.hashSignedTx(blob)); + expect(decode(blob).Destination).toBe(BOB); + }); + + it("writes VL lengths in 1, 2 and 3 bytes", () => { + expect(hex(vlPrefix(192))).toBe("c0"); + expect(hex(vlPrefix(193))).toBe("c100"); + expect(hex(vlPrefix(12480))).toBe("f0ff"); + expect(hex(vlPrefix(12481))).toBe("f10000"); + const big = { ...base, TransactionType: "AccountSet", Domain: "AB".repeat(300) }; + expect(hex(serializeObject(big)).toUpperCase()).toBe(encode(big as never)); + }); + + it("refuses what it can't serialise", () => { + expect(() => serializeObject({ ...base, TransactionType: "Payment", Delegate: BOB })).toThrow(UnsupportedField); + expect(() => serializeObject({ ...base, TransactionType: "CheckCreate" })).toThrow(UnsupportedField); + expect(() => serializeObject({ ...base, TransactionType: "Payment", Amount: { mpt_issuance_id: "00", value: "1" } })).toThrow(UnsupportedField); + expect(() => serializeObject({ ...base, TransactionType: "Payment", Amount: { currency: "USD", issuer: RLUSD, value: "1.23456789012345678" } })).toThrow(/significant/); + expect(() => serializeObject({ ...base, TransactionType: "Payment", Amount: "-1" })).toThrow(UnsupportedField); + expect(() => serializeObject({ ...base, Account: "X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ", TransactionType: "Payment" })).toThrow(UnsupportedField); + }); + + it("DER-encodes r||s", () => { + const rs = new Uint8Array(64); + rs[0] = 0x80; + rs[32] = 0x00; + rs[33] = 0x01; + const der = derSignature(rs); + expect(hex(der.subarray(0, 4))).toBe("30440221"); + expect(der[4]).toBe(0); + expect(der.length).toBe(2 + 2 + 33 + 2 + 31); + }); +}); + +describe("addresses", () => { + it("decodes and encodes X-addresses (ripple-address-codec vectors)", () => { + // xrpaddress.info / ripple-address-codec test vectors. + expect(decodeXAddress("X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ")).toEqual({ classic: "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", tag: null, test: false }); + expect(decodeXAddress("X7AcgcsBL6XDcUb289X4mJ8djcdyKaGZMhc9YTE92ehJ2Fu")).toEqual({ classic: "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", tag: 1, test: false }); + expect(decodeXAddress("T719a5UwUCnEs54UsxG9CJYYDhwmFCqkr7wxCcNcfZ6p5GZ")).toEqual({ classic: "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", tag: null, test: true }); + expect(encodeXAddress("r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", 1, false)).toBe("X7AcgcsBL6XDcUb289X4mJ8djcdyKaGZMhc9YTE92ehJ2Fu"); + expect(isClassicAddress(ME)).toBe(true); + expect(isClassicAddress(`${ME.slice(0, -1)}4`)).toBe(false); + }); +}); diff --git a/packages/chains-xrpl/test/fixtures.ts b/packages/chains-xrpl/test/fixtures.ts new file mode 100644 index 00000000..1e55c333 --- /dev/null +++ b/packages/chains-xrpl/test/fixtures.ts @@ -0,0 +1,29 @@ +/** + * The public "abandon … about" test account at m/44'/144'/0'/0/0 (what xrpl.js Wallet.fromMnemonic gives for the + * phrase) and the transactions the tests sign. Only public keys, addresses and transaction JSON live here. + */ +import { RLUSD_CODE } from "../src/networks.js"; + +export const ME = "rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"; +export const PUB = "031D68BC1A142E6766B2BDFB006CCFE135EF2E0E2E94ABB5CF5C9AB6104776FBAE"; +/** Arbitrary valid addresses nobody here holds keys for. */ +export const BOB = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"; +export const EXCHANGE = "rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv"; +export const NEWBIE = "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59"; +export const RLUSD_ISSUER = "rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV"; + +/** Ledger values the mocked servers answer with. */ +export const SEQ = 21000001; +export const LEDGER = 21322680; +export const LLS = LEDGER + 20; +export const OPEN_LEDGER_FEE = "12"; + +const filled = { Account: ME, Fee: OPEN_LEDGER_FEE, Sequence: SEQ, LastLedgerSequence: LLS, SigningPubKey: PUB, Flags: 0 }; + +/** buildTransfer: 1.5 XRP to BOB. */ +export const XRP_SEND = { TransactionType: "Payment", ...filled, Destination: BOB, Amount: "1500000" }; +/** buildTransfer: 2.5 RLUSD to BOB. */ +export const RLUSD_SEND = { TransactionType: "Payment", ...filled, Destination: BOB, Amount: { currency: RLUSD_CODE, issuer: RLUSD_ISSUER, value: "2.5" } }; +/** A dapp's AccountSet with a memo, left for the wallet to autofill (Sequence, Fee, LastLedgerSequence). */ +export const DAPP_UNFILLED = { TransactionType: "AccountSet", Account: ME, Memos: [{ Memo: { MemoData: "68656C6C6F20787270" } }] }; +export const DAPP_FILLED = { ...DAPP_UNFILLED, Sequence: SEQ, Fee: OPEN_LEDGER_FEE, LastLedgerSequence: LLS, SigningPubKey: PUB }; diff --git a/packages/chains-xrpl/test/helpers.ts b/packages/chains-xrpl/test/helpers.ts new file mode 100644 index 00000000..bd9dd6d1 --- /dev/null +++ b/packages/chains-xrpl/test/helpers.ts @@ -0,0 +1,61 @@ +import type { Account, ChainContext, SignablePayload, Signature } from "@clip-wallet/core"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { XRPL_TESTNET } from "../src/index.js"; +import { fromHex } from "../src/util.js"; +import { LEDGER, ME, OPEN_LEDGER_FEE, PUB, SEQ } from "./fixtures.js"; +import { SIGS } from "./signatures.js"; + +/** Vault stand-in: answers with the precomputed signature that verifies for the payload (verification only). */ +export const signer = { + sign(p: SignablePayload): Signature { + const sig = Object.values(SIGS) + .map(fromHex) + .find((s) => secp256k1.verify(s, p.bytes, fromHex(PUB), { prehash: false })); + if (!sig) throw new Error("no fixture signature for this payload"); + return { scheme: "ecdsa-secp256k1", bytes: sig, recovery: 0, publicKey: PUB.toLowerCase() }; + }, +}; + +export function makeAccount(): Account { + return { id: "xrpl:0", family: "xrpl", index: 0, curve: "secp256k1", derivationPath: "m/44'/144'/0'/0/0", publicKey: PUB.toLowerCase(), address: ME }; +} + +type Handler = (params: Record) => unknown; + +/** rippled JSON-RPC mock: answers by `method`; a handler's return value becomes `result` (status success unless it says error). */ +export function mockRpc(over: Record = {}) { + const calls: { url: string; method: string; params: Record }[] = []; + const routes: Record = { ...baseRoutes(), ...over }; + const f = (async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const body = JSON.parse(String(init?.body ?? "{}")) as { method: string; params: Record[] }; + const params = body.params?.[0] ?? {}; + calls.push({ url, method: body.method, params }); + const h = routes[body.method]; + if (!h) return new Response(JSON.stringify({ result: { status: "error", error: "unknownCmd" } })); + const r = h(params) as Record; + return new Response(JSON.stringify({ result: { status: "success", ...r } })); + }) as typeof fetch; + return { fetch: f, calls }; +} + +export const notFound = () => ({ status: "error", error: "actNotFound", error_message: "Account not found." }); + +export function account(address: string, over: Record = {}) { + return { account_data: { Account: address, Balance: "50000000", Flags: 0, OwnerCount: 1, Sequence: SEQ, ...over } }; +} + +export function baseRoutes(): Record { + return { + account_info: (p) => account(String(p.account)), + server_info: () => ({ info: { network_id: 1, validated_ledger: { reserve_base_xrp: 1, reserve_inc_xrp: 0.2, seq: LEDGER - 1, base_fee_xrp: 0.00001 } } }), + fee: () => ({ drops: { base_fee: "10", median_fee: "5000", minimum_fee: "10", open_ledger_fee: OPEN_LEDGER_FEE }, ledger_current_index: LEDGER }), + account_lines: () => ({ lines: [] }), + submit: () => ({ engine_result: "tesSUCCESS", engine_result_message: "The transaction was applied." }), + tx: (p) => ({ hash: p.transaction, validated: true, meta: { TransactionResult: "tesSUCCESS" } }), + }; +} + +export function ctxFor(fetchImpl: typeof fetch, network = XRPL_TESTNET): ChainContext { + return { network, account: makeAccount(), fetch: fetchImpl }; +} diff --git a/packages/chains-xrpl/test/signatures.ts b/packages/chains-xrpl/test/signatures.ts new file mode 100644 index 00000000..59dbbd6b --- /dev/null +++ b/packages/chains-xrpl/test/signatures.ts @@ -0,0 +1,11 @@ +/** + * Signatures computed once OFFLINE (outside the repo) with the vault's own `deriveFamilyKey` + `signEcdsa` (RFC 6979, + * low-S) for the public BIP-39 test vector ("abandon" ×11 + "about", empty passphrase) at m/44'/144'/0'/0/0, over + * SHA-512Half(0x53545800 ‖ signing fields) of the fixtures in ./fixtures.ts. r ‖ s hex, as the vault returns them. + * AGENTS.md rule 1: no key or signing code outside packages/vault, so only these signatures live here. + */ +export const SIGS = { + XRP_SEND: "12da991c97431ada3ee86aca1691c9d5bb4ad021c2383e362d694d8fa3a4c54c5d076277f19f17403febdb25bb290699e29dd04290d103c58f6347d233eca2ed", + RLUSD_SEND: "500f1cf861d41c9841b763c712265a0587d4dc36f22f940ad21891086b942d432fea9b5c0b2f0d7f98a3e8ec10352a1e1d6826cc71a8f83ce3a8d60b99f5e2f4", + DAPP_FILLED: "6cee20e9dc97524e11090f22d07b79760c0595790d61eec9775fb731aad614bb2ebd79ca116c7372c30b4772412e3bc88628a42a384ade646f38efc3643e53ae", +} as const; diff --git a/packages/chains-xrpl/test/xrpl.test.ts b/packages/chains-xrpl/test/xrpl.test.ts new file mode 100644 index 00000000..14680be6 --- /dev/null +++ b/packages/chains-xrpl/test/xrpl.test.ts @@ -0,0 +1,350 @@ +import { ClipError, type DappRequest, WALLET_ORIGIN } from "@clip-wallet/core"; +import { decode as rbcDecode, encodeForSigning } from "ripple-binary-codec"; +import { deriveAddress, verify } from "ripple-keypairs"; +import { hashes } from "xrpl"; +import { describe, expect, it } from "vitest"; +import { + RLUSD_CODE, + USDC_CODE, + XRPL_DEVNET, + XRPL_MAINNET, + XRPL_METHODS, + XRPL_NETWORKS, + XRPL_TESTNET, + createXrplModule, + encodeXAddress, + fromChainId, + knownTokens, + tokenAsset, +} from "../src/index.js"; +import { fromHex } from "../src/util.js"; +import { BOB, DAPP_FILLED, DAPP_UNFILLED, EXCHANGE, LLS, ME, NEWBIE, PUB, RLUSD_ISSUER, RLUSD_SEND, SEQ, XRP_SEND } from "./fixtures.js"; +import { account, ctxFor, mockRpc, notFound, signer } from "./helpers.js"; +import { SIGS } from "./signatures.js"; + +const mod = createXrplModule({ confirmPollMs: 0, confirmAttempts: 3 }); +const xrp = XRPL_TESTNET.nativeAsset; +const rlusd = tokenAsset(XRPL_TESTNET.id, RLUSD_CODE, RLUSD_ISSUER); + +function req(method: string, params: unknown, over: Partial = {}): DappRequest { + return { id: Math.random().toString(36).slice(2), origin: "https://dapp.example", via: "injected", family: "xrpl", networkId: XRPL_TESTNET.id, method, params, ...over }; +} + +async function run(r: DappRequest, ctx: ReturnType, m = mod) { + const d = await m.decode(r, ctx); + const payloads = await m.prepare(r, ctx, "approval-1"); + const out = await m.finalize(r, payloads.map((p) => signer.sign(p)), ctx); + return { d, payloads, out }; +} + +describe("networks", () => { + it("uses CAIP-2 / XLS-72d ids and checked endpoints", () => { + expect(XRPL_NETWORKS.map((n) => n.id)).toEqual(["xrpl:1", "xrpl:0", "xrpl:2"]); + expect(XRPL_MAINNET.testnet).toBe(false); + expect(fromChainId("xrpl:testnet")).toBe("xrpl:1"); + expect(fromChainId("xrpl:mainnet")).toBe("xrpl:0"); + expect(fromChainId("xrpl:21337")).toBeNull(); + expect(knownTokens(XRPL_MAINNET.id).map((a) => [a.key, a.address])).toEqual([ + ["rlusd", `${RLUSD_CODE}.rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De`], + ["usdc", `${USDC_CODE}.rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE`], + ]); + expect(knownTokens(XRPL_DEVNET.id)).toEqual([]); + }); + + it("marks look-alike stablecoins from other issuers as spam", () => { + expect(tokenAsset(XRPL_TESTNET.id, RLUSD_CODE, RLUSD_ISSUER)).toMatchObject({ key: "rlusd", symbol: "RLUSD", decimals: 6 }); + expect(tokenAsset(XRPL_TESTNET.id, RLUSD_CODE, BOB)).toMatchObject({ symbol: "RLUSD", spam: true }); + expect(tokenAsset(XRPL_TESTNET.id, USDC_CODE, RLUSD_ISSUER).spam).toBe(true); + expect(tokenAsset(XRPL_TESTNET.id, "SOL", BOB)).toMatchObject({ key: `xrpl:SOL.${BOB}`, decimals: 15 }); + expect(tokenAsset(XRPL_TESTNET.id, "SOL", BOB).spam).toBeUndefined(); + }); +}); + +describe("addresses", () => { + it("derives the abandon account like ripple-keypairs and checks checksums", () => { + expect(mod.addressFromPublicKey(fromHex(PUB), XRPL_TESTNET)).toBe(ME); + expect(deriveAddress(PUB)).toBe(ME); + expect(mod.derivationPath(0)).toBe("m/44'/144'/0'/0/0"); + expect(mod.derivationPath(2)).toBe("m/44'/144'/2'/0/0"); + expect(mod.curve).toBe("secp256k1"); + expect(mod.isAddress(ME)).toBe(true); + expect(mod.isAddress(`${ME.slice(0, -1)}4`)).toBe(false); + expect(mod.isAddress("0x1234")).toBe(false); + expect(mod.networksForAddress(ME, XRPL_NETWORKS)).toHaveLength(3); + const testX = encodeXAddress(BOB, 7, true); + expect(mod.isAddress(testX)).toBe(true); + expect(mod.networksForAddress(testX, XRPL_NETWORKS).map((n) => n.id)).toEqual(["xrpl:1", "xrpl:2"]); + expect(mod.networksForAddress(encodeXAddress(BOB, null, false), XRPL_NETWORKS).map((n) => n.id)).toEqual(["xrpl:0"]); + }); +}); + +describe("balances", () => { + it("lists XRP, RLUSD and other trust lines; skips negative (issued) lines; reports the reserve", async () => { + const { fetch } = mockRpc({ + account_info: () => account(ME, { Balance: "25000000", OwnerCount: 3 }), + account_lines: () => ({ + lines: [ + { account: RLUSD_ISSUER, balance: "12.3456789", currency: RLUSD_CODE, limit: "1000" }, + { account: BOB, balance: "5", currency: USDC_CODE, limit: "1000" }, + { account: EXCHANGE, balance: "-3", currency: "EUR", limit: "0" }, + ], + }), + }); + const b = await mod.getBalances(ctxFor(fetch)); + expect(b.map((x) => [x.asset.key, x.amount, x.asset.spam ?? false])).toEqual([ + ["xrp", "25000000", false], + ["rlusd", "12345678", false], + [`xrpl:${USDC_CODE}.${BOB}`, "5000000000000000", true], + ]); + expect(await mod.spendable(ctxFor(fetch))).toEqual({ balance: "25000000", locked: "1600000", spendable: "23400000", baseReserve: "1000000", ownerReserve: "200000", ownerCount: 3 }); + }); + + it("an account that was never funded has 0 XRP", async () => { + const { fetch } = mockRpc({ account_info: notFound }); + expect(await mod.getBalances(ctxFor(fetch))).toEqual([{ asset: xrp, amount: "0" }]); + }); +}); + +describe("buildTransfer → decode → prepare → finalize", () => { + it("sends XRP: signs, submits, waits for validation (cross-checked with xrpl.js)", async () => { + const { fetch, calls } = mockRpc(); + const ctx = ctxFor(fetch); + const r = await mod.buildTransfer({ asset: xrp, to: BOB, amount: "1500000" }, ctx); + expect(r).toMatchObject({ origin: WALLET_ORIGIN, method: XRPL_METHODS.signAndSubmitTransaction, family: "xrpl", networkId: "xrpl:1" }); + expect((r.params as { tx_json: unknown }).tx_json).toEqual(XRP_SEND); + const { d, payloads, out } = await run(r, ctx); + expect(d.title).toBe("Send 1.5 XRP to rPT1…pAYe"); + expect(d.titleMsg?.id ?? "bg.req.sendTo").toBe("bg.req.sendTo"); + expect(d.balanceChanges).toEqual([{ asset: xrp, delta: "-1500000" }]); + expect(d.fee).toEqual({ asset: xrp, amount: "12" }); + expect(d.lines).toContainEqual({ label: "Network fee", value: "0.000012 XRP" }); + expect(d.blind).toBe(false); + expect(d.warnings).toEqual([]); + // The digest is what ripple-binary-codec signs (encodeForSigning), and the vault's signature verifies with ripple-keypairs. + expect(payloads).toHaveLength(1); + const submitted = calls.find((c) => c.method === "submit")!.params.tx_blob as string; + expect(verify(encodeForSigning(XRP_SEND as never), (rbcDecode(submitted) as { TxnSignature: string }).TxnSignature, PUB)).toBe(true); + expect(rbcDecode(submitted)).toMatchObject({ ...XRP_SEND }); + expect(out).toMatchObject({ tx_hash: hashes.hashSignedTx(submitted), tx_json: { validated: true } }); + }); + + it("sends RLUSD to someone who has added it", async () => { + const { fetch, calls } = mockRpc({ + account_lines: (p) => + p.account === ME + ? { lines: [{ account: RLUSD_ISSUER, balance: "10", currency: RLUSD_CODE, limit: "1000000" }] } + : { lines: [{ account: RLUSD_ISSUER, balance: "0", currency: RLUSD_CODE, limit: "1000000" }] }, + }); + const ctx = ctxFor(fetch); + const r = await mod.buildTransfer({ asset: rlusd, to: BOB, amount: "2500000" }, ctx); + expect((r.params as { tx_json: unknown }).tx_json).toEqual(RLUSD_SEND); + const { d } = await run(r, ctx); + expect(d.title).toBe("Send 2.5 RLUSD to rPT1…pAYe"); + expect(d.balanceChanges).toEqual([{ asset: rlusd, delta: "-2500000" }]); + expect(d.lines).toContainEqual({ label: "Token", value: `RLUSD · ${RLUSD_ISSUER}` }); + expect(calls.some((c) => c.method === "submit")).toBe(true); + }); + + it("refuses in plain words: own address, reserve, unfunded recipient, missing tag, no trust line", async () => { + const ctx = ctxFor(mockRpc().fetch); + await expect(mod.buildTransfer({ asset: xrp, to: ME, amount: "1" }, ctx)).rejects.toMatchObject({ code: "xrpl/self-transfer" }); + await expect(mod.buildTransfer({ asset: xrp, to: "rNotAnAddress", amount: "1" }, ctx)).rejects.toMatchObject({ code: "xrpl/bad-address" }); + await expect(mod.buildTransfer({ asset: xrp, to: BOB, amount: "49000000" }, ctx)).rejects.toMatchObject({ + code: "xrpl/insufficient-funds", + userMessage: "You don't have enough XRP. 1.2 XRP has to stay in your account, and the fee is 0.000012 XRP.", + }); + const unfunded = ctxFor(mockRpc({ account_info: (p) => (p.account === NEWBIE ? notFound() : account(String(p.account))) }).fetch); + await expect(mod.buildTransfer({ asset: xrp, to: NEWBIE, amount: "500000" }, unfunded)).rejects.toMatchObject({ code: "xrpl/too-small" }); + await expect(mod.buildTransfer({ asset: xrp, to: NEWBIE, amount: "1000000" }, unfunded)).resolves.toBeTruthy(); + const tagged = ctxFor(mockRpc({ account_info: (p) => account(String(p.account), p.account === EXCHANGE ? { Flags: 0x00020000 } : {}) }).fetch); + await expect(mod.buildTransfer({ asset: xrp, to: EXCHANGE, amount: "1000000" }, tagged)).rejects.toMatchObject({ code: "xrpl/memo-required" }); + const viaX = await mod.buildTransfer({ asset: xrp, to: encodeXAddress(EXCHANGE, 12345, true), amount: "1000000" }, tagged); + expect((viaX.params as { tx_json: Record }).tx_json).toMatchObject({ Destination: EXCHANGE, DestinationTag: 12345 }); + await expect(mod.buildTransfer({ asset: xrp, to: encodeXAddress(EXCHANGE, 1, false), amount: "1" }, tagged)).rejects.toMatchObject({ code: "xrpl/network-mismatch" }); + const noLine = ctxFor(mockRpc({ account_lines: (p) => (p.account === ME ? { lines: [{ account: RLUSD_ISSUER, balance: "10", currency: RLUSD_CODE, limit: "1" }] } : { lines: [] }) }).fetch); + await expect(mod.buildTransfer({ asset: rlusd, to: BOB, amount: "1000000" }, noLine)).rejects.toMatchObject({ code: "xrpl/no-trust-line" }); + const keyOff = ctxFor(mockRpc({ account_info: (p) => account(String(p.account), p.account === ME ? { Flags: 0x00100000, RegularKey: BOB } : {}) }).fetch); + await expect(mod.buildTransfer({ asset: xrp, to: BOB, amount: "1" }, keyOff)).rejects.toMatchObject({ code: "xrpl/master-disabled" }); + await expect(mod.decode(req(XRPL_METHODS.signTransaction, { tx_json: XRP_SEND }), keyOff)).rejects.toMatchObject({ code: "xrpl/master-disabled" }); + const me0 = ctxFor(mockRpc({ account_info: notFound }).fetch); + await expect(mod.buildTransfer({ asset: xrp, to: BOB, amount: "1" }, me0)).rejects.toMatchObject({ code: "xrpl/not-activated" }); + }); +}); + +describe("dapp requests (XLS-72d)", () => { + it("autofills once, signs exactly what was shown, returns signed_tx_blob without submitting", async () => { + const { fetch, calls } = mockRpc(); + const ctx = ctxFor(fetch); + const r = req(XRPL_METHODS.signTransaction, { tx_json: DAPP_UNFILLED, account: ME, network: "xrpl:testnet", options: { autofill: true } }); + const { d, out } = await run(r, ctx); + expect(d.title).toBe("Change your account settings"); + expect(d.lines).toContainEqual({ label: "Memo", value: "hello xrp" }); + expect(d.lines).toContainEqual({ label: "Sent by", value: "dapp.example (it gets the signed transaction)" }); + const blob = (out as { signed_tx_blob: string }).signed_tx_blob; + expect(rbcDecode(blob)).toMatchObject({ ...DAPP_FILLED, Sequence: SEQ, LastLedgerSequence: LLS }); + expect(verify(encodeForSigning(DAPP_FILLED as never), (rbcDecode(blob) as { TxnSignature: string }).TxnSignature, PUB)).toBe(true); + expect(calls.filter((c) => c.method === "fee")).toHaveLength(1); + expect(calls.some((c) => c.method === "submit")).toBe(false); + }); + + it("refuses a bad signature before broadcasting", async () => { + const { fetch, calls } = mockRpc(); + const ctx = ctxFor(fetch); + const r = req(XRPL_METHODS.signAndSubmitTransaction, { tx_json: XRP_SEND, account: ME, network: "xrpl:1" }); + await mod.decode(r, ctx); + await mod.prepare(r, ctx, "a"); + const bad = fromHex(SIGS.RLUSD_SEND); + await expect(mod.finalize(r, [{ scheme: "ecdsa-secp256k1", bytes: bad, recovery: 0, publicKey: PUB }], ctx)).rejects.toMatchObject({ code: "xrpl/bad-signature" }); + expect(calls.some((c) => c.method === "submit")).toBe(false); + }); + + it("refuses another network, another account, multisig and already-signed transactions", () => { + const ctx = ctxFor(mockRpc().fetch); + const n = (p: unknown, over: Partial = {}) => () => mod.normalize(req(XRPL_METHODS.signTransaction, p, over), ctx); + expect(n({ tx_json: XRP_SEND, network: "xrpl:0" })).toThrow(expect.objectContaining({ code: "xrpl/network-mismatch" })); + expect(n({ tx_json: XRP_SEND }, { networkId: "xrpl:0" })).toThrow(expect.objectContaining({ code: "xrpl/network-mismatch" })); + expect(n({ tx_json: { ...XRP_SEND, NetworkID: 21337 } })).toThrow(expect.objectContaining({ code: "xrpl/network-mismatch" })); + expect(n({ tx_json: { ...XRP_SEND, Account: BOB } })).toThrow(expect.objectContaining({ code: "xrpl/wrong-account" })); + expect(n({ tx_json: XRP_SEND, account: BOB })).toThrow(expect.objectContaining({ code: "xrpl/wrong-account" })); + expect(n({ tx_json: XRP_SEND, options: { multisig: true } })).toThrow(expect.objectContaining({ code: "xrpl/unsupported" })); + expect(n({ tx_json: { ...XRP_SEND, TxnSignature: "00" } })).toThrow(ClipError); + expect(n({ tx_json: { ...XRP_SEND, SigningPubKey: "02".padEnd(66, "1") } })).toThrow(expect.objectContaining({ code: "xrpl/wrong-account" })); + expect(() => mod.normalize(req("xrpl:signMessage", {}), ctx)).toThrow(expect.objectContaining({ code: "xrpl/unsupported-method" })); + // API v2 DeliverMax becomes Amount; a NetworkID equal to a low network id is dropped (rippled refuses it there). + const ok = mod.normalize(req(XRPL_METHODS.signTransaction, { tx_json: { ...XRP_SEND, Amount: undefined, DeliverMax: "5", NetworkID: 1 } }), ctx); + expect(ok.tx).toMatchObject({ Amount: "5" }); + expect(ok.tx.NetworkID).toBeUndefined(); + expect(ok.tx.DeliverMax).toBeUndefined(); + }); + + it("an unknown transaction type is blind, and can't be signed", async () => { + const ctx = ctxFor(mockRpc().fetch); + const r = req(XRPL_METHODS.signTransaction, { tx_json: { ...XRP_SEND, TransactionType: "CheckCreate", SendMax: "1" } }); + const d = await mod.decode(r, ctx); + expect(d.blind).toBe(true); + expect(d.warnings.map((w) => w.code)).toContain("blind-signing"); + await expect(mod.prepare(r, ctx, "a")).rejects.toMatchObject({ code: "xrpl/unreadable-operation" }); + const field = await mod.decode(req(XRPL_METHODS.signTransaction, { tx_json: { ...XRP_SEND, Delegate: BOB } }), ctx); + expect(field.blind).toBe(true); + expect(field.warnings.some((w) => w.message.includes("(Delegate)"))).toBe(true); + }); + + it("explains a submit the ledger refuses, and a validated failure", async () => { + const refused = ctxFor(mockRpc({ submit: () => ({ engine_result: "tefPAST_SEQ" }) }).fetch); + const r = req(XRPL_METHODS.signAndSubmitTransaction, { tx_json: XRP_SEND }); + await mod.decode(r, refused); + const [p] = await mod.prepare(r, refused, "a"); + await expect(mod.finalize(r, [signer.sign(p!)], refused)).rejects.toMatchObject({ code: "xrpl/send-failed", userMessage: expect.stringContaining("out of date") }); + const failed = ctxFor(mockRpc({ tx: (q) => ({ hash: q.transaction, validated: true, meta: { TransactionResult: "tecPATH_DRY" } }) }).fetch); + const r2 = req(XRPL_METHODS.signAndSubmitTransaction, { tx_json: XRP_SEND }); + await mod.decode(r2, failed); + const [p2] = await mod.prepare(r2, failed, "a"); + await expect(mod.finalize(r2, [signer.sign(p2!)], failed)).rejects.toMatchObject({ code: "xrpl/failed", userMessage: expect.stringContaining("fee was still charged") }); + }); +}); + +describe("decode", () => { + const ctx = (over = {}) => ctxFor(mockRpc(over).fetch); + const dec = (tx: Record, over = {}) => mod.decode(req(XRPL_METHODS.signTransaction, { tx_json: { ...XRP_SEND, ...tx } }), ctx(over)); + + it("partial payments are a danger: the recipient can get far less", async () => { + const d = await dec({ Flags: 0x00020000, Amount: { currency: RLUSD_CODE, issuer: RLUSD_ISSUER, value: "1000" }, SendMax: "1000000" }, { + account_lines: () => ({ lines: [{ account: RLUSD_ISSUER, balance: "0", currency: RLUSD_CODE, limit: "1" }] }), + }); + expect(d.title).toBe("Pay up to 1 XRP so rPT1…pAYe gets 1000 RLUSD"); + expect(d.warnings.find((w) => w.code === "simulation-failed")).toMatchObject({ level: "danger", message: expect.stringContaining("partial payment") }); + expect(d.balanceChanges).toEqual([{ asset: xrp, delta: "-1000000" }]); + }); + + it("destination tags: required by the recipient → memo-required; shown when set", async () => { + const needs = { account_info: (p: Record) => account(String(p.account), p.account === BOB ? { Flags: 0x00020000 } : {}) }; + expect((await dec({}, needs)).warnings.map((w) => w.code)).toContain("memo-required"); + const d = await dec({ DestinationTag: 77 }, needs); + expect(d.warnings.map((w) => w.code)).not.toContain("memo-required"); + expect(d.lines).toContainEqual({ label: "Destination tag", value: "77" }); + }); + + it("a new account receiving less than the base reserve fails", async () => { + const d = await dec({ Amount: "500000" }, { account_info: (p: Record) => (p.account === BOB ? notFound() : account(String(p.account))) }); + expect(d.warnings.find((w) => w.message.includes("doesn't exist yet"))).toMatchObject({ level: "danger" }); + const ok = await dec({ Amount: "2000000" }, { account_info: (p: Record) => (p.account === BOB ? notFound() : account(String(p.account))) }); + expect(ok.warnings.find((w) => w.code === "new-recipient")).toMatchObject({ level: "info" }); + }); + + it("spending into the reserve is flagged", async () => { + const d = await dec({ Amount: "49500000" }); + expect(d.warnings.find((w) => w.message.includes("reserve"))).toMatchObject({ level: "danger", code: "simulation-failed" }); + }); + + it("TrustSet, OfferCreate, AccountSet flags in words", async () => { + const trust = await dec({ TransactionType: "TrustSet", Destination: undefined, Amount: undefined, LimitAmount: { currency: RLUSD_CODE, issuer: RLUSD_ISSUER, value: "1000000" } }); + expect(trust.title).toBe("Add RLUSD to your account"); + expect(trust.lines).toContainEqual({ label: "Locks", value: "0.2 XRP of your XRP stays locked while this token is added" }); + const spam = await dec({ TransactionType: "TrustSet", Destination: undefined, Amount: undefined, LimitAmount: { currency: USDC_CODE, issuer: BOB, value: "1" } }); + expect(spam.warnings.map((w) => w.code)).toContain("known-scam"); + const remove = await dec({ TransactionType: "TrustSet", Destination: undefined, Amount: undefined, LimitAmount: { currency: RLUSD_CODE, issuer: RLUSD_ISSUER, value: "0" } }); + expect(remove.title).toBe("Remove RLUSD from your account"); + const offer = await dec({ TransactionType: "OfferCreate", Destination: undefined, Amount: undefined, TakerGets: "10000000", TakerPays: { currency: RLUSD_CODE, issuer: RLUSD_ISSUER, value: "5" }, Flags: 0x00020000 }); + expect(offer.title).toBe("Trade 10 XRP for 5 RLUSD"); + expect(offer.balanceChanges).toEqual([{ asset: xrp, delta: "-10000000" }, { asset: rlusd, delta: "5000000" }]); + expect(offer.lines).toContainEqual({ label: "Kind", value: "Fill what it can now, cancel the rest" }); + const set = await dec({ TransactionType: "AccountSet", Destination: undefined, Amount: undefined, SetFlag: 1, ClearFlag: 3, Domain: "6578616D706C652E636F6D", Flags: 0x00100000 }); + expect(set.title).toBe("Change your account settings"); + expect(set.lines).toEqual(expect.arrayContaining([ + { label: "Turns on", value: "Require a destination tag on payments to you" }, + { label: "Turns off", value: "Ask people not to send you XRP" }, + { label: "Turns on", value: "Ask people not to send you XRP" }, + { label: "Domain", value: "example.com" }, + ])); + expect((await dec({ TransactionType: "AccountSet", Destination: undefined, Amount: undefined, SetFlag: 99 })).blind).toBe(true); + }); + + it("key changes are account-takeover; AccountDelete is account-closure", async () => { + const master = await dec({ TransactionType: "AccountSet", Destination: undefined, Amount: undefined, SetFlag: 4 }); + expect(master.warnings.find((w) => w.code === "account-takeover")).toMatchObject({ level: "danger" }); + const regular = await dec({ TransactionType: "SetRegularKey", Destination: undefined, Amount: undefined, RegularKey: BOB }); + expect(regular.title).toBe("Give rPT1…pAYe control of your account"); + expect(regular.warnings[0]).toMatchObject({ level: "danger", code: "account-takeover" }); + expect((await dec({ TransactionType: "SetRegularKey", Destination: undefined, Amount: undefined })).warnings).toEqual([]); + const signers = await dec({ TransactionType: "SignerListSet", Destination: undefined, Amount: undefined, SignerQuorum: 1, SignerEntries: [{ SignerEntry: { Account: BOB, SignerWeight: 1 } }] }); + expect(signers.title).toBe("Let 1 other accounts sign for yours"); + expect(signers.warnings[0]).toMatchObject({ level: "danger", code: "account-takeover" }); + const del = await dec({ TransactionType: "AccountDelete", Amount: undefined, Fee: "200000" }); + expect(del.title).toBe("Close your XRP Ledger account and send everything to rPT1…pAYe"); + expect(del.warnings[0]).toMatchObject({ level: "danger", code: "account-closure" }); + expect(del.balanceChanges).toEqual([{ asset: xrp, delta: "-49800000" }]); + }); + + it("NFTs and escrows", async () => { + const id = "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"; + const mint = await dec({ TransactionType: "NFTokenMint", Destination: undefined, Amount: undefined, NFTokenTaxon: 0, Flags: 8, TransferFee: 500, URI: "697066733A2F2F62616679" }); + expect(mint.title).toBe("Create an NFT"); + expect(mint.lines).toEqual(expect.arrayContaining([{ label: "Link", value: "ipfs://bafy" }, { label: "Royalty", value: "0.5%" }])); + const give = await dec({ TransactionType: "NFTokenCreateOffer", Destination: undefined, NFTokenID: id, Amount: "0", Flags: 1 }); + expect(give.warnings[0]).toMatchObject({ level: "danger", message: "Anyone can take this NFT for nothing." }); + const buy = await dec({ + TransactionType: "NFTokenAcceptOffer", + Destination: undefined, + Amount: undefined, + NFTokenSellOffer: "68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77", + }, { ledger_entry: () => ({ node: { LedgerEntryType: "NFTokenOffer", Amount: "3000000", NFTokenID: id, Owner: BOB, Flags: 1 } }) }); + expect(buy.title).toBe("Buy an NFT for 3 XRP"); + expect(buy.balanceChanges).toEqual([{ asset: xrp, delta: "-3000000" }]); + const burn = await dec({ TransactionType: "NFTokenBurn", Destination: undefined, Amount: undefined, NFTokenID: id }); + expect(burn.title).toBe("Burn an NFT"); + const esc = await dec({ TransactionType: "EscrowCreate", Amount: "10000000", FinishAfter: 0, CancelAfter: 86400 }); + expect(esc.title).toBe("Lock 10 XRP in escrow for rPT1…pAYe"); + expect(esc.lines).toContainEqual({ label: "Can be released after", value: "2000-01-01 00:00:00 UTC" }); + expect(esc.warnings[0]).toMatchObject({ code: "durable-nonce" }); + }); + + it("AMM deposit and withdraw", async () => { + const amm = { Destination: undefined, Asset: { currency: "XRP" }, Asset2: { currency: RLUSD_CODE, issuer: RLUSD_ISSUER } }; + const dep = await dec({ ...amm, TransactionType: "AMMDeposit", Flags: 0x00100000, Amount: "1000000", Amount2: { currency: RLUSD_CODE, issuer: RLUSD_ISSUER, value: "0.5" } }); + expect(dep.title).toBe("Add funds to a liquidity pool"); + expect(dep.lines).toContainEqual({ label: "Pool", value: "XRP / RLUSD" }); + expect(dep.balanceChanges).toEqual([{ asset: xrp, delta: "-1000000" }, { asset: rlusd, delta: "-500000" }]); + const wd = await dec({ ...amm, TransactionType: "AMMWithdraw", Amount: undefined, Flags: 0x00020000 }); + expect(wd.title).toBe("Withdraw funds from a liquidity pool"); + }); +}); diff --git a/packages/chains-xrpl/tsconfig.json b/packages/chains-xrpl/tsconfig.json new file mode 100644 index 00000000..9d04942c --- /dev/null +++ b/packages/chains-xrpl/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/config/README.md b/packages/config/README.md index 8a95d9f1..adc8c46c 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -2,7 +2,7 @@ The typed schema for `clip.config.ts`, the one file a wallet maker edits, for every platform the wallet ships (browser extension, desktop app, phone app): identity (name, description, rdns, homepage, icon, extension key, app ids, -deep-link scheme), theme, networks (14 families), languages, routing defaults and settle-on-Hedera, hardware wallets, +deep-link scheme), theme, networks (26 families), languages, routing defaults and settle-on-Hedera, hardware wallets, WalletConnect, passkeys, optional hosted services, and the mainnet switch. `defineConfig()` validates and fills defaults; problems come back as plain sentences, one per setting diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 9a2fc093..fcd950a2 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -34,6 +34,18 @@ export const NETWORK_FAMILIES = [ "stellar", "tezos", "algorand", + "cosmos", + "provenance", + "thorchain", + "initia", + "tron", + "xrpl", + "antelope", + "multiversx", + "icp", + "stacks", + "fuel", + "bitcoincash", ] as const; export const ROUTE_MODES = ["balanced", "cheapest", "fastest", "reliable", "greenest"] as const; export const TRUST_TIERS = ["attested", "committee", "light-client", "validity-proof"] as const; diff --git a/packages/config/test/config.test.ts b/packages/config/test/config.test.ts index 78850998..828b12a1 100644 --- a/packages/config/test/config.test.ts +++ b/packages/config/test/config.test.ts @@ -105,8 +105,8 @@ describe("validation errors in plain words", () => { expect(problems({ ...base, services: { mediaProxyUrl: "https://media.example.com/?x=1" } })[0]).toMatch(/^services.mediaProxyUrl:/); }); - it("accepts all 14 chain families", () => { - expect(NETWORK_FAMILIES).toHaveLength(14); + it("accepts all 26 chain families", () => { + expect(NETWORK_FAMILIES).toHaveLength(26); const all = ["evm:*", ...NETWORK_FAMILIES.filter((f) => f !== "evm")]; expect(validateConfig({ ...base, networks: all }).ok).toBe(true); expect(enabledFamilies(defineConfig({ ...base, networks: all }))).toEqual([...NETWORK_FAMILIES]); @@ -114,7 +114,7 @@ describe("validation errors in plain words", () => { it("explains network patterns", () => { expect(problems({ ...base, networks: ["ethereum"] })).toEqual([ - 'networks.0: use "evm:*", "evm:", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos" or "algorand"', + 'networks.0: use "evm:*", "evm:", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", "cosmos", "provenance", "thorchain", "initia", "tron", "xrpl", "antelope", "multiversx", "icp", "stacks", "fuel" or "bitcoincash"', ]); expect(problems({ ...base, networks: [] })).toEqual(["networks: turn on at least one network"]); expect(problems({ ...base, networks: ["hedera", "hedera"] })).toEqual(["networks: each network is listed once"]); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8dc2a01b..2cc6722a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -32,11 +32,30 @@ export type Family = | "near" | "stellar" | "tezos" - | "algorand"; + | "algorand" + /** + * networks87 (additive): the CLPR networks that aren't EVM and weren't covered yet. Cosmos SDK chains split by key + * (each ecosystem wallet derives at the chain's own SLIP-44 coin type): "cosmos" (coin 118: Osmosis, dYdX, + * ZIGChain), "provenance" (505), "thorchain" (931), "initia" (coin 60, ethsecp256k1). "antelope" is Vaulta, Telos + * and XPR Network (one K1 key, accounts are names on chain). + */ + | "cosmos" + | "provenance" + | "thorchain" + | "initia" + | "tron" + | "xrpl" + | "antelope" + | "multiversx" + | "icp" + | "stacks" + | "fuel" + | "bitcoincash"; export const FAMILIES: readonly Family[] = [ "evm", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", + "cosmos", "provenance", "thorchain", "initia", "tron", "xrpl", "antelope", "multiversx", "icp", "stacks", "fuel", "bitcoincash", ]; /** @@ -355,6 +374,13 @@ export interface ChainModule { finalize(request: DappRequest, signatures: Signature[], ctx: ChainContext): Promise; /** Builds a native send as a DappRequest so it goes through the same approval path. */ buildTransfer(p: { asset: AssetRef; to: string; amount: string }, ctx: ChainContext): Promise; + /** + * networks87 (additive, optional): the address to RECEIVE on `ctx.network`, when it isn't `ctx.account.address`: + * the format differs per network (Cosmos bech32 prefixes, Stacks SP/ST, Bitcoin Cash bitcoincash:/bchtest:) or the + * account lives on chain (Antelope account names). Throws a ClipError in plain words when the account can't + * receive yet. Hosts fall back to `ctx.account.address` when a module doesn't have it. + */ + receiveAddress?(ctx: ChainContext): Promise; } /* ------------------------------------------------------------------ errors in plain words */ diff --git a/packages/core/src/messages/en/index.ts b/packages/core/src/messages/en/index.ts index 301659ac..dc4c7c0d 100644 --- a/packages/core/src/messages/en/index.ts +++ b/packages/core/src/messages/en/index.ts @@ -16,9 +16,11 @@ import chainsB from "./chainsB.js"; import chainsC from "./chainsC.js"; /** features (staking, swaps, trade, steps), security, route, social handles */ import features from "./features.js"; +/** networks87: chains-cosmos, -tron, -xrpl, -antelope, -multiversx, -icp, -stacks, -fuel, -bitcoincash, their 1Mask providers */ +import n87 from "./n87.js"; /** The parts, for the duplicate-id test (a later part must never silently replace an earlier id). */ -export const BG_MESSAGE_PARTS = { requests, labels, warnings, errors, activity, chainsA, chainsB, chainsC, features } as const; +export const BG_MESSAGE_PARTS = { requests, labels, warnings, errors, activity, chainsA, chainsB, chainsC, features, n87 } as const; export const BG_MESSAGES = { ...requests, @@ -30,6 +32,7 @@ export const BG_MESSAGES = { ...chainsB, ...chainsC, ...features, + ...n87, } as const; export type BgMessages = typeof BG_MESSAGES; diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts new file mode 100644 index 00000000..4fd501a6 --- /dev/null +++ b/packages/core/src/messages/en/n87.ts @@ -0,0 +1,315 @@ +/** + * networks87 chain modules and their 1Mask providers ("bg..*", plain {arguments} only). One section per + * package: keep each package's ids between its markers (translations: ../locales/n87/.ts, same sections). + */ +export default { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "IBC channel {channel}", + "bg.cosmos.ibcLeaves": "This sends the tokens to another chain over IBC ({channel}). Check that the address is right for that chain.", + "bg.cosmos.unbonding": "Your {symbol} stays locked and earns nothing until the network's unbonding period ends.", + "bg.cosmos.moveStake": "Move {amount} of stake from {from} to {to}", + "bg.cosmos.claimRewardsFrom": "Claim staking rewards from {validator}", + "bg.cosmos.grantAuthz": "Let {grantee} act for your account", + "bg.cosmos.authzTakeAll": "This lets {grantee} move your funds without asking you again, until you take the permission back. They could take everything.", + "bg.cosmos.authzActs": "This lets {grantee} act for your account without asking you again, until you take the permission back. Only allow this for apps you trust.", + "bg.cosmos.revokeAuthz": "Stop {grantee} from acting for your account", + "bg.cosmos.grantFees": "Let {grantee} pay network fees with your {symbol}", + "bg.cosmos.feegrantUpTo": "This lets {grantee} spend up to {amount} of yours on network fees, without asking you again.", + "bg.cosmos.feegrantAll": "This lets {grantee} spend your {symbol} on network fees with no limit, without asking you again.", + "bg.cosmos.revokeFees": "Stop {grantee} from paying network fees with your funds", + "bg.cosmos.block": "block {height}", + "bg.cosmos.appGetsSigned": "{host} (it gets the signed transaction)", + "bg.cosmos.wrongPrefix": "That address is for another network. Addresses on {network} start with {prefix}.", + "bg.cosmos.noAccount": "Your account isn't on {network} yet. Receive some {symbol} first.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "This TRON account is controlled by another key, so Clip Wallet can't sign for it.", + "bg.tron.notOpen": "Your TRON account isn't open yet. Receive some TRX first.", + "bg.tron.contractRecipient": "TRX can't be sent straight to a smart contract.", + "bg.tron.newAccountFee": "This address isn't open on TRON yet. Sending to it also costs {amount} to open it.", + "bg.tron.getsSigned": "{host} (it gets the signed transaction)", + "bg.tron.energy": "energy", + "bg.tron.bandwidth": "bandwidth", + "bg.tron.tronPower": "voting power", + "bg.tron.stakeFor": "Stake {amount} for {resource}", + "bg.tron.days": "{count} days", + "bg.tron.cancelUnstaking": "Cancel your pending unstaking and stake that TRX again", + "bg.tron.lend": "Lend the {resource} of {amount} you've staked to {to}", + "bg.tron.stopLending": "Take back the {resource} of {amount} you lent to {to}", + "bg.tron.lockedHours": "About {hours} hours. You can't take it back before then.", + "bg.tron.vote": "Vote for {count} Super Representatives", + "bg.tron.votesReplace": "This replaces all your earlier votes.", + "bg.tron.claimVoteRewards": "Claim your voting rewards", + "bg.tron.changePermissions": "Change who controls your TRON account", + "bg.tron.keysThreshold": "{keys} (needs {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "Pay up to {amount} so {to} gets {get}", + "bg.xrpl.atMost": "At most {amount}", + "bg.xrpl.anyAmount": "Any amount", + "bg.xrpl.partialPayment": "This is a partial payment: the recipient may get far less than {amount}. Shops and exchanges have been tricked this way.", + "bg.xrpl.xrpToSelf": "Sending XRP to your own account does nothing, so the XRP Ledger will refuse it.", + "bg.xrpl.frees": "This frees the {amount} reserve it locked.", + "bg.xrpl.locksReserve": "{amount} of your XRP stays locked while this token is added", + "bg.xrpl.freezesLine": "This also freezes the token for the other side.", + "bg.xrpl.offer.fillOrKill": "Fill completely now, or cancel", + "bg.xrpl.offer.immediate": "Fill what it can now, cancel the rest", + "bg.xrpl.offer.standing": "Stays open until it fills, expires or you cancel it", + "bg.xrpl.cancelOffer": "Cancel an offer", + "bg.xrpl.changeSettings": "Change your account settings", + "bg.xrpl.asf.requireDest": "Require a destination tag on payments to you", + "bg.xrpl.asf.requireAuth": "Approve each holder of your tokens", + "bg.xrpl.asf.disallowXrp": "Ask people not to send you XRP", + "bg.xrpl.asf.disableMaster": "Turn off your account's own key", + "bg.xrpl.asf.accountTxnId": "Track your last transaction id", + "bg.xrpl.asf.noFreeze": "Give up freezing your tokens, for good", + "bg.xrpl.asf.globalFreeze": "Freeze every token you issued", + "bg.xrpl.asf.defaultRipple": "Let your tokens move between holders (rippling)", + "bg.xrpl.asf.depositAuth": "Only accept payments from accounts you approve", + "bg.xrpl.asf.nftMinter": "Let another account create NFTs for you", + "bg.xrpl.asf.noNftOffers": "Block incoming NFT offers", + "bg.xrpl.asf.noChecks": "Block incoming checks", + "bg.xrpl.asf.noPayChannels": "Block incoming payment channels", + "bg.xrpl.asf.noTrustLines": "Block incoming trust lines", + "bg.xrpl.asf.clawback": "Let you take back tokens you issue (clawback)", + "bg.xrpl.asf.lockTokens": "Let holders lock your tokens in escrow", + "bg.xrpl.disableMasterWarning": "This turns off your account's own key. Only another key or signer list can use the account after this; if there is none, it is locked for good.", + "bg.xrpl.issuerSettingWarning": "This changes how tokens you issue behave. Some of these can't be undone.", + "bg.xrpl.nftMinterWarning": "This lets {who} create NFTs in your account's name.", + "bg.xrpl.cleared": "(cleared)", + "bg.xrpl.removeRegularKey": "Remove the extra key from your account", + "bg.xrpl.regularKeyWarning": "{who} gets a key that can do anything with your account, including emptying it.", + "bg.xrpl.removeSigners": "Remove the accounts that can sign for yours", + "bg.xrpl.setSigners": "Let {count} other accounts sign for yours", + "bg.xrpl.signersWarning": "Together these accounts can do anything with your account, including emptying it.", + "bg.xrpl.closeAccount": "Close your XRP Ledger account and send everything to {to}", + "bg.xrpl.closeWarning": "This deletes your XRP Ledger account and sends all your XRP to {to}. The {fee} fee is burned.", + "bg.xrpl.mintNft": "Create an NFT", + "bg.xrpl.taxon": "Taxon {taxon}", + "bg.xrpl.nft.transferable": "Can be sold or given away", + "bg.xrpl.nft.notTransferable": "Can't be passed on", + "bg.xrpl.nft.issuerCanBurn": "The creator can burn it", + "bg.xrpl.nft.mutable": "Its link can be changed later", + "bg.xrpl.burnNft": "Burn an NFT", + "bg.xrpl.giveNft": "Offer your NFT to {to} for free", + "bg.xrpl.freeForAnyone": "Anyone can take this NFT for nothing.", + "bg.xrpl.sellNft": "Offer your NFT for {amount}", + "bg.xrpl.buyNft": "Offer {amount} for an NFT", + "bg.xrpl.offerHolds": "Nothing is taken until the owner accepts", + "bg.xrpl.acceptNftOffer": "Accept an NFT offer", + "bg.xrpl.brokered": "Matches a buy offer and a sell offer (brokered sale)", + "bg.xrpl.offerUnknown": "Clip Wallet couldn't look up this offer, so it can't show what you pay or get.", + "bg.xrpl.acceptNftSell": "Buy an NFT for {amount}", + "bg.xrpl.acceptNftBuy": "Sell your NFT for {amount}", + "bg.xrpl.cancelNftOffers": "Cancel {count} NFT offers", + "bg.xrpl.escrowCreate": "Lock {amount} in escrow for {to}", + "bg.xrpl.escrowCondition": "Releasing it needs a secret (crypto-condition)", + "bg.xrpl.escrowLocked": "These funds stay locked until the escrow is released or cancelled.", + "bg.xrpl.escrowFinish": "Release an escrow", + "bg.xrpl.escrowCancel": "Cancel an escrow", + "bg.xrpl.ammDeposit": "Add funds to a liquidity pool", + "bg.xrpl.ammWithdraw": "Withdraw funds from a liquidity pool", + "bg.xrpl.yourShare": "Your share of the pool", + "bg.xrpl.poolAmounts": "The pool decides the exact amounts when this runs.", + "bg.xrpl.unknownTx": "Approve an XRP Ledger {type} transaction", + "bg.xrpl.recipientUnchecked": "Clip Wallet couldn't check the recipient's account.", + "bg.xrpl.newAccountTooSmall": "This XRP Ledger account doesn't exist yet. Opening it takes at least {reserve}, so this will fail.", + "bg.xrpl.opensAccount": "This opens a new XRP Ledger account; {reserve} of it stays locked there as its reserve.", + "bg.xrpl.noRecipientAccount": "This XRP Ledger account doesn't exist, so it can't receive tokens.", + "bg.xrpl.destinationTagNeeded": "This recipient needs a destination tag and none is set. Without it, the funds may be lost.", + "bg.xrpl.disallowsXrp": "This account says it doesn't want to receive XRP.", + "bg.xrpl.depositAuth": "This account only accepts payments from accounts it approved, so this may fail.", + "bg.xrpl.noTrustLine": "The recipient hasn't added {symbol} to their account, so this payment will fail.", + "bg.xrpl.notEnoughXrp": "You don't have enough XRP for this: {reserve} has to stay in your account as its reserve.", + "bg.xrpl.highFee": "The network fee is unusually high: {fee}.", + "bg.xrpl.appSends": "{host} (it gets the signed transaction)", + "bg.xrpl.feeCharged": "The network fee was still charged.", + "bg.xrpl.label.destinationTag": "Destination tag", + "bg.xrpl.label.sourceTag": "Source tag", + "bg.xrpl.label.deliversAtLeast": "Delivers at least", + "bg.xrpl.label.issuer": "Issuer", + "bg.xrpl.label.replacesOffer": "Replaces offer", + "bg.xrpl.label.turnsOn": "Turns on", + "bg.xrpl.label.turnsOff": "Turns off", + "bg.xrpl.label.domain": "Domain", + "bg.xrpl.label.transferFee": "Transfer fee", + "bg.xrpl.label.nftMinter": "NFT minter", + "bg.xrpl.label.quorum": "Signatures needed", + "bg.xrpl.label.link": "Link", + "bg.xrpl.label.royalty": "Royalty", + "bg.xrpl.label.releaseAfter": "Can be released after", + "bg.xrpl.label.cancelAfter": "Can be cancelled after", + "bg.xrpl.err.wrongNetwork": "This app is asking for a different XRP Ledger network than the one it's connected to. Nothing was signed.", + "bg.xrpl.err.multisig": "Multi-signing isn't available in Clip Wallet yet.", + "bg.xrpl.err.alreadySigned": "This transaction is already signed, so Clip Wallet won't sign it again.", + "bg.xrpl.err.cantRead": "Clip Wallet can't read part of this XRP Ledger transaction ({field}), so it can't sign it.", + "bg.xrpl.err.notActivated": "Your XRP Ledger account isn't activated yet. Receive at least {reserve} XRP to activate it.", + "bg.xrpl.err.otherKey": "This account's own key is turned off and another key controls it, so Clip Wallet can't sign for it.", + "bg.xrpl.err.xAddressNetwork": "This X-address is for a different XRP Ledger network.", + "bg.xrpl.err.badAddress": "That doesn't look like an XRP Ledger address.", + "bg.xrpl.err.notToken": "That isn't an XRP Ledger token.", + "bg.xrpl.err.tagNeeded": "This address needs a destination tag. Ask the recipient for it, or for an X-address that includes it.", + "bg.xrpl.err.notEnoughXrp": "You don't have enough XRP. {reserve} XRP has to stay in your account, and the fee is {fee} XRP.", + "bg.xrpl.err.newAccount": "This XRP Ledger account doesn't exist yet. Send at least {reserve} XRP to open it.", + "bg.xrpl.err.feeOnly": "You need a little XRP to pay the network fee.", + "bg.xrpl.err.noTrustLine": "They haven't added {symbol} to their XRP Ledger account yet. Ask them to add it first.", + "bg.xrpl.err.reserveForToken": "Adding a token locks {amount} XRP, plus the fee. Add some XRP first.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "Call {action} on {contract}", + "bg.antelope.unknownToken": "{symbol} from {contract} isn't a token Clip Wallet knows. Check the contract before you send it.", + "bg.antelope.noRecipient": "There's no account named {to} on this network, so this will fail.", + "bg.antelope.updateAuth": "Change who controls the {permission} permission of {account}", + "bg.antelope.deleteAuth": "Delete the {permission} permission of {account}", + "bg.antelope.linkAuth": "Let the {permission} permission run {action}", + "bg.antelope.unlinkAuth": "Stop a permission from running {action}", + "bg.antelope.authWarning": "This changes which keys or accounts control {account}. If they aren't yours, they could take everything in it.", + "bg.antelope.unlinkWarning": "This changes which key can run some of your account's actions.", + "bg.antelope.label.alsoSigns": "Also needs", + "bg.antelope.delayed": "This only runs {seconds} seconds after it is sent, and can be cancelled until then.", + "bg.antelope.feeFree": "None (this network covers CPU and NET for you)", + "bg.antelope.feeResources": "None in tokens: it uses your account's CPU and NET", + "bg.antelope.appSends": "{host} (it gets the signed transaction)", + "bg.antelope.err.offline": "Couldn't reach {network}. Check your connection and try again.", + "bg.antelope.err.wrongNetwork": "This request is for a different Antelope network than the one you're on. Nothing was signed.", + "bg.antelope.err.contextFree": "This transaction uses features Clip Wallet doesn't sign (context-free actions or extensions).", + "bg.antelope.err.noAbi": "Clip Wallet can't read the contract {contract}, so it can't build this request.", + "bg.antelope.err.permission": "This needs a permission of your account that this key doesn't control.", + "bg.antelope.err.otherSigners": "This transaction also needs other accounts to sign, so Clip Wallet can't send it alone.", + "bg.antelope.err.noAccountTestnet": "No {network} account uses this key yet. Accounts on Antelope networks are names created on chain: create a free test account for your public key with the network's testnet tool, then come back.", + "bg.antelope.err.noAccount": "No {network} account uses this key yet. Accounts on Antelope networks are names someone creates on chain for your public key: an exchange, a friend who has an account, or an account-creation service can make one.", + "bg.antelope.err.notToken": "That isn't a token on this network.", + "bg.antelope.err.badAccount": "That isn't a valid account name. Names have up to 12 characters: a–z, 1–5 and dots.", + "bg.antelope.err.cpuPowerup": "Your account is out of CPU or NET time on {network}. Rent some with PowerUp (it costs a little EOS for 24 hours), then try again.", + "bg.antelope.err.cpuStake": "Your account is out of CPU or NET time on {network}. Stake some TLOS for CPU and NET, or rent it with PowerUp, then try again.", + "bg.antelope.err.cpuFree": "{network} couldn't cover the CPU or NET for this right now. Wait a moment and try again.", + "bg.antelope.err.ram": "Your account doesn't have enough RAM on {network} for this. Buy a little RAM, then try again.", + "bg.antelope.err.balance": "You don't have enough of this token for that.", + "bg.antelope.err.expired": "This transaction expired before it was sent. Try again.", + "bg.antelope.err.auth": "The network says this key isn't allowed to sign for that account. Nothing was sent.", + "bg.antelope.err.noRecipient": "The recipient account doesn't exist on this network. Nothing was sent.", + "bg.antelope.err.duplicate": "This exact transaction was already sent. Check Activity.", + "bg.antelope.err.rejected": "{network} rejected this transaction. Nothing was sent.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "Claim your staking rewards from {validator}", + "bg.multiversx.withdrawFrom": "Withdraw your unstaked EGLD from {validator}", + "bg.multiversx.restakeRewardsWith": "Restake your rewards with {validator}", + "bg.multiversx.labelGuardian": "Guardian", + "bg.multiversx.setGuardianTitle": "Make {guardian} your account's guardian", + "bg.multiversx.setGuardianWarn": "Once a guardian is active, every transaction from this account needs its co-signature. If you didn't choose {guardian}, someone could lock you out of your account.", + "bg.multiversx.guardAccountTitle": "Turn on your account's guardian", + "bg.multiversx.guardAccountWarn": "From now on every transaction from this account needs the guardian's co-signature. Clip Wallet can't provide it, so you won't be able to send from Clip Wallet any more.", + "bg.multiversx.unguardAccountTitle": "Turn off your account's guardian", + "bg.multiversx.changeOwnerTitle": "Give contract {contract} to {owner}", + "bg.multiversx.changeOwnerWarn": "This makes {owner} the owner of contract {contract}. The new owner can change its code and take what it holds. Only do this if you mean to give it away.", + "bg.multiversx.guardedAccount": "This account has a guardian, and Clip Wallet can't get the guardian's co-signature. Nothing was sent.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "This sends to an account ID, the kind of deposit address exchanges give. Check that it matches exactly what the exchange shows.", + "bg.icp.expired": "This transfer expired before you approved it. Nothing was sent. Try again.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "Post-condition", + "bg.stacks.pcYouSendExactly": "You send exactly {amount}", + "bg.stacks.pcYouSendAtMost": "You send at most {amount}", + "bg.stacks.pcYouSendAtLeast": "You send at least {amount}", + "bg.stacks.pcYouSendMoreThan": "You send more than {amount}", + "bg.stacks.pcYouSendLessThan": "You send less than {amount}", + "bg.stacks.pcSendsExactly": "{who} sends exactly {amount}", + "bg.stacks.pcSendsAtMost": "{who} sends at most {amount}", + "bg.stacks.pcSendsAtLeast": "{who} sends at least {amount}", + "bg.stacks.pcSendsMoreThan": "{who} sends more than {amount}", + "bg.stacks.pcSendsLessThan": "{who} sends less than {amount}", + "bg.stacks.pcYouSendNft": "You send {item}", + "bg.stacks.pcYouKeepNft": "You keep {item}", + "bg.stacks.pcYouMaySendNft": "You may send {item}", + "bg.stacks.pcSendsNft": "{who} sends {item}", + "bg.stacks.pcKeepsNft": "{who} keeps {item}", + "bg.stacks.pcMaySendNft": "{who} may send {item}", + "bg.stacks.pcStakingRule": "A staking rule for {who}", + "bg.stacks.allowMode": "This lets the contract move any of your assets, not only the ones listed. Only approve it if you trust {host} completely.", + "bg.stacks.originatorMode": "Only the transfers listed can leave your account. The contract may still move other accounts' assets.", + "bg.stacks.nothingLeaves": "None of your assets can leave your account in this transaction, apart from the network fee.", + "bg.stacks.highFee": "The network fee is {fee}, which is unusually high.", + "bg.stacks.sponsorPays": "A sponsor chosen by {host}", + "bg.stacks.deployNamed": "Create the smart contract {name}", + "bg.stacks.notYourTx": "This transaction is signed by a different Stacks account, so Clip Wallet can't sign it.", + "bg.stacks.multisig": "Clip Wallet can't sign for shared (multi-signature) Stacks accounts yet.", + "bg.stacks.memoTooLong": "The memo is too long. A Stacks memo holds up to 34 bytes.", + "bg.stacks.feeTooLow": "The network fee was too low, so the network didn't take it. Nothing was sent. Try again.", + "bg.stacks.nonceBusy": "Another transaction from this account is still waiting. Wait for it to finish, then try again.", + "bg.stacks.otherNetworkTx": "This transaction is for a different Stacks network, so it wasn't sent.", + "bg.stacks.badContractCall": "The app's contract call doesn't match the contract on the network. Nothing was sent.", + "bg.stacks.tooManyPending": "This account has too many transactions waiting. Wait for some to finish, then try again.", + "bg.stacks.signatureRefused": "The network didn't accept the signature. Nothing was sent.", + "bg.stacks.testAddressOnMainnet": "That's a Stacks test-network address (ST…). Use a mainnet address (SP…).", + "bg.stacks.mainAddressOnTestnet": "That's a Stacks mainnet address (SP…). Use a test-network address (ST…).", + "bg.stacks.notAnAddress": "That doesn't look like a Stacks address.", + "bg.stacks.needStxForFee": "You need a little STX to pay the network fee.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "Everything left of your {symbol} after this goes to {to}, not back to you.", + "bg.fuel.leftoverLost": "{amount} isn't sent anywhere by this transaction and would be lost.", + "bg.fuel.coinsSpent": "Some of the coins this transaction uses were already spent. Ask the app to try again.", + "bg.fuel.feeRose": "The network fee went up since this was prepared. Nothing was sent. Try again.", + "bg.fuel.failedOnChain": "This transaction failed on the Fuel network. Only the network fee was spent.", + "bg.fuel.tooManyCoins": "This needs too many small coins at once. Send a smaller amount first.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "This app asks Clip Wallet to put its key or signature into a smart contract's data. Clip Wallet can't sign that kind of transaction yet.", + "bg.bch.spendsTokens": "This sends CashTokens from your wallet: {tokens}.", + "bg.bch.highFee": "The network fee is {fee}, which is unusually high.", + "bg.bch.othersInputs": "{count} coins from other owners or contracts", + "bg.bch.notCashAddr": "That doesn't look like a Bitcoin Cash address. Use its CashAddr form (bitcoincash:q…).", + "bg.bch.testAddressOnMainnet": "That's a Bitcoin Cash test-network address (bchtest:…). Use a mainnet address (bitcoincash:…).", + "bg.bch.mainAddressOnTestnet": "That's a Bitcoin Cash mainnet address (bitcoincash:…). Use a test-network address (bchtest:…).", + "bg.bch.tokenSendUnavailable": "Sending CashTokens isn't available yet.", + "bg.bch.belowDust": "Send at least {amount}. Smaller amounts can't be sent on Bitcoin Cash.", + "bg.bch.feeTooLow": "The fee was too low for the network. Nothing was sent. Try again.", + "bg.bch.coinsSpent": "Some coins in this transaction were already spent. Nothing was sent. Refresh and try again.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm (networks87: STRATO) + "bg.label.networkCharge": "Network charge", + "bg.evm.flatFee": "{amount} per transaction, even if it fails", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "{symbol} can't be sent from one Chainflip account to another. To move it, redeem it to an Ethereum address, or fund the other account from Ethereum.", + "bg.chainflip.redeem": "Redeem {amount} to {to} on Ethereum", + "bg.chainflip.redeemAll": "Redeem all your {symbol} to {to} on Ethereum", + "bg.chainflip.allRedeemable": "All your {symbol} that isn't bonded", + "bg.chainflip.label.executor": "Who can finish it", + "bg.chainflip.anyone": "Anyone", + "bg.chainflip.redeemSteps": "After a waiting period, the redemption is finished on Ethereum (that costs ETH gas). If nobody finishes it before it expires, it has to be redone.", + "bg.chainflip.redeemWarn": "Your {symbol} leaves Chainflip for {to} on Ethereum. Check that you control this Ethereum address.", + "bg.chainflip.bindRedeem": "Allow redemptions only to {to}, forever", + "bg.chainflip.bindRedeemWarn": "This can never be undone. This account will only ever redeem {symbol} to {to}. If you don't control that Ethereum address, your {symbol} is locked for good.", + "bg.chainflip.bindExecutor": "Let only {to} finish your redemptions, forever", + "bg.chainflip.bindExecutorWarn": "This can never be undone. Only {to} will be able to finish this account's {symbol} redemptions on Ethereum. If that key is lost, redeemed {symbol} can't be claimed.", + "bg.chainflip.moveTo": "Move {amount} to Chainflip account {to}", + "bg.chainflip.moveAllTo": "Move all your {symbol} to Chainflip account {to}", + "bg.chainflip.otherAccount": "{to} isn't one of your accounts. Check it before you move anything there.", + "bg.chainflip.foreignRecipient": "This goes to {to} on {chain}. Check the address: Clip Wallet can't undo it.", + "bg.chainflip.registerLp": "Register as a Chainflip liquidity provider", + "bg.chainflip.deregisterLp": "Close your Chainflip liquidity provider account", + "bg.chainflip.refundAddress": "Set your {chain} refund address to {to}", + "bg.chainflip.depositAddress": "Get a deposit address for {asset}", + "bg.chainflip.label.boostFee": "Boost fee", + "bg.chainflip.label.refundTo": "Refunds go to", + "bg.chainflip.withdraw": "Withdraw {amount} to {to} on {chain}", + "bg.chainflip.swap": "Swap {amount} for {asset}", + "bg.chainflip.swapChannel": "Open a channel to swap {from} for {to}", + "bg.chainflip.addBoost": "Boost: add {amount} to the {tier} bps pool", + "bg.chainflip.stopBoost": "Stop boosting {asset} in the {tier} bps pool", + "bg.chainflip.lend": "Lend {amount}", + "bg.chainflip.unlend": "Take back {amount} you lent", + "bg.chainflip.unlendAll": "Take back all the {asset} you lent", + "bg.chainflip.borrow": "Borrow {amount}", + "bg.chainflip.repay": "Repay part of loan #{id}", + "bg.chainflip.repayAll": "Repay loan #{id} in full", + "bg.chainflip.onSubAccount": "On sub-account #{n}: {inner}", + "bg.chainflip.orderFee": "Placing or changing an order needs at least 1 {symbol} in this account. Most of it comes back, but changing orders in the same pool many times in one block costs more each time.", + // ---- end chains-substrate (Chainflip) +} as const; diff --git a/packages/core/src/messages/locales/ar.ts b/packages/core/src/messages/locales/ar.ts index c5cbc78b..feeabfd1 100644 --- a/packages/core/src/messages/locales/ar.ts +++ b/packages/core/src/messages/locales/ar.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/ar.js"; /** Arabic: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/ar/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{count, plural, zero {\u2068#\u2069 خطوة} one {خطوة واحدة} two {خطوتان} few {\u2068#\u2069 خطوات} many {\u2068#\u2069 خطوة} other {\u2068#\u2069 خطوة}} على \u2068{app}\u2069", "bg.act.batchStopped": "{count, plural, zero {\u2068#\u2069 خطوة} one {خطوة واحدة} two {خطوتان} few {\u2068#\u2069 خطوات} many {\u2068#\u2069 خطوة} other {\u2068#\u2069 خطوة}} على \u2068{app}\u2069، توقفت عند الخطوة \u2068{n}\u2069", "bg.err.batchHardware": "تنفيذ عدة خطوات بموافقة واحدة غير متاح بعد في المحافظ العتادية. اطلب من التطبيق إرسالها واحدة تلو الأخرى.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/de.ts b/packages/core/src/messages/locales/de.ts index ff5aa5f1..1cbaed63 100644 --- a/packages/core/src/messages/locales/de.ts +++ b/packages/core/src/messages/locales/de.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/de.js"; /** German: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/de/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{count} Schritte auf {app}", "bg.act.batchStopped": "{count} Schritte auf {app}, bei Schritt {n} gestoppt", "bg.err.batchHardware": "Mehrere Schritte in einer Genehmigung gehen mit Hardware-Wallets noch nicht. Bitte die App, sie einzeln zu senden.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/es.ts b/packages/core/src/messages/locales/es.ts index e26f51bb..da66b21c 100644 --- a/packages/core/src/messages/locales/es.ts +++ b/packages/core/src/messages/locales/es.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/es.js"; /** Spanish: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/es/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{count} pasos en {app}", "bg.act.batchStopped": "{count} pasos en {app}, detenido en el paso {n}", "bg.err.batchHardware": "Varios pasos en una sola aprobación todavía no están disponibles para billeteras de hardware. Pide a la app que los envíe de uno en uno.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/fr.ts b/packages/core/src/messages/locales/fr.ts index f807e012..ca3d1199 100644 --- a/packages/core/src/messages/locales/fr.ts +++ b/packages/core/src/messages/locales/fr.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/fr.js"; /** French: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/fr/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{count} étapes sur {app}", "bg.act.batchStopped": "{count} étapes sur {app}, arrêtées à l’étape {n}", "bg.err.batchHardware": "Plusieurs étapes en une seule validation ne sont pas encore possibles avec un portefeuille matériel. Demandez à l’app de les envoyer une par une.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/hi.ts b/packages/core/src/messages/locales/hi.ts index cbe87837..ca1265db 100644 --- a/packages/core/src/messages/locales/hi.ts +++ b/packages/core/src/messages/locales/hi.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/hi.js"; /** Hindi: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/hi/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{app} पर {count} स्टेप", "bg.act.batchStopped": "{app} पर {count} स्टेप, स्टेप {n} पर रुके", "bg.err.batchHardware": "एक ही मंज़ूरी में कई स्टेप अभी हार्डवेयर वॉलेट के लिए उपलब्ध नहीं हैं। ऐप से इन्हें एक-एक करके भेजने को कहें।", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/it.ts b/packages/core/src/messages/locales/it.ts index 0fb99158..2152cccd 100644 --- a/packages/core/src/messages/locales/it.ts +++ b/packages/core/src/messages/locales/it.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/it.js"; /** Italian: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/it/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{count} passaggi su {app}", "bg.act.batchStopped": "{count} passaggi su {app}, interrotti al passaggio {n}", "bg.err.batchHardware": "Più passaggi in un’unica approvazione non sono ancora disponibili con un hardware wallet. Chiedi all’app di inviarli uno alla volta.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/ja.ts b/packages/core/src/messages/locales/ja.ts index f162ca75..24349549 100644 --- a/packages/core/src/messages/locales/ja.ts +++ b/packages/core/src/messages/locales/ja.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/ja.js"; /** Japanese: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/ja/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{app}で{count}ステップ", "bg.act.batchStopped": "{app}で{count}ステップ(ステップ{n}で停止)", "bg.err.batchHardware": "複数のステップを1回の承認で行う機能は、ハードウェアウォレットではまだ使えません。アプリに1つずつ送るよう依頼してください。", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/ko.ts b/packages/core/src/messages/locales/ko.ts index 357caf53..a76cb8fc 100644 --- a/packages/core/src/messages/locales/ko.ts +++ b/packages/core/src/messages/locales/ko.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/ko.js"; /** Korean: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/ko/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{app}에서 {count}단계", "bg.act.batchStopped": "{app}에서 {count}단계, {n}단계에서 멈춤", "bg.err.batchHardware": "여러 단계를 한 번에 승인하는 기능은 아직 하드웨어 지갑에서 쓸 수 없어요. 앱에 하나씩 보내 달라고 요청하세요.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts new file mode 100644 index 00000000..838a0418 --- /dev/null +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Arabic: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../ar.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "قناة IBC \u2068{channel}\u2069", + "bg.cosmos.ibcLeaves": "يرسل هذا الرموز إلى سلسلة أخرى عبر IBC (\u2068{channel}\u2069). تأكد من أن العنوان صحيح على تلك السلسلة.", + "bg.cosmos.unbonding": "تبقى \u2068{symbol}\u2069 الخاصة بك مقفلة ولا تحقق أي عائد حتى تنتهي فترة إلغاء الربط في الشبكة.", + "bg.cosmos.moveStake": "نقل تخزين \u2068{amount}\u2069 من \u2068{from}\u2069 إلى \u2068{to}\u2069", + "bg.cosmos.claimRewardsFrom": "المطالبة بمكافآت التخزين من \u2068{validator}\u2069", + "bg.cosmos.grantAuthz": "السماح لـ \u2068{grantee}\u2069 بالتصرف نيابةً عن حسابك", + "bg.cosmos.authzTakeAll": "يتيح هذا لـ \u2068{grantee}\u2069 نقل أموالك من دون أن يسألك مجددًا، حتى تسحب الإذن. قد يأخذ كل شيء.", + "bg.cosmos.authzActs": "يتيح هذا لـ \u2068{grantee}\u2069 التصرف نيابةً عن حسابك من دون أن يسألك مجددًا، حتى تسحب الإذن. لا تسمح بذلك إلا لتطبيقات تثق بها.", + "bg.cosmos.revokeAuthz": "منع \u2068{grantee}\u2069 من التصرف نيابةً عن حسابك", + "bg.cosmos.grantFees": "السماح لـ \u2068{grantee}\u2069 بدفع رسوم الشبكة من \u2068{symbol}\u2069 الخاصة بك", + "bg.cosmos.feegrantUpTo": "يتيح هذا لـ \u2068{grantee}\u2069 إنفاق ما يصل إلى \u2068{amount}\u2069 من أموالك على رسوم الشبكة، من دون أن يسألك مجددًا.", + "bg.cosmos.feegrantAll": "يتيح هذا لـ \u2068{grantee}\u2069 إنفاق \u2068{symbol}\u2069 الخاصة بك على رسوم الشبكة بلا حد، من دون أن يسألك مجددًا.", + "bg.cosmos.revokeFees": "منع \u2068{grantee}\u2069 من دفع رسوم الشبكة من أموالك", + "bg.cosmos.block": "الكتلة \u2068{height}\u2069", + "bg.cosmos.appGetsSigned": "\u2068{host}\u2069 (يحصل على المعاملة الموقّعة)", + "bg.cosmos.wrongPrefix": "هذا العنوان تابع لشبكة أخرى. تبدأ العناوين على \u2068{network}\u2069 بـ \u2068{prefix}\u2069.", + "bg.cosmos.noAccount": "حسابك غير موجود على \u2068{network}\u2069 بعد. استلم بعض \u2068{symbol}\u2069 أولًا.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "يتحكم مفتاح آخر في حساب TRON هذا، لذا لا تستطيع Clip Wallet التوقيع عنه.", + "bg.tron.notOpen": "حسابك على TRON غير مفتوح بعد. استلم بعض TRX أولًا.", + "bg.tron.contractRecipient": "لا يمكن إرسال TRX مباشرةً إلى عقد ذكي.", + "bg.tron.newAccountFee": "هذا العنوان غير مفتوح على TRON بعد. الإرسال إليه يكلّف أيضًا \u2068{amount}\u2069 لفتحه.", + "bg.tron.getsSigned": "\u2068{host}\u2069 (يتلقى المعاملة الموقّعة)", + "bg.tron.energy": "الطاقة", + "bg.tron.bandwidth": "عرض النطاق", + "bg.tron.tronPower": "قوة التصويت", + "bg.tron.stakeFor": "تخزين \u2068{amount}\u2069 مقابل \u2068{resource}\u2069", + "bg.tron.days": "\u2068{count}\u2069 يومًا", + "bg.tron.cancelUnstaking": "إلغاء عمليات إلغاء التخزين المعلّقة وتخزين تلك TRX مجددًا", + "bg.tron.lend": "إقراض \u2068{resource}\u2069 من \u2068{amount}\u2069 التي خزّنتها إلى \u2068{to}\u2069", + "bg.tron.stopLending": "استرداد \u2068{resource}\u2069 من \u2068{amount}\u2069 التي أقرضتها إلى \u2068{to}\u2069", + "bg.tron.lockedHours": "حوالي \u2068{hours}\u2069 ساعة. لا يمكنك استرداده قبل ذلك.", + "bg.tron.vote": "التصويت لـ \u2068{count}\u2069 من Super Representatives", + "bg.tron.votesReplace": "يحل هذا محل جميع أصواتك السابقة.", + "bg.tron.claimVoteRewards": "المطالبة بمكافآت التصويت", + "bg.tron.changePermissions": "تغيير من يتحكم في حسابك على TRON", + "bg.tron.keysThreshold": "\u2068{keys}\u2069 (يتطلب \u2068{threshold}\u2069)", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "دفع ما يصل إلى \u2068{amount}\u2069 ليحصل \u2068{to}\u2069 على \u2068{get}\u2069", + "bg.xrpl.atMost": "\u2068{amount}\u2069 كحد أقصى", + "bg.xrpl.anyAmount": "أي مبلغ", + "bg.xrpl.partialPayment": "هذه دفعة جزئية: قد يحصل المستلم على أقل بكثير من \u2068{amount}\u2069. خُدعت متاجر ومنصات تداول بهذه الطريقة.", + "bg.xrpl.xrpToSelf": "إرسال XRP إلى حسابك نفسه لا يفعل شيئًا، لذلك سيرفضه XRP Ledger.", + "bg.xrpl.frees": "يحرر هذا الاحتياطي البالغ \u2068{amount}\u2069 الذي كان مقفلًا.", + "bg.xrpl.locksReserve": "يبقى \u2068{amount}\u2069 من XRP لديك مقفلًا ما دام هذا الرمز مضافًا", + "bg.xrpl.freezesLine": "يجمّد هذا الرمز أيضًا لدى الطرف الآخر.", + "bg.xrpl.offer.fillOrKill": "التنفيذ بالكامل الآن أو الإلغاء", + "bg.xrpl.offer.immediate": "تنفيذ الممكن الآن وإلغاء الباقي", + "bg.xrpl.offer.standing": "يبقى مفتوحًا حتى يُنفَّذ أو تنتهي صلاحيته أو تلغيه", + "bg.xrpl.cancelOffer": "إلغاء عرض", + "bg.xrpl.changeSettings": "تغيير إعدادات حسابك", + "bg.xrpl.asf.requireDest": "طلب وسم وجهة (Destination tag) على المدفوعات إليك", + "bg.xrpl.asf.requireAuth": "الموافقة على كل حامل لرموزك", + "bg.xrpl.asf.disallowXrp": "طلب عدم إرسال XRP إليك", + "bg.xrpl.asf.disableMaster": "إيقاف المفتاح الخاص بحسابك", + "bg.xrpl.asf.accountTxnId": "تتبّع معرّف آخر معاملة لك", + "bg.xrpl.asf.noFreeze": "التخلي نهائيًا عن تجميد رموزك", + "bg.xrpl.asf.globalFreeze": "تجميد كل الرموز التي أصدرتها", + "bg.xrpl.asf.defaultRipple": "السماح بانتقال رموزك بين الحاملين (rippling)", + "bg.xrpl.asf.depositAuth": "قبول المدفوعات من الحسابات التي توافق عليها فقط", + "bg.xrpl.asf.nftMinter": "السماح لحساب آخر بإنشاء NFT نيابةً عنك", + "bg.xrpl.asf.noNftOffers": "حظر عروض NFT الواردة", + "bg.xrpl.asf.noChecks": "حظر الشيكات الواردة", + "bg.xrpl.asf.noPayChannels": "حظر قنوات الدفع الواردة", + "bg.xrpl.asf.noTrustLines": "حظر خطوط الثقة (trust lines) الواردة", + "bg.xrpl.asf.clawback": "السماح لك باسترداد الرموز التي تصدرها (clawback)", + "bg.xrpl.asf.lockTokens": "السماح للحاملين بقفل رموزك في ضمان (escrow)", + "bg.xrpl.disableMasterWarning": "يوقف هذا المفتاح الخاص بحسابك. بعد ذلك لا يستطيع استخدام الحساب إلا مفتاح آخر أو قائمة موقّعين؛ وإن لم يوجد أيٌّ منهما يُقفل الحساب نهائيًا.", + "bg.xrpl.issuerSettingWarning": "يغيّر هذا طريقة عمل الرموز التي تصدرها. بعض هذه التغييرات لا يمكن التراجع عنه.", + "bg.xrpl.nftMinterWarning": "يسمح هذا لـ \u2068{who}\u2069 بإنشاء NFT باسم حسابك.", + "bg.xrpl.cleared": "(تم المسح)", + "bg.xrpl.removeRegularKey": "إزالة المفتاح الإضافي من حسابك", + "bg.xrpl.regularKeyWarning": "يحصل \u2068{who}\u2069 على مفتاح يستطيع فعل أي شيء بحسابك، بما في ذلك إفراغه.", + "bg.xrpl.removeSigners": "إزالة الحسابات التي يمكنها التوقيع عن حسابك", + "bg.xrpl.setSigners": "السماح لـ \u2068{count}\u2069 حسابات أخرى بالتوقيع عن حسابك", + "bg.xrpl.signersWarning": "تستطيع هذه الحسابات معًا فعل أي شيء بحسابك، بما في ذلك إفراغه.", + "bg.xrpl.closeAccount": "إغلاق حسابك على XRP Ledger وإرسال كل شيء إلى \u2068{to}\u2069", + "bg.xrpl.closeWarning": "يحذف هذا حسابك على XRP Ledger ويرسل كل XRP لديك إلى \u2068{to}\u2069. تُحرق الرسوم البالغة \u2068{fee}\u2069.", + "bg.xrpl.mintNft": "إنشاء NFT", + "bg.xrpl.taxon": "التصنيف \u2068{taxon}\u2069", + "bg.xrpl.nft.transferable": "يمكن بيعه أو إهداؤه", + "bg.xrpl.nft.notTransferable": "لا يمكن نقله لغيرك", + "bg.xrpl.nft.issuerCanBurn": "يمكن لمنشئه حرقه", + "bg.xrpl.nft.mutable": "يمكن تغيير رابطه لاحقًا", + "bg.xrpl.burnNft": "حرق NFT", + "bg.xrpl.giveNft": "عرض الـ NFT الخاص بك على \u2068{to}\u2069 مجانًا", + "bg.xrpl.freeForAnyone": "يمكن لأي شخص أخذ هذا الـ NFT دون مقابل.", + "bg.xrpl.sellNft": "عرض الـ NFT الخاص بك مقابل \u2068{amount}\u2069", + "bg.xrpl.buyNft": "عرض \u2068{amount}\u2069 مقابل NFT", + "bg.xrpl.offerHolds": "لا يُخصم شيء حتى يقبل المالك", + "bg.xrpl.acceptNftOffer": "قبول عرض NFT", + "bg.xrpl.brokered": "يطابق عرض شراء مع عرض بيع (بيع عبر وسيط)", + "bg.xrpl.offerUnknown": "لم تتمكن Clip Wallet من قراءة هذا العرض، لذلك لا يمكنها أن تُظهر ما تدفعه أو ما تحصل عليه.", + "bg.xrpl.acceptNftSell": "شراء NFT مقابل \u2068{amount}\u2069", + "bg.xrpl.acceptNftBuy": "بيع الـ NFT الخاص بك مقابل \u2068{amount}\u2069", + "bg.xrpl.cancelNftOffers": "إلغاء \u2068{count}\u2069 من عروض NFT", + "bg.xrpl.escrowCreate": "قفل \u2068{amount}\u2069 في ضمان (escrow) لصالح \u2068{to}\u2069", + "bg.xrpl.escrowCondition": "تحريره يتطلب سرًّا (crypto-condition)", + "bg.xrpl.escrowLocked": "تبقى هذه الأموال مقفلة حتى يُحرَّر الضمان أو يُلغى.", + "bg.xrpl.escrowFinish": "تحرير ضمان (escrow)", + "bg.xrpl.escrowCancel": "إلغاء ضمان (escrow)", + "bg.xrpl.ammDeposit": "إضافة أموال إلى مجمع سيولة", + "bg.xrpl.ammWithdraw": "سحب أموال من مجمع سيولة", + "bg.xrpl.yourShare": "حصتك في المجمع", + "bg.xrpl.poolAmounts": "يحدد المجمع المبالغ الدقيقة عند التنفيذ.", + "bg.xrpl.unknownTx": "الموافقة على معاملة \u2068{type}\u2069 على XRP Ledger", + "bg.xrpl.recipientUnchecked": "لم تتمكن Clip Wallet من التحقق من حساب المستلم.", + "bg.xrpl.newAccountTooSmall": "حساب XRP Ledger هذا غير موجود بعد. فتحه يتطلب \u2068{reserve}\u2069 على الأقل، لذلك ستفشل هذه العملية.", + "bg.xrpl.opensAccount": "يفتح هذا حسابًا جديدًا على XRP Ledger؛ ويبقى \u2068{reserve}\u2069 منه مقفلًا فيه كاحتياطي.", + "bg.xrpl.noRecipientAccount": "حساب XRP Ledger هذا غير موجود، لذلك لا يمكنه استلام الرموز.", + "bg.xrpl.destinationTagNeeded": "يحتاج هذا المستلم إلى وسم وجهة (Destination tag) ولم يُحدَّد أي وسم. من دونه قد تضيع الأموال.", + "bg.xrpl.disallowsXrp": "يذكر هذا الحساب أنه لا يريد استلام XRP.", + "bg.xrpl.depositAuth": "يقبل هذا الحساب المدفوعات من الحسابات التي وافق عليها فقط، لذلك قد تفشل العملية.", + "bg.xrpl.noTrustLine": "لم يُضف المستلم \u2068{symbol}\u2069 إلى حسابه، لذلك ستفشل هذه الدفعة.", + "bg.xrpl.notEnoughXrp": "لا يكفي ما لديك من XRP لهذا: يجب أن يبقى \u2068{reserve}\u2069 في حسابك كاحتياطي.", + "bg.xrpl.highFee": "رسوم الشبكة مرتفعة على غير العادة: \u2068{fee}\u2069.", + "bg.xrpl.appSends": "\u2068{host}\u2069 (يحصل على المعاملة الموقّعة)", + "bg.xrpl.feeCharged": "تم خصم رسوم الشبكة رغم ذلك.", + "bg.xrpl.label.destinationTag": "وسم الوجهة (Destination tag)", + "bg.xrpl.label.sourceTag": "وسم المصدر (Source tag)", + "bg.xrpl.label.deliversAtLeast": "يُسلّم على الأقل", + "bg.xrpl.label.issuer": "المُصدِر", + "bg.xrpl.label.replacesOffer": "يحل محل العرض", + "bg.xrpl.label.turnsOn": "يُفعّل", + "bg.xrpl.label.turnsOff": "يُوقف", + "bg.xrpl.label.domain": "النطاق", + "bg.xrpl.label.transferFee": "رسوم التحويل", + "bg.xrpl.label.nftMinter": "منشئ NFT", + "bg.xrpl.label.quorum": "التواقيع المطلوبة", + "bg.xrpl.label.link": "الرابط", + "bg.xrpl.label.royalty": "العائد للمنشئ", + "bg.xrpl.label.releaseAfter": "يمكن تحريره بعد", + "bg.xrpl.label.cancelAfter": "يمكن إلغاؤه بعد", + "bg.xrpl.err.wrongNetwork": "يطلب هذا التطبيق شبكة XRP Ledger مختلفة عن الشبكة المتصل بها. لم يُوقَّع أي شيء.", + "bg.xrpl.err.multisig": "التوقيع المتعدد غير متاح في Clip Wallet بعد.", + "bg.xrpl.err.alreadySigned": "هذه المعاملة موقّعة بالفعل، لذلك لن توقّعها Clip Wallet مرة أخرى.", + "bg.xrpl.err.cantRead": "لا تستطيع Clip Wallet قراءة جزء من معاملة XRP Ledger هذه (\u2068{field}\u2069)، لذلك لا يمكنها توقيعها.", + "bg.xrpl.err.notActivated": "حسابك على XRP Ledger غير مفعّل بعد. استلم \u2068{reserve}\u2069 XRP على الأقل لتفعيله.", + "bg.xrpl.err.otherKey": "المفتاح الخاص بهذا الحساب متوقف ويتحكم فيه مفتاح آخر، لذلك لا يمكن لـ Clip Wallet التوقيع عنه.", + "bg.xrpl.err.xAddressNetwork": "عنوان X هذا تابع لشبكة XRP Ledger أخرى.", + "bg.xrpl.err.badAddress": "لا يبدو هذا عنوانًا على XRP Ledger.", + "bg.xrpl.err.notToken": "هذا ليس رمزًا على XRP Ledger.", + "bg.xrpl.err.tagNeeded": "يحتاج هذا العنوان إلى وسم وجهة (Destination tag). اطلبه من المستلم، أو اطلب عنوان X يتضمنه.", + "bg.xrpl.err.notEnoughXrp": "لا يكفي ما لديك من XRP. يجب أن يبقى \u2068{reserve}\u2069 XRP في حسابك، والرسوم \u2068{fee}\u2069 XRP.", + "bg.xrpl.err.newAccount": "حساب XRP Ledger هذا غير موجود بعد. أرسل \u2068{reserve}\u2069 XRP على الأقل لفتحه.", + "bg.xrpl.err.feeOnly": "تحتاج إلى قليل من XRP لدفع رسوم الشبكة.", + "bg.xrpl.err.noTrustLine": "لم يُضيفوا \u2068{symbol}\u2069 إلى حسابهم على XRP Ledger بعد. اطلب منهم إضافته أولًا.", + "bg.xrpl.err.reserveForToken": "إضافة رمز تقفل \u2068{amount}\u2069 XRP، إضافة إلى الرسوم. أضف بعض XRP أولًا.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "استدعاء \u2068{action}\u2069 على \u2068{contract}\u2069", + "bg.antelope.unknownToken": "\u2068{symbol}\u2069 من \u2068{contract}\u2069 ليس رمزًا تعرفه Clip Wallet. تحقّق من العقد قبل الإرسال.", + "bg.antelope.noRecipient": "لا يوجد حساب باسم \u2068{to}\u2069 على هذه الشبكة، لذلك ستفشل العملية.", + "bg.antelope.updateAuth": "تغيير من يتحكم في الصلاحية \u2068{permission}\u2069 للحساب \u2068{account}\u2069", + "bg.antelope.deleteAuth": "حذف الصلاحية \u2068{permission}\u2069 للحساب \u2068{account}\u2069", + "bg.antelope.linkAuth": "السماح للصلاحية \u2068{permission}\u2069 بتنفيذ \u2068{action}\u2069", + "bg.antelope.unlinkAuth": "منع صلاحية من تنفيذ \u2068{action}\u2069", + "bg.antelope.authWarning": "يغيّر هذا المفاتيح أو الحسابات التي تتحكم في \u2068{account}\u2069. إن لم تكن لك، فقد تأخذ كل ما فيه.", + "bg.antelope.unlinkWarning": "يغيّر هذا المفتاح الذي يمكنه تنفيذ بعض إجراءات حسابك.", + "bg.antelope.label.alsoSigns": "يحتاج أيضًا إلى", + "bg.antelope.delayed": "لا يُنفَّذ هذا إلا بعد \u2068{seconds}\u2069 ثانية من إرساله، ويمكن إلغاؤه حتى ذلك الحين.", + "bg.antelope.feeFree": "لا شيء (هذه الشبكة تتكفّل بـ CPU وNET عنك)", + "bg.antelope.feeResources": "لا رسوم بالرموز: يستخدم CPU وNET الخاصين بحسابك", + "bg.antelope.appSends": "\u2068{host}\u2069 (يحصل على المعاملة الموقّعة)", + "bg.antelope.err.offline": "تعذّر الوصول إلى \u2068{network}\u2069. تحقّق من اتصالك وحاول مرة أخرى.", + "bg.antelope.err.wrongNetwork": "هذا الطلب لشبكة Antelope غير الشبكة التي تستخدمها. لم يُوقَّع أي شيء.", + "bg.antelope.err.contextFree": "تستخدم هذه المعاملة ميزات لا توقّعها Clip Wallet (إجراءات بلا سياق أو امتدادات).", + "bg.antelope.err.noAbi": "لا تستطيع Clip Wallet قراءة العقد \u2068{contract}\u2069، لذلك لا يمكنها إعداد هذا الطلب.", + "bg.antelope.err.permission": "يحتاج هذا إلى صلاحية في حسابك لا يتحكم فيها هذا المفتاح.", + "bg.antelope.err.otherSigners": "تحتاج هذه المعاملة أيضًا إلى توقيع حسابات أخرى، لذلك لا تستطيع Clip Wallet إرسالها وحدها.", + "bg.antelope.err.noAccountTestnet": "لا يوجد بعد حساب على \u2068{network}\u2069 يستخدم هذا المفتاح. الحسابات على شبكات Antelope أسماء تُنشأ على السلسلة: أنشئ حساب اختبار مجانيًا لمفتاحك العام بأداة الشبكة التجريبية، ثم عُد.", + "bg.antelope.err.noAccount": "لا يوجد بعد حساب على \u2068{network}\u2069 يستخدم هذا المفتاح. الحسابات على شبكات Antelope أسماء يُنشئها أحدٌ على السلسلة لمفتاحك العام: منصة تداول، أو صديق لديه حساب، أو خدمة لإنشاء الحسابات.", + "bg.antelope.err.notToken": "هذا ليس رمزًا على هذه الشبكة.", + "bg.antelope.err.badAccount": "هذا ليس اسم حساب صالحًا. الأسماء حتى 12 حرفًا: a–z و1–5 والنقاط.", + "bg.antelope.err.cpuPowerup": "نفد وقت CPU أو NET لحسابك على \u2068{network}\u2069. استأجر بعضه عبر PowerUp (يكلّف قليلًا من EOS لمدة 24 ساعة)، ثم حاول مرة أخرى.", + "bg.antelope.err.cpuStake": "نفد وقت CPU أو NET لحسابك على \u2068{network}\u2069. خزّن بعض TLOS للحصول على CPU وNET، أو استأجرهما عبر PowerUp، ثم حاول مرة أخرى.", + "bg.antelope.err.cpuFree": "لم تتمكن \u2068{network}\u2069 من تغطية CPU أو NET لهذا الآن. انتظر قليلًا وحاول مرة أخرى.", + "bg.antelope.err.ram": "لا يملك حسابك ما يكفي من RAM على \u2068{network}\u2069 لهذا. اشترِ قليلًا من RAM، ثم حاول مرة أخرى.", + "bg.antelope.err.balance": "لا يكفي ما لديك من هذا الرمز لذلك.", + "bg.antelope.err.expired": "انتهت صلاحية هذه المعاملة قبل إرسالها. حاول مرة أخرى.", + "bg.antelope.err.auth": "تقول الشبكة إن هذا المفتاح غير مسموح له بالتوقيع عن ذلك الحساب. لم يُرسَل أي شيء.", + "bg.antelope.err.noRecipient": "حساب المستلم غير موجود على هذه الشبكة. لم يُرسَل أي شيء.", + "bg.antelope.err.duplicate": "أُرسلت هذه المعاملة نفسها من قبل. راجع النشاط.", + "bg.antelope.err.rejected": "رفضت \u2068{network}\u2069 هذه المعاملة. لم يُرسَل أي شيء.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "استلام مكافآت التخزين من \u2068{validator}\u2069", + "bg.multiversx.withdrawFrom": "سحب EGLD الذي أُلغي تخزينه من \u2068{validator}\u2069", + "bg.multiversx.restakeRewardsWith": "إعادة تخزين مكافآتك لدى \u2068{validator}\u2069", + "bg.multiversx.labelGuardian": "الحارس", + "bg.multiversx.setGuardianTitle": "جعل \u2068{guardian}\u2069 حارسًا لحسابك", + "bg.multiversx.setGuardianWarn": "بعد تفعيل الحارس، تحتاج كل معاملة من هذا الحساب إلى توقيعه المشترك. إذا لم تختر \u2068{guardian}\u2069 بنفسك، فقد يمنعك شخص ما من الوصول إلى حسابك.", + "bg.multiversx.guardAccountTitle": "تشغيل حارس حسابك", + "bg.multiversx.guardAccountWarn": "من الآن، تحتاج كل معاملة من هذا الحساب إلى التوقيع المشترك للحارس. لا يستطيع Clip Wallet توفيره، لذا لن تتمكن من الإرسال من Clip Wallet بعد الآن.", + "bg.multiversx.unguardAccountTitle": "إيقاف حارس حسابك", + "bg.multiversx.changeOwnerTitle": "تسليم العقد \u2068{contract}\u2069 إلى \u2068{owner}\u2069", + "bg.multiversx.changeOwnerWarn": "يجعل هذا \u2068{owner}\u2069 مالك العقد \u2068{contract}\u2069. يستطيع المالك الجديد تغيير شيفرته وأخذ ما فيه. لا تفعل ذلك إلا إذا كنت تريد التنازل عنه فعلًا.", + "bg.multiversx.guardedAccount": "لهذا الحساب حارس، ولا يستطيع Clip Wallet الحصول على توقيعه المشترك. لم يُرسَل أي شيء.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "يرسل هذا إلى معرّف حساب، وهو نوع عنوان الإيداع الذي تعطيه منصات التداول. تأكد من أنه يطابق تمامًا ما تعرضه المنصة.", + "bg.icp.expired": "انتهت صلاحية هذا التحويل قبل أن توافق عليه. لم يُرسَل أي شيء. حاول مرة أخرى.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "شرط لاحق", + "bg.stacks.pcYouSendExactly": "ترسل \u2068{amount}\u2069 بالضبط", + "bg.stacks.pcYouSendAtMost": "ترسل \u2068{amount}\u2069 على الأكثر", + "bg.stacks.pcYouSendAtLeast": "ترسل \u2068{amount}\u2069 على الأقل", + "bg.stacks.pcYouSendMoreThan": "ترسل أكثر من \u2068{amount}\u2069", + "bg.stacks.pcYouSendLessThan": "ترسل أقل من \u2068{amount}\u2069", + "bg.stacks.pcSendsExactly": "يرسل \u2068{who}\u2069 \u2068{amount}\u2069 بالضبط", + "bg.stacks.pcSendsAtMost": "يرسل \u2068{who}\u2069 \u2068{amount}\u2069 على الأكثر", + "bg.stacks.pcSendsAtLeast": "يرسل \u2068{who}\u2069 \u2068{amount}\u2069 على الأقل", + "bg.stacks.pcSendsMoreThan": "يرسل \u2068{who}\u2069 أكثر من \u2068{amount}\u2069", + "bg.stacks.pcSendsLessThan": "يرسل \u2068{who}\u2069 أقل من \u2068{amount}\u2069", + "bg.stacks.pcYouSendNft": "ترسل \u2068{item}\u2069", + "bg.stacks.pcYouKeepNft": "تحتفظ بـ \u2068{item}\u2069", + "bg.stacks.pcYouMaySendNft": "قد ترسل \u2068{item}\u2069", + "bg.stacks.pcSendsNft": "يرسل \u2068{who}\u2069 \u2068{item}\u2069", + "bg.stacks.pcKeepsNft": "يحتفظ \u2068{who}\u2069 بـ \u2068{item}\u2069", + "bg.stacks.pcMaySendNft": "قد يرسل \u2068{who}\u2069 \u2068{item}\u2069", + "bg.stacks.pcStakingRule": "قاعدة تخزين لـ \u2068{who}\u2069", + "bg.stacks.allowMode": "يتيح هذا للعقد نقل أي من أصولك، لا الأصول المدرجة فقط. لا توافق عليه إلا إذا كنت تثق بـ \u2068{host}\u2069 ثقة تامة.", + "bg.stacks.originatorMode": "لا يمكن أن تغادر حسابك إلا التحويلات المدرجة. لكن قد يظل العقد قادرًا على نقل أصول حسابات أخرى.", + "bg.stacks.nothingLeaves": "لا يمكن لأي من أصولك أن يغادر حسابك في هذه المعاملة، باستثناء رسوم الشبكة.", + "bg.stacks.highFee": "رسوم الشبكة \u2068{fee}\u2069، وهي مرتفعة على نحو غير معتاد.", + "bg.stacks.sponsorPays": "راعٍ اختاره \u2068{host}\u2069", + "bg.stacks.deployNamed": "إنشاء العقد الذكي \u2068{name}\u2069", + "bg.stacks.notYourTx": "هذه المعاملة موقّعة من حساب Stacks مختلف، لذا لا تستطيع Clip Wallet توقيعها.", + "bg.stacks.multisig": "لا تستطيع Clip Wallet بعدُ التوقيع لحسابات Stacks المشتركة (متعددة التوقيعات).", + "bg.stacks.memoTooLong": "المذكرة (Memo) طويلة جدًا. تتسع مذكرة Stacks لـ 34 بايت كحد أقصى.", + "bg.stacks.feeTooLow": "كانت رسوم الشبكة منخفضة جدًا، لذا لم تقبل الشبكة المعاملة. لم يُرسَل أي شيء. حاول مرة أخرى.", + "bg.stacks.nonceBusy": "لا تزال معاملة أخرى من هذا الحساب قيد الانتظار. انتظر حتى تكتمل، ثم حاول مرة أخرى.", + "bg.stacks.otherNetworkTx": "هذه المعاملة لشبكة Stacks مختلفة، لذا لم تُرسَل.", + "bg.stacks.badContractCall": "استدعاء العقد من التطبيق لا يطابق العقد على الشبكة. لم يُرسَل أي شيء.", + "bg.stacks.tooManyPending": "لدى هذا الحساب معاملات كثيرة جدًا قيد الانتظار. انتظر حتى يكتمل بعضها، ثم حاول مرة أخرى.", + "bg.stacks.signatureRefused": "لم تقبل الشبكة التوقيع. لم يُرسَل أي شيء.", + "bg.stacks.testAddressOnMainnet": "هذا عنوان Stacks على شبكة تجريبية (ST…). استخدم عنوانًا على الشبكة الرئيسية (SP…).", + "bg.stacks.mainAddressOnTestnet": "هذا عنوان Stacks على الشبكة الرئيسية (SP…). استخدم عنوانًا على شبكة تجريبية (ST…).", + "bg.stacks.notAnAddress": "لا يبدو هذا عنوان Stacks.", + "bg.stacks.needStxForFee": "تحتاج إلى قليل من STX لدفع رسوم الشبكة.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "كل ما يتبقى من \u2068{symbol}\u2069 بعد هذا يذهب إلى \u2068{to}\u2069، ولا يعود إليك.", + "bg.fuel.leftoverLost": "لا تُرسل هذه المعاملة \u2068{amount}\u2069 إلى أي مكان، وسيضيع.", + "bg.fuel.coinsSpent": "بعض العملات التي تستخدمها هذه المعاملة أُنفقت بالفعل. اطلب من التطبيق المحاولة مرة أخرى.", + "bg.fuel.feeRose": "ارتفعت رسوم الشبكة منذ تجهيز هذا. لم يُرسَل أي شيء. حاول مرة أخرى.", + "bg.fuel.failedOnChain": "فشلت هذه المعاملة على شبكة Fuel. لم يُدفع إلا رسوم الشبكة.", + "bg.fuel.tooManyCoins": "يحتاج هذا إلى عدد كبير جدًا من العملات الصغيرة في وقت واحد. أرسل مبلغًا أصغر أولًا.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "يطلب هذا التطبيق من Clip Wallet وضع مفتاحها أو توقيعها في بيانات عقد ذكي. لا تستطيع Clip Wallet بعدُ توقيع هذا النوع من المعاملات.", + "bg.bch.spendsTokens": "يُرسل هذا CashTokens من محفظتك: \u2068{tokens}\u2069.", + "bg.bch.highFee": "رسوم الشبكة \u2068{fee}\u2069، وهي مرتفعة على نحو غير معتاد.", + "bg.bch.othersInputs": "عملات من مالكين آخرين أو عقود: \u2068{count}\u2069", + "bg.bch.notCashAddr": "لا يبدو هذا عنوان Bitcoin Cash. استخدم صيغة CashAddr الخاصة به (bitcoincash:q…).", + "bg.bch.testAddressOnMainnet": "هذا عنوان Bitcoin Cash على شبكة تجريبية (bchtest:…). استخدم عنوانًا على الشبكة الرئيسية (bitcoincash:…).", + "bg.bch.mainAddressOnTestnet": "هذا عنوان Bitcoin Cash على الشبكة الرئيسية (bitcoincash:…). استخدم عنوانًا على شبكة تجريبية (bchtest:…).", + "bg.bch.tokenSendUnavailable": "إرسال CashTokens غير متاح بعد.", + "bg.bch.belowDust": "أرسل \u2068{amount}\u2069 على الأقل. لا يمكن إرسال مبالغ أصغر على Bitcoin Cash.", + "bg.bch.feeTooLow": "كانت الرسوم منخفضة جدًا بالنسبة إلى الشبكة. لم يُرسَل أي شيء. حاول مرة أخرى.", + "bg.bch.coinsSpent": "بعض العملات في هذه المعاملة أُنفقت بالفعل. لم يُرسَل أي شيء. حدِّث ثم حاول مرة أخرى.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "رسم ثابت للشبكة", + "bg.evm.flatFee": "⁨{amount}⁩ لكل معاملة، حتى إن فشلت", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "لا يمكن إرسال ⁨{symbol}⁩ من حساب Chainflip إلى حساب آخر. لنقله، استرده إلى عنوان على Ethereum، أو موّل الحساب الآخر من Ethereum.", + "bg.chainflip.redeem": "استرداد ⁨{amount}⁩ إلى ⁨{to}⁩ على Ethereum", + "bg.chainflip.redeemAll": "استرداد كل ⁨{symbol}⁩ لديك إلى ⁨{to}⁩ على Ethereum", + "bg.chainflip.allRedeemable": "كل ⁨{symbol}⁩ لديك غير المرهون", + "bg.chainflip.label.executor": "من يمكنه إتمامه", + "bg.chainflip.anyone": "أي شخص", + "bg.chainflip.redeemSteps": "بعد فترة انتظار، يُتمّ الاسترداد على Ethereum (وهذا يكلّف رسوم غاز بعملة ETH). إن لم يُتمّه أحد قبل انتهاء صلاحيته، فيجب إعادته.", + "bg.chainflip.redeemWarn": "سيغادر ⁨{symbol}⁩ الخاص بك Chainflip إلى ⁨{to}⁩ على Ethereum. تحقق من أنك تتحكم في عنوان Ethereum هذا.", + "bg.chainflip.bindRedeem": "السماح بالاسترداد إلى ⁨{to}⁩ فقط، إلى الأبد", + "bg.chainflip.bindRedeemWarn": "لا يمكن التراجع عن هذا أبدًا. لن يسترد هذا الحساب ⁨{symbol}⁩ إلا إلى ⁨{to}⁩. إن لم تكن تتحكم في عنوان Ethereum هذا، فسيبقى ⁨{symbol}⁩ مقفلًا إلى الأبد.", + "bg.chainflip.bindExecutor": "السماح لـ ⁨{to}⁩ وحده بإتمام عمليات الاسترداد، إلى الأبد", + "bg.chainflip.bindExecutorWarn": "لا يمكن التراجع عن هذا أبدًا. لن يستطيع إتمام عمليات استرداد ⁨{symbol}⁩ لهذا الحساب على Ethereum إلا ⁨{to}⁩. إن ضاع ذلك المفتاح، فلن يمكن استلام ⁨{symbol}⁩ المسترد.", + "bg.chainflip.moveTo": "نقل ⁨{amount}⁩ إلى حساب Chainflip ⁨{to}⁩", + "bg.chainflip.moveAllTo": "نقل كل ⁨{symbol}⁩ لديك إلى حساب Chainflip ⁨{to}⁩", + "bg.chainflip.otherAccount": "⁨{to}⁩ ليس من حساباتك. تحقق منه قبل نقل أي شيء إليه.", + "bg.chainflip.foreignRecipient": "سيذهب هذا إلى ⁨{to}⁩ على ⁨{chain}⁩. تحقق من العنوان: لا تستطيع Clip Wallet التراجع عنه.", + "bg.chainflip.registerLp": "التسجيل كمزوّد سيولة في Chainflip", + "bg.chainflip.deregisterLp": "إغلاق حساب مزوّد السيولة في Chainflip", + "bg.chainflip.refundAddress": "تعيين عنوان الاسترجاع على ⁨{chain}⁩ إلى ⁨{to}⁩", + "bg.chainflip.depositAddress": "الحصول على عنوان إيداع لـ ⁨{asset}⁩", + "bg.chainflip.label.boostFee": "رسوم Boost", + "bg.chainflip.label.refundTo": "تذهب المبالغ المستردة إلى", + "bg.chainflip.withdraw": "سحب ⁨{amount}⁩ إلى ⁨{to}⁩ على ⁨{chain}⁩", + "bg.chainflip.swap": "مبادلة ⁨{amount}⁩ مقابل ⁨{asset}⁩", + "bg.chainflip.swapChannel": "فتح قناة لمبادلة ⁨{from}⁩ مقابل ⁨{to}⁩", + "bg.chainflip.addBoost": "Boost: إضافة ⁨{amount}⁩ إلى مجمّع ⁨{tier}⁩ bps", + "bg.chainflip.stopBoost": "إيقاف Boost لـ ⁨{asset}⁩ في مجمّع ⁨{tier}⁩ bps", + "bg.chainflip.lend": "إقراض ⁨{amount}⁩", + "bg.chainflip.unlend": "استعادة ⁨{amount}⁩ أقرضتها", + "bg.chainflip.unlendAll": "استعادة كل ⁨{asset}⁩ الذي أقرضته", + "bg.chainflip.borrow": "اقتراض ⁨{amount}⁩", + "bg.chainflip.repay": "سداد جزء من القرض #⁨{id}⁩", + "bg.chainflip.repayAll": "سداد القرض #⁨{id}⁩ بالكامل", + "bg.chainflip.onSubAccount": "على الحساب الفرعي #⁨{n}⁩: ⁨{inner}⁩", + "bg.chainflip.orderFee": "يتطلب وضع أمر أو تغييره وجود 1 ⁨{symbol}⁩ على الأقل في هذا الحساب. يعود معظمه إليك، لكن تغيير الأوامر في المجمّع نفسه مرات كثيرة في الكتلة نفسها يرفع الرسوم في كل مرة.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts new file mode 100644 index 00000000..8d7f01ba --- /dev/null +++ b/packages/core/src/messages/locales/n87/de.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** German: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../de.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "IBC-Kanal {channel}", + "bg.cosmos.ibcLeaves": "Damit gehen die Token über IBC ({channel}) an eine andere Chain. Prüfe, ob die Adresse auf dieser Chain stimmt.", + "bg.cosmos.unbonding": "Deine {symbol} bleiben gesperrt und bringen nichts ein, bis die Unbonding-Frist des Netzwerks endet.", + "bg.cosmos.moveStake": "{amount} Stake von {from} zu {to} verschieben", + "bg.cosmos.claimRewardsFrom": "Staking-Belohnungen von {validator} abholen", + "bg.cosmos.grantAuthz": "{grantee} für dein Konto handeln lassen", + "bg.cosmos.authzTakeAll": "Damit kann {grantee} dein Guthaben bewegen, ohne erneut zu fragen, bis du die Berechtigung zurücknimmst. {grantee} könnte alles nehmen.", + "bg.cosmos.authzActs": "Damit kann {grantee} für dein Konto handeln, ohne erneut zu fragen, bis du die Berechtigung zurücknimmst. Erlaube das nur Apps, denen du vertraust.", + "bg.cosmos.revokeAuthz": "{grantee} nicht mehr für dein Konto handeln lassen", + "bg.cosmos.grantFees": "{grantee} Netzwerkgebühren mit deinen {symbol} bezahlen lassen", + "bg.cosmos.feegrantUpTo": "Damit kann {grantee} bis zu {amount} von deinem Guthaben für Netzwerkgebühren ausgeben, ohne erneut zu fragen.", + "bg.cosmos.feegrantAll": "Damit kann {grantee} deine {symbol} ohne Limit für Netzwerkgebühren ausgeben, ohne erneut zu fragen.", + "bg.cosmos.revokeFees": "{grantee} keine Netzwerkgebühren mehr mit deinem Guthaben bezahlen lassen", + "bg.cosmos.block": "Block {height}", + "bg.cosmos.appGetsSigned": "{host} (erhält die signierte Transaktion)", + "bg.cosmos.wrongPrefix": "Diese Adresse gehört zu einem anderen Netzwerk. Adressen auf {network} beginnen mit {prefix}.", + "bg.cosmos.noAccount": "Dein Konto ist noch nicht auf {network}. Empfange zuerst etwas {symbol}.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "Dieses TRON-Konto wird von einem anderen Schlüssel kontrolliert, deshalb kann Clip Wallet dafür nicht signieren.", + "bg.tron.notOpen": "Dein TRON-Konto ist noch nicht eröffnet. Empfange zuerst etwas TRX.", + "bg.tron.contractRecipient": "TRX kann nicht direkt an einen Smart Contract gesendet werden.", + "bg.tron.newAccountFee": "Diese Adresse ist auf TRON noch nicht eröffnet. Eine Sendung dorthin kostet zusätzlich {amount}, um sie zu eröffnen.", + "bg.tron.getsSigned": "{host} (erhält die signierte Transaktion)", + "bg.tron.energy": "Energie", + "bg.tron.bandwidth": "Bandbreite", + "bg.tron.tronPower": "Stimmrecht", + "bg.tron.stakeFor": "{amount} für {resource} staken", + "bg.tron.days": "{count} Tage", + "bg.tron.cancelUnstaking": "Ausstehende Auszahlungen aus dem Staking abbrechen und diese TRX wieder staken", + "bg.tron.lend": "{resource} aus deinen gestakten {amount} an {to} verleihen", + "bg.tron.stopLending": "{resource} aus {amount}, verliehen an {to}, zurücknehmen", + "bg.tron.lockedHours": "Etwa {hours} Stunden. Vorher kannst du es nicht zurücknehmen.", + "bg.tron.vote": "Für {count} Super Representatives stimmen", + "bg.tron.votesReplace": "Das ersetzt alle deine bisherigen Stimmen.", + "bg.tron.claimVoteRewards": "Deine Belohnungen fürs Abstimmen abholen", + "bg.tron.changePermissions": "Ändern, wer dein TRON-Konto kontrolliert", + "bg.tron.keysThreshold": "{keys} (benötigt {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "Bis zu {amount} zahlen, damit {to} {get} bekommt", + "bg.xrpl.atMost": "Höchstens {amount}", + "bg.xrpl.anyAmount": "Beliebiger Betrag", + "bg.xrpl.partialPayment": "Das ist eine Teilzahlung: Der Empfänger bekommt eventuell viel weniger als {amount}. Shops und Börsen wurden so schon getäuscht.", + "bg.xrpl.xrpToSelf": "XRP an dein eigenes Konto zu senden bewirkt nichts, deshalb lehnt das XRP Ledger es ab.", + "bg.xrpl.frees": "Damit wird die gesperrte Reserve von {amount} wieder frei.", + "bg.xrpl.locksReserve": "{amount} deiner XRP bleiben gesperrt, solange dieser Token hinzugefügt ist", + "bg.xrpl.freezesLine": "Damit wird der Token auch für die Gegenseite eingefroren.", + "bg.xrpl.offer.fillOrKill": "Jetzt komplett ausführen oder stornieren", + "bg.xrpl.offer.immediate": "Jetzt so viel wie möglich ausführen, den Rest stornieren", + "bg.xrpl.offer.standing": "Bleibt offen, bis es ausgeführt wird, abläuft oder du es stornierst", + "bg.xrpl.cancelOffer": "Angebot stornieren", + "bg.xrpl.changeSettings": "Deine Kontoeinstellungen ändern", + "bg.xrpl.asf.requireDest": "Destination-Tag für Zahlungen an dich verlangen", + "bg.xrpl.asf.requireAuth": "Jeden Halter deiner Token einzeln genehmigen", + "bg.xrpl.asf.disallowXrp": "Andere bitten, dir kein XRP zu senden", + "bg.xrpl.asf.disableMaster": "Den eigenen Schlüssel deines Kontos abschalten", + "bg.xrpl.asf.accountTxnId": "Die ID deiner letzten Transaktion mitführen", + "bg.xrpl.asf.noFreeze": "Für immer darauf verzichten, deine Token einzufrieren", + "bg.xrpl.asf.globalFreeze": "Alle von dir ausgegebenen Token einfrieren", + "bg.xrpl.asf.defaultRipple": "Deine Token zwischen Haltern wandern lassen (Rippling)", + "bg.xrpl.asf.depositAuth": "Nur Zahlungen von Konten annehmen, die du genehmigst", + "bg.xrpl.asf.nftMinter": "Einem anderen Konto erlauben, NFTs für dich zu erstellen", + "bg.xrpl.asf.noNftOffers": "Eingehende NFT-Angebote blockieren", + "bg.xrpl.asf.noChecks": "Eingehende Schecks blockieren", + "bg.xrpl.asf.noPayChannels": "Eingehende Zahlungskanäle blockieren", + "bg.xrpl.asf.noTrustLines": "Eingehende Trust Lines blockieren", + "bg.xrpl.asf.clawback": "Zurückholen deiner ausgegebenen Token erlauben (Clawback)", + "bg.xrpl.asf.lockTokens": "Haltern erlauben, deine Token in Escrow zu sperren", + "bg.xrpl.disableMasterWarning": "Damit wird der eigene Schlüssel deines Kontos abgeschaltet. Danach kann nur noch ein anderer Schlüssel oder eine Signer-Liste das Konto nutzen; gibt es keine, ist es für immer gesperrt.", + "bg.xrpl.issuerSettingWarning": "Das ändert, wie sich von dir ausgegebene Token verhalten. Manches davon lässt sich nicht rückgängig machen.", + "bg.xrpl.nftMinterWarning": "Damit kann {who} NFTs im Namen deines Kontos erstellen.", + "bg.xrpl.cleared": "(gelöscht)", + "bg.xrpl.removeRegularKey": "Den zusätzlichen Schlüssel von deinem Konto entfernen", + "bg.xrpl.regularKeyWarning": "{who} bekommt einen Schlüssel, mit dem sich alles mit deinem Konto machen lässt, auch es leeren.", + "bg.xrpl.removeSigners": "Die Konten entfernen, die für deins signieren können", + "bg.xrpl.setSigners": "{count} anderen Konten erlauben, für deins zu signieren", + "bg.xrpl.signersWarning": "Zusammen können diese Konten alles mit deinem Konto machen, auch es leeren.", + "bg.xrpl.closeAccount": "Dein XRP-Ledger-Konto schließen und alles an {to} senden", + "bg.xrpl.closeWarning": "Damit wird dein XRP-Ledger-Konto gelöscht und dein gesamtes XRP an {to} gesendet. Die Gebühr von {fee} wird verbrannt.", + "bg.xrpl.mintNft": "Ein NFT erstellen", + "bg.xrpl.taxon": "Taxon {taxon}", + "bg.xrpl.nft.transferable": "Kann verkauft oder verschenkt werden", + "bg.xrpl.nft.notTransferable": "Kann nicht weitergegeben werden", + "bg.xrpl.nft.issuerCanBurn": "Der Ersteller kann es verbrennen", + "bg.xrpl.nft.mutable": "Sein Link kann später geändert werden", + "bg.xrpl.burnNft": "Ein NFT verbrennen", + "bg.xrpl.giveNft": "Dein NFT {to} kostenlos anbieten", + "bg.xrpl.freeForAnyone": "Jeder kann dieses NFT umsonst nehmen.", + "bg.xrpl.sellNft": "Dein NFT für {amount} anbieten", + "bg.xrpl.buyNft": "{amount} für ein NFT bieten", + "bg.xrpl.offerHolds": "Es wird erst etwas abgebucht, wenn der Besitzer annimmt", + "bg.xrpl.acceptNftOffer": "Ein NFT-Angebot annehmen", + "bg.xrpl.brokered": "Bringt ein Kauf- und ein Verkaufsangebot zusammen (vermittelter Verkauf)", + "bg.xrpl.offerUnknown": "Clip Wallet konnte dieses Angebot nicht abrufen und kann daher nicht zeigen, was du zahlst oder bekommst.", + "bg.xrpl.acceptNftSell": "Ein NFT für {amount} kaufen", + "bg.xrpl.acceptNftBuy": "Dein NFT für {amount} verkaufen", + "bg.xrpl.cancelNftOffers": "{count} NFT-Angebote stornieren", + "bg.xrpl.escrowCreate": "{amount} für {to} in Escrow sperren", + "bg.xrpl.escrowCondition": "Zum Freigeben ist ein Geheimnis nötig (Crypto-Condition)", + "bg.xrpl.escrowLocked": "Dieses Guthaben bleibt gesperrt, bis das Escrow freigegeben oder storniert wird.", + "bg.xrpl.escrowFinish": "Ein Escrow freigeben", + "bg.xrpl.escrowCancel": "Ein Escrow stornieren", + "bg.xrpl.ammDeposit": "Guthaben in einen Liquiditätspool einzahlen", + "bg.xrpl.ammWithdraw": "Guthaben aus einem Liquiditätspool abheben", + "bg.xrpl.yourShare": "Dein Anteil am Pool", + "bg.xrpl.poolAmounts": "Die genauen Beträge legt der Pool bei der Ausführung fest.", + "bg.xrpl.unknownTx": "Eine XRP-Ledger-Transaktion vom Typ {type} genehmigen", + "bg.xrpl.recipientUnchecked": "Clip Wallet konnte das Konto des Empfängers nicht prüfen.", + "bg.xrpl.newAccountTooSmall": "Dieses XRP-Ledger-Konto gibt es noch nicht. Zum Eröffnen braucht es mindestens {reserve}, deshalb schlägt das fehl.", + "bg.xrpl.opensAccount": "Damit wird ein neues XRP-Ledger-Konto eröffnet; {reserve} davon bleiben dort als Reserve gesperrt.", + "bg.xrpl.noRecipientAccount": "Dieses XRP-Ledger-Konto gibt es nicht, deshalb kann es keine Token empfangen.", + "bg.xrpl.destinationTagNeeded": "Dieser Empfänger braucht einen Destination-Tag, und es ist keiner angegeben. Ohne ihn kann das Guthaben verloren gehen.", + "bg.xrpl.disallowsXrp": "Dieses Konto gibt an, dass es kein XRP empfangen möchte.", + "bg.xrpl.depositAuth": "Dieses Konto nimmt nur Zahlungen von Konten an, die es zugelassen hat; das kann fehlschlagen.", + "bg.xrpl.noTrustLine": "Der Empfänger hat {symbol} nicht zu seinem Konto hinzugefügt, deshalb schlägt diese Zahlung fehl.", + "bg.xrpl.notEnoughXrp": "Du hast nicht genug XRP dafür: {reserve} müssen als Reserve auf deinem Konto bleiben.", + "bg.xrpl.highFee": "Die Netzwerkgebühr ist ungewöhnlich hoch: {fee}.", + "bg.xrpl.appSends": "{host} (bekommt die signierte Transaktion)", + "bg.xrpl.feeCharged": "Die Netzwerkgebühr wurde trotzdem berechnet.", + "bg.xrpl.label.destinationTag": "Destination-Tag", + "bg.xrpl.label.sourceTag": "Source-Tag", + "bg.xrpl.label.deliversAtLeast": "Liefert mindestens", + "bg.xrpl.label.issuer": "Herausgeber", + "bg.xrpl.label.replacesOffer": "Ersetzt Angebot", + "bg.xrpl.label.turnsOn": "Schaltet ein", + "bg.xrpl.label.turnsOff": "Schaltet aus", + "bg.xrpl.label.domain": "Domain", + "bg.xrpl.label.transferFee": "Transfergebühr", + "bg.xrpl.label.nftMinter": "NFT-Ersteller", + "bg.xrpl.label.quorum": "Nötige Signaturen", + "bg.xrpl.label.link": "Link", + "bg.xrpl.label.royalty": "Lizenzgebühr", + "bg.xrpl.label.releaseAfter": "Freigabe möglich ab", + "bg.xrpl.label.cancelAfter": "Stornierung möglich ab", + "bg.xrpl.err.wrongNetwork": "Diese App fragt nach einem anderen XRP-Ledger-Netzwerk als dem, mit dem sie verbunden ist. Es wurde nichts signiert.", + "bg.xrpl.err.multisig": "Multi-Signing gibt es in Clip Wallet noch nicht.", + "bg.xrpl.err.alreadySigned": "Diese Transaktion ist schon signiert, deshalb signiert Clip Wallet sie nicht noch einmal.", + "bg.xrpl.err.cantRead": "Clip Wallet kann einen Teil dieser XRP-Ledger-Transaktion nicht lesen ({field}) und kann sie daher nicht signieren.", + "bg.xrpl.err.notActivated": "Dein XRP-Ledger-Konto ist noch nicht aktiviert. Empfange mindestens {reserve} XRP, um es zu aktivieren.", + "bg.xrpl.err.otherKey": "Der eigene Schlüssel dieses Kontos ist abgeschaltet und ein anderer Schlüssel steuert es, deshalb kann Clip Wallet nicht dafür signieren.", + "bg.xrpl.err.xAddressNetwork": "Diese X-Adresse gehört zu einem anderen XRP-Ledger-Netzwerk.", + "bg.xrpl.err.badAddress": "Das sieht nicht nach einer XRP-Ledger-Adresse aus.", + "bg.xrpl.err.notToken": "Das ist kein Token auf dem XRP Ledger.", + "bg.xrpl.err.tagNeeded": "Diese Adresse braucht einen Destination-Tag. Frag den Empfänger danach oder nach einer X-Adresse, die ihn enthält.", + "bg.xrpl.err.notEnoughXrp": "Du hast nicht genug XRP. {reserve} XRP müssen auf deinem Konto bleiben, und die Gebühr beträgt {fee} XRP.", + "bg.xrpl.err.newAccount": "Dieses XRP-Ledger-Konto gibt es noch nicht. Sende mindestens {reserve} XRP, um es zu eröffnen.", + "bg.xrpl.err.feeOnly": "Du brauchst ein wenig XRP für die Netzwerkgebühr.", + "bg.xrpl.err.noTrustLine": "{symbol} ist noch nicht zu ihrem XRP-Ledger-Konto hinzugefügt. Bitte sie, das zuerst zu tun.", + "bg.xrpl.err.reserveForToken": "Einen Token hinzuzufügen sperrt {amount} XRP, plus die Gebühr. Füge zuerst etwas XRP hinzu.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "{action} auf {contract} aufrufen", + "bg.antelope.unknownToken": "{symbol} von {contract} ist kein Token, das Clip Wallet kennt. Prüfe den Vertrag, bevor du es sendest.", + "bg.antelope.noRecipient": "Auf diesem Netzwerk gibt es kein Konto namens {to}, deshalb schlägt das fehl.", + "bg.antelope.updateAuth": "Ändern, wer die Berechtigung {permission} von {account} steuert", + "bg.antelope.deleteAuth": "Die Berechtigung {permission} von {account} löschen", + "bg.antelope.linkAuth": "Der Berechtigung {permission} erlauben, {action} auszuführen", + "bg.antelope.unlinkAuth": "Einer Berechtigung verbieten, {action} auszuführen", + "bg.antelope.authWarning": "Das ändert, welche Schlüssel oder Konten {account} steuern. Gehören sie nicht dir, könnten sie alles darauf nehmen.", + "bg.antelope.unlinkWarning": "Das ändert, welcher Schlüssel manche Aktionen deines Kontos ausführen darf.", + "bg.antelope.label.alsoSigns": "Braucht auch", + "bg.antelope.delayed": "Das wird erst {seconds} Sekunden nach dem Senden ausgeführt und kann bis dahin storniert werden.", + "bg.antelope.feeFree": "Keine (das Netzwerk übernimmt CPU und NET für dich)", + "bg.antelope.feeResources": "Keine in Token: Es nutzt CPU und NET deines Kontos", + "bg.antelope.appSends": "{host} (bekommt die signierte Transaktion)", + "bg.antelope.err.offline": "{network} ist nicht erreichbar. Prüfe deine Verbindung und versuche es noch einmal.", + "bg.antelope.err.wrongNetwork": "Diese Anfrage ist für ein anderes Antelope-Netzwerk als das, auf dem du bist. Es wurde nichts signiert.", + "bg.antelope.err.contextFree": "Diese Transaktion nutzt Funktionen, die Clip Wallet nicht signiert (kontextfreie Aktionen oder Erweiterungen).", + "bg.antelope.err.noAbi": "Clip Wallet kann den Vertrag {contract} nicht lesen und diese Anfrage daher nicht erstellen.", + "bg.antelope.err.permission": "Dafür ist eine Berechtigung deines Kontos nötig, die dieser Schlüssel nicht steuert.", + "bg.antelope.err.otherSigners": "Diese Transaktion braucht auch Signaturen anderer Konten, deshalb kann Clip Wallet sie nicht allein senden.", + "bg.antelope.err.noAccountTestnet": "Noch kein {network}-Konto nutzt diesen Schlüssel. Konten auf Antelope-Netzwerken sind Namen, die auf der Chain erstellt werden: Erstelle mit dem Testnet-Tool des Netzwerks ein kostenloses Testkonto für deinen öffentlichen Schlüssel und komm dann zurück.", + "bg.antelope.err.noAccount": "Noch kein {network}-Konto nutzt diesen Schlüssel. Konten auf Antelope-Netzwerken sind Namen, die jemand auf der Chain für deinen öffentlichen Schlüssel erstellt: eine Börse, ein Freund mit Konto oder ein Dienst zur Kontoerstellung.", + "bg.antelope.err.notToken": "Das ist kein Token auf diesem Netzwerk.", + "bg.antelope.err.badAccount": "Das ist kein gültiger Kontoname. Namen haben bis zu 12 Zeichen: a–z, 1–5 und Punkte.", + "bg.antelope.err.cpuPowerup": "Deinem Konto ist auf {network} die CPU- oder NET-Zeit ausgegangen. Miete welche mit PowerUp (kostet ein wenig EOS für 24 Stunden) und versuche es dann noch einmal.", + "bg.antelope.err.cpuStake": "Deinem Konto ist auf {network} die CPU- oder NET-Zeit ausgegangen. Stake etwas TLOS für CPU und NET oder miete sie mit PowerUp und versuche es dann noch einmal.", + "bg.antelope.err.cpuFree": "{network} konnte CPU oder NET dafür gerade nicht übernehmen. Warte kurz und versuche es noch einmal.", + "bg.antelope.err.ram": "Dein Konto hat auf {network} nicht genug RAM dafür. Kaufe etwas RAM und versuche es dann noch einmal.", + "bg.antelope.err.balance": "Du hast nicht genug von diesem Token dafür.", + "bg.antelope.err.expired": "Diese Transaktion ist abgelaufen, bevor sie gesendet wurde. Versuche es noch einmal.", + "bg.antelope.err.auth": "Laut Netzwerk darf dieser Schlüssel nicht für dieses Konto signieren. Es wurde nichts gesendet.", + "bg.antelope.err.noRecipient": "Das Empfängerkonto gibt es auf diesem Netzwerk nicht. Es wurde nichts gesendet.", + "bg.antelope.err.duplicate": "Genau diese Transaktion wurde schon gesendet. Sieh in den Aktivitäten nach.", + "bg.antelope.err.rejected": "{network} hat diese Transaktion abgelehnt. Es wurde nichts gesendet.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "Deine Staking-Belohnungen von {validator} abholen", + "bg.multiversx.withdrawFrom": "Deine aus dem Staking genommenen EGLD von {validator} abheben", + "bg.multiversx.restakeRewardsWith": "Deine Belohnungen erneut bei {validator} staken", + "bg.multiversx.labelGuardian": "Guardian", + "bg.multiversx.setGuardianTitle": "{guardian} als Guardian deines Kontos festlegen", + "bg.multiversx.setGuardianWarn": "Sobald ein Guardian aktiv ist, braucht jede Transaktion von diesem Konto seine Mitsignatur. Wenn du {guardian} nicht selbst gewählt hast, könnte dich jemand aus deinem Konto aussperren.", + "bg.multiversx.guardAccountTitle": "Guardian deines Kontos einschalten", + "bg.multiversx.guardAccountWarn": "Ab jetzt braucht jede Transaktion von diesem Konto die Mitsignatur des Guardians. Clip Wallet kann sie nicht liefern, daher kannst du mit Clip Wallet nichts mehr von diesem Konto senden.", + "bg.multiversx.unguardAccountTitle": "Guardian deines Kontos ausschalten", + "bg.multiversx.changeOwnerTitle": "Vertrag {contract} an {owner} übergeben", + "bg.multiversx.changeOwnerWarn": "Damit wird {owner} Eigentümer des Vertrags {contract}. Der neue Eigentümer kann seinen Code ändern und nehmen, was darin liegt. Tu das nur, wenn du ihn wirklich abgeben willst.", + "bg.multiversx.guardedAccount": "Dieses Konto hat einen Guardian, und Clip Wallet kann dessen Mitsignatur nicht einholen. Es wurde nichts gesendet.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "Das geht an eine Konto-ID, die Art von Einzahlungsadresse, die Börsen vergeben. Prüfe, ob sie genau mit der Anzeige der Börse übereinstimmt.", + "bg.icp.expired": "Diese Überweisung ist abgelaufen, bevor du sie genehmigt hast. Es wurde nichts gesendet. Versuche es noch einmal.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "Nachbedingung", + "bg.stacks.pcYouSendExactly": "Du sendest genau {amount}", + "bg.stacks.pcYouSendAtMost": "Du sendest höchstens {amount}", + "bg.stacks.pcYouSendAtLeast": "Du sendest mindestens {amount}", + "bg.stacks.pcYouSendMoreThan": "Du sendest mehr als {amount}", + "bg.stacks.pcYouSendLessThan": "Du sendest weniger als {amount}", + "bg.stacks.pcSendsExactly": "{who} sendet genau {amount}", + "bg.stacks.pcSendsAtMost": "{who} sendet höchstens {amount}", + "bg.stacks.pcSendsAtLeast": "{who} sendet mindestens {amount}", + "bg.stacks.pcSendsMoreThan": "{who} sendet mehr als {amount}", + "bg.stacks.pcSendsLessThan": "{who} sendet weniger als {amount}", + "bg.stacks.pcYouSendNft": "Du sendest {item}", + "bg.stacks.pcYouKeepNft": "Du behältst {item}", + "bg.stacks.pcYouMaySendNft": "Du sendest möglicherweise {item}", + "bg.stacks.pcSendsNft": "{who} sendet {item}", + "bg.stacks.pcKeepsNft": "{who} behält {item}", + "bg.stacks.pcMaySendNft": "{who} sendet möglicherweise {item}", + "bg.stacks.pcStakingRule": "Eine Staking-Regel für {who}", + "bg.stacks.allowMode": "Damit kann der Vertrag jedes deiner Assets bewegen, nicht nur die aufgeführten. Genehmige das nur, wenn du {host} voll und ganz vertraust.", + "bg.stacks.originatorMode": "Nur die aufgeführten Übertragungen können dein Konto verlassen. Der Vertrag kann trotzdem Assets anderer Konten bewegen.", + "bg.stacks.nothingLeaves": "Bei dieser Transaktion kann keines deiner Assets dein Konto verlassen, abgesehen von der Netzwerkgebühr.", + "bg.stacks.highFee": "Die Netzwerkgebühr beträgt {fee}, das ist ungewöhnlich hoch.", + "bg.stacks.sponsorPays": "Ein von {host} gewählter Sponsor", + "bg.stacks.deployNamed": "Smart Contract {name} erstellen", + "bg.stacks.notYourTx": "Diese Transaktion wird von einem anderen Stacks-Konto signiert, daher kann Clip Wallet sie nicht signieren.", + "bg.stacks.multisig": "Clip Wallet kann noch nicht für gemeinsame Stacks-Konten (Multisignatur) signieren.", + "bg.stacks.memoTooLong": "Das Memo ist zu lang. Ein Stacks-Memo fasst höchstens 34 Byte.", + "bg.stacks.feeTooLow": "Die Netzwerkgebühr war zu niedrig, deshalb hat das Netzwerk die Transaktion nicht angenommen. Es wurde nichts gesendet. Versuch es noch einmal.", + "bg.stacks.nonceBusy": "Eine andere Transaktion dieses Kontos wartet noch. Warte, bis sie abgeschlossen ist, und versuch es dann noch einmal.", + "bg.stacks.otherNetworkTx": "Diese Transaktion ist für ein anderes Stacks-Netzwerk bestimmt, deshalb wurde sie nicht gesendet.", + "bg.stacks.badContractCall": "Der Vertragsaufruf der App passt nicht zum Vertrag im Netzwerk. Es wurde nichts gesendet.", + "bg.stacks.tooManyPending": "Bei diesem Konto warten zu viele Transaktionen. Warte, bis einige abgeschlossen sind, und versuch es dann noch einmal.", + "bg.stacks.signatureRefused": "Das Netzwerk hat die Signatur nicht akzeptiert. Es wurde nichts gesendet.", + "bg.stacks.testAddressOnMainnet": "Das ist eine Stacks-Adresse eines Testnetzwerks (ST…). Verwende eine Mainnet-Adresse (SP…).", + "bg.stacks.mainAddressOnTestnet": "Das ist eine Stacks-Mainnet-Adresse (SP…). Verwende eine Adresse eines Testnetzwerks (ST…).", + "bg.stacks.notAnAddress": "Das sieht nicht nach einer Stacks-Adresse aus.", + "bg.stacks.needStxForFee": "Du brauchst etwas STX, um die Netzwerkgebühr zu bezahlen.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "Alles, was danach von deinem {symbol} übrig ist, geht an {to}, nicht zurück an dich.", + "bg.fuel.leftoverLost": "{amount} wird von dieser Transaktion nirgendwohin gesendet und ginge verloren.", + "bg.fuel.coinsSpent": "Einige der Coins, die diese Transaktion nutzt, wurden schon ausgegeben. Bitte die App, es noch einmal zu versuchen.", + "bg.fuel.feeRose": "Die Netzwerkgebühr ist gestiegen, seit das vorbereitet wurde. Es wurde nichts gesendet. Versuch es noch einmal.", + "bg.fuel.failedOnChain": "Diese Transaktion ist im Fuel-Netzwerk fehlgeschlagen. Nur die Netzwerkgebühr wurde bezahlt.", + "bg.fuel.tooManyCoins": "Dafür werden zu viele kleine Coins auf einmal gebraucht. Sende zuerst einen kleineren Betrag.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "Diese App möchte, dass Clip Wallet seinen Schlüssel oder seine Signatur in die Daten eines Smart Contracts einfügt. Clip Wallet kann diese Art von Transaktion noch nicht signieren.", + "bg.bch.spendsTokens": "Damit werden CashTokens aus deiner Wallet gesendet: {tokens}.", + "bg.bch.highFee": "Die Netzwerkgebühr beträgt {fee}, das ist ungewöhnlich hoch.", + "bg.bch.othersInputs": "{count} Coins von anderen Eigentümern oder Verträgen", + "bg.bch.notCashAddr": "Das sieht nicht nach einer Adresse für Bitcoin Cash aus. Verwende ihre CashAddr-Form (bitcoincash:q…).", + "bg.bch.testAddressOnMainnet": "Das ist eine Adresse eines Testnetzwerks für Bitcoin Cash (bchtest:…). Verwende eine Mainnet-Adresse (bitcoincash:…).", + "bg.bch.mainAddressOnTestnet": "Das ist eine Mainnet-Adresse für Bitcoin Cash (bitcoincash:…). Verwende eine Adresse eines Testnetzwerks (bchtest:…).", + "bg.bch.tokenSendUnavailable": "Das Senden von CashTokens ist noch nicht verfügbar.", + "bg.bch.belowDust": "Sende mindestens {amount}. Kleinere Beträge können auf Bitcoin Cash nicht gesendet werden.", + "bg.bch.feeTooLow": "Die Gebühr war für das Netzwerk zu niedrig. Es wurde nichts gesendet. Versuch es noch einmal.", + "bg.bch.coinsSpent": "Einige Coins in dieser Transaktion wurden bereits ausgegeben. Es wurde nichts gesendet. Aktualisiere und versuch es noch einmal.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "Netzwerkabgabe", + "bg.evm.flatFee": "{amount} pro Transaktion, auch wenn sie fehlschlägt", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "{symbol} kann nicht von einem Chainflip-Konto auf ein anderes gesendet werden. Um es zu bewegen, löse es an eine Ethereum-Adresse ein oder lade das andere Konto von Ethereum aus auf.", + "bg.chainflip.redeem": "{amount} an {to} auf Ethereum einlösen", + "bg.chainflip.redeemAll": "Dein gesamtes {symbol} an {to} auf Ethereum einlösen", + "bg.chainflip.allRedeemable": "Dein gesamtes {symbol}, das nicht gebunden ist", + "bg.chainflip.label.executor": "Wer sie abschließen kann", + "bg.chainflip.anyone": "Jeder", + "bg.chainflip.redeemSteps": "Nach einer Wartezeit wird die Einlösung auf Ethereum abgeschlossen (das kostet ETH-Gas). Schließt sie niemand vor Ablauf ab, muss sie neu gestartet werden.", + "bg.chainflip.redeemWarn": "Dein {symbol} verlässt Chainflip in Richtung {to} auf Ethereum. Prüfe, dass du diese Ethereum-Adresse kontrollierst.", + "bg.chainflip.bindRedeem": "Einlösungen für immer nur an {to} erlauben", + "bg.chainflip.bindRedeemWarn": "Das lässt sich nie rückgängig machen. Dieses Konto wird {symbol} nur noch an {to} einlösen. Wenn du diese Ethereum-Adresse nicht kontrollierst, ist dein {symbol} für immer gesperrt.", + "bg.chainflip.bindExecutor": "Für immer nur {to} deine Einlösungen abschließen lassen", + "bg.chainflip.bindExecutorWarn": "Das lässt sich nie rückgängig machen. Nur {to} kann die {symbol}-Einlösungen dieses Kontos auf Ethereum abschließen. Geht dieser Schlüssel verloren, kann eingelöstes {symbol} nicht mehr abgeholt werden.", + "bg.chainflip.moveTo": "{amount} auf das Chainflip-Konto {to} verschieben", + "bg.chainflip.moveAllTo": "Dein gesamtes {symbol} auf das Chainflip-Konto {to} verschieben", + "bg.chainflip.otherAccount": "{to} ist keines deiner Konten. Prüfe es, bevor du etwas dorthin verschiebst.", + "bg.chainflip.foreignRecipient": "Das geht an {to} auf {chain}. Prüfe die Adresse: Clip Wallet kann das nicht rückgängig machen.", + "bg.chainflip.registerLp": "Als Chainflip-Liquiditätsanbieter registrieren", + "bg.chainflip.deregisterLp": "Dein Chainflip-Liquiditätsanbieter-Konto schließen", + "bg.chainflip.refundAddress": "Deine Rückerstattungsadresse auf {chain} auf {to} setzen", + "bg.chainflip.depositAddress": "Eine Einzahlungsadresse für {asset} anfordern", + "bg.chainflip.label.boostFee": "Boost-Gebühr", + "bg.chainflip.label.refundTo": "Rückerstattungen gehen an", + "bg.chainflip.withdraw": "{amount} an {to} auf {chain} abheben", + "bg.chainflip.swap": "{amount} gegen {asset} tauschen", + "bg.chainflip.swapChannel": "Einen Kanal öffnen, um {from} gegen {to} zu tauschen", + "bg.chainflip.addBoost": "Boost: {amount} in den {tier}-bps-Pool einzahlen", + "bg.chainflip.stopBoost": "Boosten von {asset} im {tier}-bps-Pool beenden", + "bg.chainflip.lend": "{amount} verleihen", + "bg.chainflip.unlend": "Verliehene {amount} zurückholen", + "bg.chainflip.unlendAll": "Alle verliehenen {asset} zurückholen", + "bg.chainflip.borrow": "{amount} leihen", + "bg.chainflip.repay": "Kredit #{id} teilweise zurückzahlen", + "bg.chainflip.repayAll": "Kredit #{id} vollständig zurückzahlen", + "bg.chainflip.onSubAccount": "Auf Unterkonto #{n}: {inner}", + "bg.chainflip.orderFee": "Eine Order zu platzieren oder zu ändern erfordert mindestens 1 {symbol} auf diesem Konto. Das meiste davon kommt zurück, aber wer Orders im selben Pool mehrmals in einem Block ändert, zahlt jedes Mal mehr Gebühr.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts new file mode 100644 index 00000000..29937986 --- /dev/null +++ b/packages/core/src/messages/locales/n87/es.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Spanish: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../es.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "Canal IBC {channel}", + "bg.cosmos.ibcLeaves": "Esto envía los tokens a otra cadena por IBC ({channel}). Comprueba que la dirección sea correcta en esa cadena.", + "bg.cosmos.unbonding": "Tus {symbol} quedan bloqueados y no generan nada hasta que termine el período de desvinculación de la red.", + "bg.cosmos.moveStake": "Mover {amount} de staking de {from} a {to}", + "bg.cosmos.claimRewardsFrom": "Reclamar recompensas de staking de {validator}", + "bg.cosmos.grantAuthz": "Permitir que {grantee} actúe en nombre de tu cuenta", + "bg.cosmos.authzTakeAll": "Esto permite que {grantee} mueva tus fondos sin volver a preguntar, hasta que retires el permiso. Podría llevárselo todo.", + "bg.cosmos.authzActs": "Esto permite que {grantee} actúe en nombre de tu cuenta sin volver a preguntar, hasta que retires el permiso. Permítelo solo a apps en las que confíes.", + "bg.cosmos.revokeAuthz": "Impedir que {grantee} actúe en nombre de tu cuenta", + "bg.cosmos.grantFees": "Permitir que {grantee} pague comisiones de red con tus {symbol}", + "bg.cosmos.feegrantUpTo": "Esto permite que {grantee} gaste hasta {amount} de tus fondos en comisiones de red, sin volver a preguntar.", + "bg.cosmos.feegrantAll": "Esto permite que {grantee} gaste tus {symbol} en comisiones de red sin límite, sin volver a preguntar.", + "bg.cosmos.revokeFees": "Impedir que {grantee} pague comisiones de red con tus fondos", + "bg.cosmos.block": "bloque {height}", + "bg.cosmos.appGetsSigned": "{host} (recibe la transacción firmada)", + "bg.cosmos.wrongPrefix": "Esa dirección es de otra red. Las direcciones en {network} empiezan por {prefix}.", + "bg.cosmos.noAccount": "Tu cuenta aún no está en {network}. Recibe algo de {symbol} primero.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "Otra clave controla esta cuenta de TRON, así que Clip Wallet no puede firmar por ella.", + "bg.tron.notOpen": "Tu cuenta de TRON aún no está abierta. Primero recibe algo de TRX.", + "bg.tron.contractRecipient": "No se puede enviar TRX directamente a un contrato inteligente.", + "bg.tron.newAccountFee": "Esta dirección aún no está abierta en TRON. Enviarle fondos cuesta además {amount} para abrirla.", + "bg.tron.getsSigned": "{host} (recibe la transacción firmada)", + "bg.tron.energy": "energía", + "bg.tron.bandwidth": "ancho de banda", + "bg.tron.tronPower": "poder de voto", + "bg.tron.stakeFor": "Hacer staking de {amount} para obtener {resource}", + "bg.tron.days": "{count} días", + "bg.tron.cancelUnstaking": "Cancelar tus retiros pendientes del staking y volver a hacer staking de esos TRX", + "bg.tron.lend": "Prestar {resource} de los {amount} que tienes en staking a {to}", + "bg.tron.stopLending": "Recuperar {resource} de los {amount} que prestaste a {to}", + "bg.tron.lockedHours": "Unas {hours} horas. No puedes recuperarlo antes.", + "bg.tron.vote": "Votar por {count} Super Representatives", + "bg.tron.votesReplace": "Esto reemplaza todos tus votos anteriores.", + "bg.tron.claimVoteRewards": "Reclamar tus recompensas por votar", + "bg.tron.changePermissions": "Cambiar quién controla tu cuenta de TRON", + "bg.tron.keysThreshold": "{keys} (necesita {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "Pagar hasta {amount} para que {to} reciba {get}", + "bg.xrpl.atMost": "Como máximo {amount}", + "bg.xrpl.anyAmount": "Cualquier cantidad", + "bg.xrpl.partialPayment": "Es un pago parcial: el destinatario puede recibir mucho menos de {amount}. Así se ha engañado a tiendas y exchanges.", + "bg.xrpl.xrpToSelf": "Enviar XRP a tu propia cuenta no hace nada, así que el XRP Ledger lo rechazará.", + "bg.xrpl.frees": "Esto libera la reserva de {amount} que bloqueaba.", + "bg.xrpl.locksReserve": "{amount} de tu XRP queda bloqueado mientras este token esté añadido", + "bg.xrpl.freezesLine": "Esto también congela el token para la otra parte.", + "bg.xrpl.offer.fillOrKill": "Ejecutar todo ahora o cancelar", + "bg.xrpl.offer.immediate": "Ejecutar lo que se pueda ahora y cancelar el resto", + "bg.xrpl.offer.standing": "Sigue abierta hasta que se ejecute, venza o la canceles", + "bg.xrpl.cancelOffer": "Cancelar una oferta", + "bg.xrpl.changeSettings": "Cambiar los ajustes de tu cuenta", + "bg.xrpl.asf.requireDest": "Exigir una etiqueta de destino en los pagos que recibes", + "bg.xrpl.asf.requireAuth": "Aprobar a cada titular de tus tokens", + "bg.xrpl.asf.disallowXrp": "Pedir que no te envíen XRP", + "bg.xrpl.asf.disableMaster": "Desactivar la clave propia de tu cuenta", + "bg.xrpl.asf.accountTxnId": "Registrar el id de tu última transacción", + "bg.xrpl.asf.noFreeze": "Renunciar para siempre a congelar tus tokens", + "bg.xrpl.asf.globalFreeze": "Congelar todos los tokens que emitiste", + "bg.xrpl.asf.defaultRipple": "Dejar que tus tokens pasen entre titulares (rippling)", + "bg.xrpl.asf.depositAuth": "Aceptar solo pagos de cuentas que apruebes", + "bg.xrpl.asf.nftMinter": "Permitir que otra cuenta cree NFTs por ti", + "bg.xrpl.asf.noNftOffers": "Bloquear ofertas de NFT entrantes", + "bg.xrpl.asf.noChecks": "Bloquear cheques entrantes", + "bg.xrpl.asf.noPayChannels": "Bloquear canales de pago entrantes", + "bg.xrpl.asf.noTrustLines": "Bloquear trust lines entrantes", + "bg.xrpl.asf.clawback": "Permitirte recuperar los tokens que emites (clawback)", + "bg.xrpl.asf.lockTokens": "Permitir que los titulares bloqueen tus tokens en escrow", + "bg.xrpl.disableMasterWarning": "Esto desactiva la clave propia de tu cuenta. Después solo otra clave o una lista de firmantes podrá usarla; si no hay ninguna, queda bloqueada para siempre.", + "bg.xrpl.issuerSettingWarning": "Esto cambia cómo se comportan los tokens que emites. Algunos cambios no se pueden deshacer.", + "bg.xrpl.nftMinterWarning": "Esto permite que {who} cree NFTs en nombre de tu cuenta.", + "bg.xrpl.cleared": "(borrado)", + "bg.xrpl.removeRegularKey": "Quitar la clave adicional de tu cuenta", + "bg.xrpl.regularKeyWarning": "{who} recibe una clave que puede hacer cualquier cosa con tu cuenta, incluso vaciarla.", + "bg.xrpl.removeSigners": "Quitar las cuentas que pueden firmar por la tuya", + "bg.xrpl.setSigners": "Permitir que {count} cuentas más firmen por la tuya", + "bg.xrpl.signersWarning": "Juntas, estas cuentas pueden hacer cualquier cosa con tu cuenta, incluso vaciarla.", + "bg.xrpl.closeAccount": "Cerrar tu cuenta del XRP Ledger y enviar todo a {to}", + "bg.xrpl.closeWarning": "Esto elimina tu cuenta del XRP Ledger y envía todo tu XRP a {to}. La comisión de {fee} se quema.", + "bg.xrpl.mintNft": "Crear un NFT", + "bg.xrpl.taxon": "Taxón {taxon}", + "bg.xrpl.nft.transferable": "Se puede vender o regalar", + "bg.xrpl.nft.notTransferable": "No se puede transferir", + "bg.xrpl.nft.issuerCanBurn": "Su creador puede quemarlo", + "bg.xrpl.nft.mutable": "Su enlace se puede cambiar más adelante", + "bg.xrpl.burnNft": "Quemar un NFT", + "bg.xrpl.giveNft": "Ofrecer tu NFT a {to} gratis", + "bg.xrpl.freeForAnyone": "Cualquiera puede llevarse este NFT sin pagar nada.", + "bg.xrpl.sellNft": "Ofrecer tu NFT por {amount}", + "bg.xrpl.buyNft": "Ofrecer {amount} por un NFT", + "bg.xrpl.offerHolds": "No se cobra nada hasta que el dueño acepte", + "bg.xrpl.acceptNftOffer": "Aceptar una oferta de NFT", + "bg.xrpl.brokered": "Une una oferta de compra con una de venta (venta intermediada)", + "bg.xrpl.offerUnknown": "Clip Wallet no pudo consultar esta oferta, así que no puede mostrar lo que pagas o recibes.", + "bg.xrpl.acceptNftSell": "Comprar un NFT por {amount}", + "bg.xrpl.acceptNftBuy": "Vender tu NFT por {amount}", + "bg.xrpl.cancelNftOffers": "Cancelar {count} ofertas de NFT", + "bg.xrpl.escrowCreate": "Bloquear {amount} en escrow para {to}", + "bg.xrpl.escrowCondition": "Liberarlo requiere un secreto (crypto-condition)", + "bg.xrpl.escrowLocked": "Estos fondos quedan bloqueados hasta que el escrow se libere o se cancele.", + "bg.xrpl.escrowFinish": "Liberar un escrow", + "bg.xrpl.escrowCancel": "Cancelar un escrow", + "bg.xrpl.ammDeposit": "Añadir fondos a un pool de liquidez", + "bg.xrpl.ammWithdraw": "Retirar fondos de un pool de liquidez", + "bg.xrpl.yourShare": "Tu parte del pool", + "bg.xrpl.poolAmounts": "El pool decide las cantidades exactas al ejecutarse.", + "bg.xrpl.unknownTx": "Aprobar una transacción {type} del XRP Ledger", + "bg.xrpl.recipientUnchecked": "Clip Wallet no pudo comprobar la cuenta del destinatario.", + "bg.xrpl.newAccountTooSmall": "Esta cuenta del XRP Ledger aún no existe. Abrirla requiere al menos {reserve}, así que esto fallará.", + "bg.xrpl.opensAccount": "Esto abre una cuenta nueva del XRP Ledger; {reserve} quedan bloqueados en ella como reserva.", + "bg.xrpl.noRecipientAccount": "Esta cuenta del XRP Ledger no existe, así que no puede recibir tokens.", + "bg.xrpl.destinationTagNeeded": "Este destinatario necesita una etiqueta de destino y no hay ninguna. Sin ella, los fondos pueden perderse.", + "bg.xrpl.disallowsXrp": "Esta cuenta indica que no quiere recibir XRP.", + "bg.xrpl.depositAuth": "Esta cuenta solo acepta pagos de cuentas que autorizó, así que esto puede fallar.", + "bg.xrpl.noTrustLine": "El destinatario no ha añadido {symbol} a su cuenta, así que este pago fallará.", + "bg.xrpl.notEnoughXrp": "No tienes suficiente XRP para esto: {reserve} debe quedar en tu cuenta como reserva.", + "bg.xrpl.highFee": "La comisión de red es inusualmente alta: {fee}.", + "bg.xrpl.appSends": "{host} (recibe la transacción firmada)", + "bg.xrpl.feeCharged": "La comisión de red se cobró igualmente.", + "bg.xrpl.label.destinationTag": "Etiqueta de destino", + "bg.xrpl.label.sourceTag": "Etiqueta de origen", + "bg.xrpl.label.deliversAtLeast": "Entrega al menos", + "bg.xrpl.label.issuer": "Emisor", + "bg.xrpl.label.replacesOffer": "Reemplaza la oferta", + "bg.xrpl.label.turnsOn": "Activa", + "bg.xrpl.label.turnsOff": "Desactiva", + "bg.xrpl.label.domain": "Dominio", + "bg.xrpl.label.transferFee": "Comisión de transferencia", + "bg.xrpl.label.nftMinter": "Creador de NFT", + "bg.xrpl.label.quorum": "Firmas necesarias", + "bg.xrpl.label.link": "Enlace", + "bg.xrpl.label.royalty": "Regalía", + "bg.xrpl.label.releaseAfter": "Se puede liberar después de", + "bg.xrpl.label.cancelAfter": "Se puede cancelar después de", + "bg.xrpl.err.wrongNetwork": "Esta app pide una red del XRP Ledger distinta de la que tiene conectada. No se firmó nada.", + "bg.xrpl.err.multisig": "La firma múltiple aún no está disponible en Clip Wallet.", + "bg.xrpl.err.alreadySigned": "Esta transacción ya está firmada, así que Clip Wallet no la volverá a firmar.", + "bg.xrpl.err.cantRead": "Clip Wallet no puede leer una parte de esta transacción del XRP Ledger ({field}), así que no puede firmarla.", + "bg.xrpl.err.notActivated": "Tu cuenta del XRP Ledger aún no está activada. Recibe al menos {reserve} XRP para activarla.", + "bg.xrpl.err.otherKey": "La clave propia de esta cuenta está desactivada y la controla otra clave, así que Clip Wallet no puede firmar por ella.", + "bg.xrpl.err.xAddressNetwork": "Esta dirección X es de otra red del XRP Ledger.", + "bg.xrpl.err.badAddress": "Eso no parece una dirección del XRP Ledger.", + "bg.xrpl.err.notToken": "Eso no es un token del XRP Ledger.", + "bg.xrpl.err.tagNeeded": "Esta dirección necesita una etiqueta de destino. Pídesela al destinatario, o pídele una dirección X que la incluya.", + "bg.xrpl.err.notEnoughXrp": "No tienes suficiente XRP. {reserve} XRP deben quedar en tu cuenta y la comisión es de {fee} XRP.", + "bg.xrpl.err.newAccount": "Esta cuenta del XRP Ledger aún no existe. Envía al menos {reserve} XRP para abrirla.", + "bg.xrpl.err.feeOnly": "Necesitas un poco de XRP para pagar la comisión de red.", + "bg.xrpl.err.noTrustLine": "Aún no han añadido {symbol} a su cuenta del XRP Ledger. Pídeles que lo añadan primero.", + "bg.xrpl.err.reserveForToken": "Añadir un token bloquea {amount} XRP, más la comisión. Añade algo de XRP primero.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "Llamar a {action} en {contract}", + "bg.antelope.unknownToken": "{symbol} de {contract} no es un token que Clip Wallet conozca. Revisa el contrato antes de enviarlo.", + "bg.antelope.noRecipient": "No hay ninguna cuenta llamada {to} en esta red, así que esto fallará.", + "bg.antelope.updateAuth": "Cambiar quién controla el permiso {permission} de {account}", + "bg.antelope.deleteAuth": "Eliminar el permiso {permission} de {account}", + "bg.antelope.linkAuth": "Permitir que el permiso {permission} ejecute {action}", + "bg.antelope.unlinkAuth": "Impedir que un permiso ejecute {action}", + "bg.antelope.authWarning": "Esto cambia qué claves o cuentas controlan {account}. Si no son tuyas, podrían llevarse todo lo que contiene.", + "bg.antelope.unlinkWarning": "Esto cambia qué clave puede ejecutar algunas acciones de tu cuenta.", + "bg.antelope.label.alsoSigns": "También necesita", + "bg.antelope.delayed": "Esto solo se ejecuta {seconds} segundos después de enviarse y se puede cancelar hasta entonces.", + "bg.antelope.feeFree": "Ninguna (esta red cubre la CPU y la NET por ti)", + "bg.antelope.feeResources": "Ninguna en tokens: usa la CPU y la NET de tu cuenta", + "bg.antelope.appSends": "{host} (recibe la transacción firmada)", + "bg.antelope.err.offline": "No se pudo conectar con {network}. Revisa tu conexión e inténtalo de nuevo.", + "bg.antelope.err.wrongNetwork": "Esta solicitud es para una red Antelope distinta de la que usas. No se firmó nada.", + "bg.antelope.err.contextFree": "Esta transacción usa funciones que Clip Wallet no firma (acciones sin contexto o extensiones).", + "bg.antelope.err.noAbi": "Clip Wallet no puede leer el contrato {contract}, así que no puede preparar esta solicitud.", + "bg.antelope.err.permission": "Esto necesita un permiso de tu cuenta que esta clave no controla.", + "bg.antelope.err.otherSigners": "Esta transacción también necesita la firma de otras cuentas, así que Clip Wallet no puede enviarla sola.", + "bg.antelope.err.noAccountTestnet": "Ninguna cuenta de {network} usa esta clave todavía. En las redes Antelope, las cuentas son nombres creados en la cadena: crea una cuenta de prueba gratis para tu clave pública con la herramienta de testnet de la red y vuelve.", + "bg.antelope.err.noAccount": "Ninguna cuenta de {network} usa esta clave todavía. En las redes Antelope, las cuentas son nombres que alguien crea en la cadena para tu clave pública: un exchange, un amigo con cuenta o un servicio de creación de cuentas puede hacerlo.", + "bg.antelope.err.notToken": "Eso no es un token de esta red.", + "bg.antelope.err.badAccount": "Ese no es un nombre de cuenta válido. Los nombres tienen hasta 12 caracteres: a–z, 1–5 y puntos.", + "bg.antelope.err.cpuPowerup": "Tu cuenta se quedó sin tiempo de CPU o NET en {network}. Alquila un poco con PowerUp (cuesta algo de EOS por 24 horas) e inténtalo de nuevo.", + "bg.antelope.err.cpuStake": "Tu cuenta se quedó sin tiempo de CPU o NET en {network}. Pon algo de TLOS en staking para CPU y NET, o alquílalo con PowerUp, e inténtalo de nuevo.", + "bg.antelope.err.cpuFree": "{network} no pudo cubrir la CPU o la NET de esto ahora. Espera un momento e inténtalo de nuevo.", + "bg.antelope.err.ram": "Tu cuenta no tiene suficiente RAM en {network} para esto. Compra un poco de RAM e inténtalo de nuevo.", + "bg.antelope.err.balance": "No tienes suficiente de este token para eso.", + "bg.antelope.err.expired": "Esta transacción caducó antes de enviarse. Inténtalo de nuevo.", + "bg.antelope.err.auth": "La red dice que esta clave no puede firmar por esa cuenta. No se envió nada.", + "bg.antelope.err.noRecipient": "La cuenta del destinatario no existe en esta red. No se envió nada.", + "bg.antelope.err.duplicate": "Esta misma transacción ya se envió. Revisa la Actividad.", + "bg.antelope.err.rejected": "{network} rechazó esta transacción. No se envió nada.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "Cobrar tus recompensas de staking de {validator}", + "bg.multiversx.withdrawFrom": "Retirar de {validator} tus EGLD que ya salieron del staking", + "bg.multiversx.restakeRewardsWith": "Volver a hacer staking de tus recompensas con {validator}", + "bg.multiversx.labelGuardian": "Guardián", + "bg.multiversx.setGuardianTitle": "Hacer que {guardian} sea el guardián de tu cuenta", + "bg.multiversx.setGuardianWarn": "Cuando hay un guardián activo, cada transacción de esta cuenta necesita su firma conjunta. Si no elegiste a {guardian}, alguien podría dejarte sin acceso a tu cuenta.", + "bg.multiversx.guardAccountTitle": "Activar el guardián de tu cuenta", + "bg.multiversx.guardAccountWarn": "A partir de ahora, cada transacción de esta cuenta necesita la firma conjunta del guardián. Clip Wallet no puede darla, así que ya no podrás enviar desde Clip Wallet.", + "bg.multiversx.unguardAccountTitle": "Desactivar el guardián de tu cuenta", + "bg.multiversx.changeOwnerTitle": "Entregar el contrato {contract} a {owner}", + "bg.multiversx.changeOwnerWarn": "Esto convierte a {owner} en propietario del contrato {contract}. El nuevo propietario puede cambiar su código y llevarse lo que contiene. Hazlo solo si de verdad quieres cederlo.", + "bg.multiversx.guardedAccount": "Esta cuenta tiene un guardián y Clip Wallet no puede conseguir su firma conjunta. No se envió nada.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "Esto se envía a un ID de cuenta, el tipo de dirección de depósito que dan los exchanges. Comprueba que coincida exactamente con lo que muestra el exchange.", + "bg.icp.expired": "Esta transferencia caducó antes de que la aprobaras. No se envió nada. Inténtalo de nuevo.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "Poscondición", + "bg.stacks.pcYouSendExactly": "Envías exactamente {amount}", + "bg.stacks.pcYouSendAtMost": "Envías como máximo {amount}", + "bg.stacks.pcYouSendAtLeast": "Envías como mínimo {amount}", + "bg.stacks.pcYouSendMoreThan": "Envías más de {amount}", + "bg.stacks.pcYouSendLessThan": "Envías menos de {amount}", + "bg.stacks.pcSendsExactly": "{who} envía exactamente {amount}", + "bg.stacks.pcSendsAtMost": "{who} envía como máximo {amount}", + "bg.stacks.pcSendsAtLeast": "{who} envía como mínimo {amount}", + "bg.stacks.pcSendsMoreThan": "{who} envía más de {amount}", + "bg.stacks.pcSendsLessThan": "{who} envía menos de {amount}", + "bg.stacks.pcYouSendNft": "Envías {item}", + "bg.stacks.pcYouKeepNft": "Conservas {item}", + "bg.stacks.pcYouMaySendNft": "Puede que envíes {item}", + "bg.stacks.pcSendsNft": "{who} envía {item}", + "bg.stacks.pcKeepsNft": "{who} conserva {item}", + "bg.stacks.pcMaySendNft": "Puede que {who} envíe {item}", + "bg.stacks.pcStakingRule": "Una regla de staking para {who}", + "bg.stacks.allowMode": "Esto permite que el contrato mueva cualquiera de tus activos, no solo los indicados. Apruébalo solo si confías plenamente en {host}.", + "bg.stacks.originatorMode": "Solo las transferencias indicadas pueden salir de tu cuenta. El contrato aún puede mover activos de otras cuentas.", + "bg.stacks.nothingLeaves": "Ninguno de tus activos puede salir de tu cuenta en esta transacción, aparte de la comisión de red.", + "bg.stacks.highFee": "La comisión de red es {fee}, un importe inusualmente alto.", + "bg.stacks.sponsorPays": "Un patrocinador elegido por {host}", + "bg.stacks.deployNamed": "Crear el contrato inteligente {name}", + "bg.stacks.notYourTx": "Esta transacción la firma otra cuenta de Stacks, así que Clip Wallet no puede firmarla.", + "bg.stacks.multisig": "Clip Wallet aún no puede firmar para cuentas compartidas (multifirma) de Stacks.", + "bg.stacks.memoTooLong": "El memo es demasiado largo. Un memo de Stacks admite hasta 34 bytes.", + "bg.stacks.feeTooLow": "La comisión de red era demasiado baja, así que la red no aceptó la transacción. No se envió nada. Inténtalo de nuevo.", + "bg.stacks.nonceBusy": "Otra transacción de esta cuenta sigue pendiente. Espera a que termine e inténtalo de nuevo.", + "bg.stacks.otherNetworkTx": "Esta transacción es para otra red de Stacks, así que no se envió.", + "bg.stacks.badContractCall": "La llamada al contrato de la app no coincide con el contrato en la red. No se envió nada.", + "bg.stacks.tooManyPending": "Esta cuenta tiene demasiadas transacciones pendientes. Espera a que terminen algunas e inténtalo de nuevo.", + "bg.stacks.signatureRefused": "La red no aceptó la firma. No se envió nada.", + "bg.stacks.testAddressOnMainnet": "Esa es una dirección de Stacks de una red de prueba (ST…). Usa una dirección de la red principal (SP…).", + "bg.stacks.mainAddressOnTestnet": "Esa es una dirección de Stacks de la red principal (SP…). Usa una dirección de una red de prueba (ST…).", + "bg.stacks.notAnAddress": "Eso no parece una dirección de Stacks.", + "bg.stacks.needStxForFee": "Necesitas un poco de STX para pagar la comisión de red.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "Todo lo que quede de tu {symbol} después de esto va a {to}, no vuelve a ti.", + "bg.fuel.leftoverLost": "{amount} no se envía a ningún sitio en esta transacción y se perdería.", + "bg.fuel.coinsSpent": "Algunas de las monedas que usa esta transacción ya se gastaron. Pide a la app que lo intente de nuevo.", + "bg.fuel.feeRose": "La comisión de red subió desde que se preparó esto. No se envió nada. Inténtalo de nuevo.", + "bg.fuel.failedOnChain": "Esta transacción falló en la red Fuel. Solo se gastó la comisión de red.", + "bg.fuel.tooManyCoins": "Esto necesita demasiadas monedas pequeñas a la vez. Envía primero una cantidad menor.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "Esta app le pide a Clip Wallet que ponga su clave o su firma en los datos de un contrato inteligente. Clip Wallet aún no puede firmar ese tipo de transacción.", + "bg.bch.spendsTokens": "Esto envía CashTokens desde tu billetera: {tokens}.", + "bg.bch.highFee": "La comisión de red es {fee}, un importe inusualmente alto.", + "bg.bch.othersInputs": "{count} monedas de otros propietarios o contratos", + "bg.bch.notCashAddr": "Eso no parece una dirección de Bitcoin Cash. Usa su formato CashAddr (bitcoincash:q…).", + "bg.bch.testAddressOnMainnet": "Esa es una dirección de Bitcoin Cash de una red de prueba (bchtest:…). Usa una dirección de la red principal (bitcoincash:…).", + "bg.bch.mainAddressOnTestnet": "Esa es una dirección de Bitcoin Cash de la red principal (bitcoincash:…). Usa una dirección de una red de prueba (bchtest:…).", + "bg.bch.tokenSendUnavailable": "Todavía no se pueden enviar CashTokens.", + "bg.bch.belowDust": "Envía al menos {amount}. En Bitcoin Cash no se pueden enviar cantidades menores.", + "bg.bch.feeTooLow": "La comisión era demasiado baja para la red. No se envió nada. Inténtalo de nuevo.", + "bg.bch.coinsSpent": "Algunas monedas de esta transacción ya se habían gastado. No se envió nada. Actualiza e inténtalo de nuevo.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "Cargo de la red", + "bg.evm.flatFee": "{amount} por transacción, aunque falle", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "No se puede enviar {symbol} de una cuenta de Chainflip a otra. Para moverlo, canjéalo a una dirección de Ethereum o financia la otra cuenta desde Ethereum.", + "bg.chainflip.redeem": "Canjear {amount} a {to} en Ethereum", + "bg.chainflip.redeemAll": "Canjear todo tu {symbol} a {to} en Ethereum", + "bg.chainflip.allRedeemable": "Todo tu {symbol} que no está en garantía", + "bg.chainflip.label.executor": "Quién puede completarlo", + "bg.chainflip.anyone": "Cualquiera", + "bg.chainflip.redeemSteps": "Tras un periodo de espera, el canje se completa en Ethereum (eso cuesta gas en ETH). Si nadie lo completa antes de que caduque, hay que repetirlo.", + "bg.chainflip.redeemWarn": "Tu {symbol} sale de Chainflip hacia {to} en Ethereum. Comprueba que controlas esta dirección de Ethereum.", + "bg.chainflip.bindRedeem": "Permitir canjes solo a {to}, para siempre", + "bg.chainflip.bindRedeemWarn": "Esto no se puede deshacer nunca. Esta cuenta solo podrá canjear {symbol} a {to}. Si no controlas esa dirección de Ethereum, tu {symbol} quedará bloqueado para siempre.", + "bg.chainflip.bindExecutor": "Que solo {to} pueda completar tus canjes, para siempre", + "bg.chainflip.bindExecutorWarn": "Esto no se puede deshacer nunca. Solo {to} podrá completar los canjes de {symbol} de esta cuenta en Ethereum. Si se pierde esa clave, el {symbol} canjeado no se podrá reclamar.", + "bg.chainflip.moveTo": "Mover {amount} a la cuenta de Chainflip {to}", + "bg.chainflip.moveAllTo": "Mover todo tu {symbol} a la cuenta de Chainflip {to}", + "bg.chainflip.otherAccount": "{to} no es una de tus cuentas. Compruébala antes de mover nada allí.", + "bg.chainflip.foreignRecipient": "Esto va a {to} en {chain}. Comprueba la dirección: Clip Wallet no puede deshacerlo.", + "bg.chainflip.registerLp": "Registrarte como proveedor de liquidez de Chainflip", + "bg.chainflip.deregisterLp": "Cerrar tu cuenta de proveedor de liquidez de Chainflip", + "bg.chainflip.refundAddress": "Establecer tu dirección de reembolso de {chain} en {to}", + "bg.chainflip.depositAddress": "Obtener una dirección de depósito para {asset}", + "bg.chainflip.label.boostFee": "Comisión de Boost", + "bg.chainflip.label.refundTo": "Los reembolsos van a", + "bg.chainflip.withdraw": "Retirar {amount} a {to} en {chain}", + "bg.chainflip.swap": "Intercambiar {amount} por {asset}", + "bg.chainflip.swapChannel": "Abrir un canal para intercambiar {from} por {to}", + "bg.chainflip.addBoost": "Boost: añadir {amount} al pool de {tier} bps", + "bg.chainflip.stopBoost": "Dejar de hacer boost de {asset} en el pool de {tier} bps", + "bg.chainflip.lend": "Prestar {amount}", + "bg.chainflip.unlend": "Recuperar {amount} que prestaste", + "bg.chainflip.unlendAll": "Recuperar todo el {asset} que prestaste", + "bg.chainflip.borrow": "Pedir prestado {amount}", + "bg.chainflip.repay": "Devolver parte del préstamo #{id}", + "bg.chainflip.repayAll": "Devolver por completo el préstamo #{id}", + "bg.chainflip.onSubAccount": "En la subcuenta #{n}: {inner}", + "bg.chainflip.orderFee": "Colocar o cambiar una orden requiere al menos 1 {symbol} en esta cuenta. La mayor parte se devuelve, pero cambiar órdenes del mismo pool muchas veces en un bloque tiene una comisión más alta cada vez.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts new file mode 100644 index 00000000..9cbbb4b5 --- /dev/null +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** French: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../fr.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "Canal IBC {channel}", + "bg.cosmos.ibcLeaves": "Ceci envoie les jetons vers une autre chaîne via IBC ({channel}). Vérifiez que l'adresse est la bonne sur cette chaîne.", + "bg.cosmos.unbonding": "Vos {symbol} restent bloqués et ne rapportent rien jusqu'à la fin de la période de déblocage du réseau.", + "bg.cosmos.moveStake": "Déplacer {amount} de staking de {from} vers {to}", + "bg.cosmos.claimRewardsFrom": "Récupérer les récompenses de staking de {validator}", + "bg.cosmos.grantAuthz": "Laisser {grantee} agir pour votre compte", + "bg.cosmos.authzTakeAll": "Ceci permet à {grantee} de déplacer vos fonds sans vous redemander, jusqu'à ce que vous retiriez l'autorisation. {grantee} pourrait tout prendre.", + "bg.cosmos.authzActs": "Ceci permet à {grantee} d'agir pour votre compte sans vous redemander, jusqu'à ce que vous retiriez l'autorisation. N'autorisez cela que pour des applications de confiance.", + "bg.cosmos.revokeAuthz": "Empêcher {grantee} d'agir pour votre compte", + "bg.cosmos.grantFees": "Laisser {grantee} payer les frais de réseau avec vos {symbol}", + "bg.cosmos.feegrantUpTo": "Ceci permet à {grantee} de dépenser jusqu'à {amount} de vos fonds en frais de réseau, sans vous redemander.", + "bg.cosmos.feegrantAll": "Ceci permet à {grantee} de dépenser vos {symbol} en frais de réseau sans limite, sans vous redemander.", + "bg.cosmos.revokeFees": "Empêcher {grantee} de payer les frais de réseau avec vos fonds", + "bg.cosmos.block": "bloc {height}", + "bg.cosmos.appGetsSigned": "{host} (reçoit la transaction signée)", + "bg.cosmos.wrongPrefix": "Cette adresse appartient à un autre réseau. Les adresses sur {network} commencent par {prefix}.", + "bg.cosmos.noAccount": "Votre compte n'existe pas encore sur {network}. Recevez d'abord des {symbol}.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "Ce compte TRON est contrôlé par une autre clé, donc Clip Wallet ne peut pas signer pour lui.", + "bg.tron.notOpen": "Votre compte TRON n'est pas encore ouvert. Recevez d'abord un peu de TRX.", + "bg.tron.contractRecipient": "Impossible d'envoyer des TRX directement à un smart contract.", + "bg.tron.newAccountFee": "Cette adresse n'est pas encore ouverte sur TRON. Y envoyer des fonds coûte aussi {amount} pour l'ouvrir.", + "bg.tron.getsSigned": "{host} (reçoit la transaction signée)", + "bg.tron.energy": "énergie", + "bg.tron.bandwidth": "bande passante", + "bg.tron.tronPower": "droit de vote", + "bg.tron.stakeFor": "Staker {amount} (ressource : {resource})", + "bg.tron.days": "{count} jours", + "bg.tron.cancelUnstaking": "Annuler vos retraits du staking en attente et staker à nouveau ces TRX", + "bg.tron.lend": "Prêter la ressource {resource} de vos {amount} stakés à {to}", + "bg.tron.stopLending": "Reprendre la ressource {resource} des {amount} prêtés à {to}", + "bg.tron.lockedHours": "Environ {hours} heures. Vous ne pouvez pas le reprendre avant.", + "bg.tron.vote": "Voter pour {count} Super Representatives", + "bg.tron.votesReplace": "Cela remplace tous vos votes précédents.", + "bg.tron.claimVoteRewards": "Réclamer vos récompenses de vote", + "bg.tron.changePermissions": "Changer qui contrôle votre compte TRON", + "bg.tron.keysThreshold": "{keys} (requiert {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "Payer jusqu'à {amount} pour que {to} reçoive {get}", + "bg.xrpl.atMost": "Au plus {amount}", + "bg.xrpl.anyAmount": "N'importe quel montant", + "bg.xrpl.partialPayment": "C'est un paiement partiel : le destinataire peut recevoir bien moins que {amount}. Des boutiques et des plateformes d'échange ont été trompées ainsi.", + "bg.xrpl.xrpToSelf": "Envoyer des XRP vers votre propre compte ne fait rien, donc le XRP Ledger le refusera.", + "bg.xrpl.frees": "Cela libère la réserve de {amount} qu'il bloquait.", + "bg.xrpl.locksReserve": "{amount} de vos XRP restent bloqués tant que ce jeton est ajouté", + "bg.xrpl.freezesLine": "Cela gèle aussi le jeton pour l'autre partie.", + "bg.xrpl.offer.fillOrKill": "Exécuter entièrement maintenant, sinon annuler", + "bg.xrpl.offer.immediate": "Exécuter ce qui est possible maintenant, annuler le reste", + "bg.xrpl.offer.standing": "Reste ouverte jusqu'à son exécution, son expiration ou son annulation", + "bg.xrpl.cancelOffer": "Annuler une offre", + "bg.xrpl.changeSettings": "Modifier les réglages de votre compte", + "bg.xrpl.asf.requireDest": "Exiger un tag de destination sur les paiements reçus", + "bg.xrpl.asf.requireAuth": "Approuver chaque détenteur de vos jetons", + "bg.xrpl.asf.disallowXrp": "Demander de ne pas vous envoyer de XRP", + "bg.xrpl.asf.disableMaster": "Désactiver la clé propre de votre compte", + "bg.xrpl.asf.accountTxnId": "Suivre l'identifiant de votre dernière transaction", + "bg.xrpl.asf.noFreeze": "Renoncer définitivement à geler vos jetons", + "bg.xrpl.asf.globalFreeze": "Geler tous les jetons que vous avez émis", + "bg.xrpl.asf.defaultRipple": "Laisser vos jetons passer d'un détenteur à l'autre (rippling)", + "bg.xrpl.asf.depositAuth": "N'accepter que les paiements des comptes que vous approuvez", + "bg.xrpl.asf.nftMinter": "Autoriser un autre compte à créer des NFT pour vous", + "bg.xrpl.asf.noNftOffers": "Bloquer les offres de NFT entrantes", + "bg.xrpl.asf.noChecks": "Bloquer les chèques entrants", + "bg.xrpl.asf.noPayChannels": "Bloquer les canaux de paiement entrants", + "bg.xrpl.asf.noTrustLines": "Bloquer les trust lines entrantes", + "bg.xrpl.asf.clawback": "Vous permettre de reprendre les jetons que vous émettez (clawback)", + "bg.xrpl.asf.lockTokens": "Permettre aux détenteurs de bloquer vos jetons en séquestre", + "bg.xrpl.disableMasterWarning": "Cela désactive la clé propre de votre compte. Ensuite, seule une autre clé ou une liste de signataires pourra l'utiliser ; s'il n'y en a pas, il est bloqué définitivement.", + "bg.xrpl.issuerSettingWarning": "Cela change le fonctionnement des jetons que vous émettez. Certains réglages sont irréversibles.", + "bg.xrpl.nftMinterWarning": "Cela permet à {who} de créer des NFT au nom de votre compte.", + "bg.xrpl.cleared": "(effacé)", + "bg.xrpl.removeRegularKey": "Retirer la clé supplémentaire de votre compte", + "bg.xrpl.regularKeyWarning": "{who} obtient une clé qui peut tout faire avec votre compte, y compris le vider.", + "bg.xrpl.removeSigners": "Retirer les comptes qui peuvent signer pour le vôtre", + "bg.xrpl.setSigners": "Autoriser {count} autres comptes à signer pour le vôtre", + "bg.xrpl.signersWarning": "Ensemble, ces comptes peuvent tout faire avec votre compte, y compris le vider.", + "bg.xrpl.closeAccount": "Fermer votre compte XRP Ledger et tout envoyer à {to}", + "bg.xrpl.closeWarning": "Cela supprime votre compte XRP Ledger et envoie tous vos XRP à {to}. Les frais de {fee} sont brûlés.", + "bg.xrpl.mintNft": "Créer un NFT", + "bg.xrpl.taxon": "Taxon {taxon}", + "bg.xrpl.nft.transferable": "Peut être vendu ou donné", + "bg.xrpl.nft.notTransferable": "Ne peut pas être transmis", + "bg.xrpl.nft.issuerCanBurn": "Son créateur peut le brûler", + "bg.xrpl.nft.mutable": "Son lien peut être modifié plus tard", + "bg.xrpl.burnNft": "Brûler un NFT", + "bg.xrpl.giveNft": "Offrir votre NFT gratuitement à {to}", + "bg.xrpl.freeForAnyone": "N'importe qui peut prendre ce NFT sans rien payer.", + "bg.xrpl.sellNft": "Proposer votre NFT pour {amount}", + "bg.xrpl.buyNft": "Proposer {amount} pour un NFT", + "bg.xrpl.offerHolds": "Rien n'est prélevé tant que le propriétaire n'a pas accepté", + "bg.xrpl.acceptNftOffer": "Accepter une offre de NFT", + "bg.xrpl.brokered": "Rapproche une offre d'achat et une offre de vente (vente par intermédiaire)", + "bg.xrpl.offerUnknown": "Clip Wallet n'a pas pu consulter cette offre et ne peut donc pas montrer ce que vous payez ou recevez.", + "bg.xrpl.acceptNftSell": "Acheter un NFT pour {amount}", + "bg.xrpl.acceptNftBuy": "Vendre votre NFT pour {amount}", + "bg.xrpl.cancelNftOffers": "Annuler {count} offres de NFT", + "bg.xrpl.escrowCreate": "Bloquer {amount} en séquestre pour {to}", + "bg.xrpl.escrowCondition": "Sa libération exige un secret (crypto-condition)", + "bg.xrpl.escrowLocked": "Ces fonds restent bloqués jusqu'à la libération ou l'annulation du séquestre.", + "bg.xrpl.escrowFinish": "Libérer un séquestre", + "bg.xrpl.escrowCancel": "Annuler un séquestre", + "bg.xrpl.ammDeposit": "Ajouter des fonds à un pool de liquidité", + "bg.xrpl.ammWithdraw": "Retirer des fonds d'un pool de liquidité", + "bg.xrpl.yourShare": "Votre part du pool", + "bg.xrpl.poolAmounts": "Le pool fixe les montants exacts au moment de l'exécution.", + "bg.xrpl.unknownTx": "Approuver une transaction XRP Ledger de type {type}", + "bg.xrpl.recipientUnchecked": "Clip Wallet n'a pas pu vérifier le compte du destinataire.", + "bg.xrpl.newAccountTooSmall": "Ce compte XRP Ledger n'existe pas encore. L'ouvrir demande au moins {reserve}, donc cela échouera.", + "bg.xrpl.opensAccount": "Cela ouvre un nouveau compte XRP Ledger ; {reserve} y restent bloqués comme réserve.", + "bg.xrpl.noRecipientAccount": "Ce compte XRP Ledger n'existe pas, il ne peut donc pas recevoir de jetons.", + "bg.xrpl.destinationTagNeeded": "Ce destinataire a besoin d'un tag de destination et aucun n'est indiqué. Sans lui, les fonds peuvent être perdus.", + "bg.xrpl.disallowsXrp": "Ce compte indique qu'il ne veut pas recevoir de XRP.", + "bg.xrpl.depositAuth": "Ce compte n'accepte que les paiements des comptes qu'il a approuvés, cela peut donc échouer.", + "bg.xrpl.noTrustLine": "Le destinataire n'a pas ajouté {symbol} à son compte, donc ce paiement échouera.", + "bg.xrpl.notEnoughXrp": "Vous n'avez pas assez de XRP : {reserve} doivent rester sur votre compte comme réserve.", + "bg.xrpl.highFee": "Les frais de réseau sont inhabituellement élevés : {fee}.", + "bg.xrpl.appSends": "{host} (reçoit la transaction signée)", + "bg.xrpl.feeCharged": "Les frais de réseau ont tout de même été prélevés.", + "bg.xrpl.label.destinationTag": "Tag de destination", + "bg.xrpl.label.sourceTag": "Tag d'origine", + "bg.xrpl.label.deliversAtLeast": "Livre au moins", + "bg.xrpl.label.issuer": "Émetteur", + "bg.xrpl.label.replacesOffer": "Remplace l'offre", + "bg.xrpl.label.turnsOn": "Active", + "bg.xrpl.label.turnsOff": "Désactive", + "bg.xrpl.label.domain": "Domaine", + "bg.xrpl.label.transferFee": "Frais de transfert", + "bg.xrpl.label.nftMinter": "Créateur de NFT", + "bg.xrpl.label.quorum": "Signatures nécessaires", + "bg.xrpl.label.link": "Lien", + "bg.xrpl.label.royalty": "Redevance", + "bg.xrpl.label.releaseAfter": "Libérable après", + "bg.xrpl.label.cancelAfter": "Annulable après", + "bg.xrpl.err.wrongNetwork": "Cette app demande un autre réseau XRP Ledger que celui auquel elle est connectée. Rien n'a été signé.", + "bg.xrpl.err.multisig": "La multisignature n'est pas encore disponible dans Clip Wallet.", + "bg.xrpl.err.alreadySigned": "Cette transaction est déjà signée, Clip Wallet ne la signera donc pas une deuxième fois.", + "bg.xrpl.err.cantRead": "Clip Wallet ne peut pas lire une partie de cette transaction XRP Ledger ({field}), il ne peut donc pas la signer.", + "bg.xrpl.err.notActivated": "Votre compte XRP Ledger n'est pas encore activé. Recevez au moins {reserve} XRP pour l'activer.", + "bg.xrpl.err.otherKey": "La clé propre de ce compte est désactivée et une autre clé le contrôle, Clip Wallet ne peut donc pas signer pour lui.", + "bg.xrpl.err.xAddressNetwork": "Cette adresse X appartient à un autre réseau XRP Ledger.", + "bg.xrpl.err.badAddress": "Cela ne ressemble pas à une adresse XRP Ledger.", + "bg.xrpl.err.notToken": "Ce n'est pas un jeton du XRP Ledger.", + "bg.xrpl.err.tagNeeded": "Cette adresse a besoin d'un tag de destination. Demandez-le au destinataire, ou demandez une adresse X qui l'inclut.", + "bg.xrpl.err.notEnoughXrp": "Vous n'avez pas assez de XRP. {reserve} XRP doivent rester sur votre compte, et les frais sont de {fee} XRP.", + "bg.xrpl.err.newAccount": "Ce compte XRP Ledger n'existe pas encore. Envoyez au moins {reserve} XRP pour l'ouvrir.", + "bg.xrpl.err.feeOnly": "Il vous faut un peu de XRP pour payer les frais de réseau.", + "bg.xrpl.err.noTrustLine": "Le destinataire n'a pas encore ajouté {symbol} à son compte XRP Ledger. Demandez-lui de l'ajouter d'abord.", + "bg.xrpl.err.reserveForToken": "Ajouter un jeton bloque {amount} XRP, plus les frais. Ajoutez d'abord un peu de XRP.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "Appeler {action} sur {contract}", + "bg.antelope.unknownToken": "{symbol} de {contract} n'est pas un jeton que Clip Wallet connaît. Vérifiez le contrat avant de l'envoyer.", + "bg.antelope.noRecipient": "Aucun compte nommé {to} n'existe sur ce réseau, donc cela échouera.", + "bg.antelope.updateAuth": "Changer qui contrôle la permission {permission} de {account}", + "bg.antelope.deleteAuth": "Supprimer la permission {permission} de {account}", + "bg.antelope.linkAuth": "Autoriser la permission {permission} à exécuter {action}", + "bg.antelope.unlinkAuth": "Empêcher une permission d'exécuter {action}", + "bg.antelope.authWarning": "Cela change les clés ou comptes qui contrôlent {account}. S'ils ne sont pas à vous, ils pourraient tout prendre.", + "bg.antelope.unlinkWarning": "Cela change la clé qui peut exécuter certaines actions de votre compte.", + "bg.antelope.label.alsoSigns": "Il faut aussi", + "bg.antelope.delayed": "Cela ne s'exécute que {seconds} secondes après l'envoi et peut être annulé d'ici là.", + "bg.antelope.feeFree": "Aucuns frais (ce réseau prend en charge le CPU et le NET pour vous)", + "bg.antelope.feeResources": "Aucuns frais en jetons : cela utilise le CPU et le NET de votre compte", + "bg.antelope.appSends": "{host} (reçoit la transaction signée)", + "bg.antelope.err.offline": "Impossible de joindre {network}. Vérifiez votre connexion et réessayez.", + "bg.antelope.err.wrongNetwork": "Cette demande vise un autre réseau Antelope que celui sur lequel vous êtes. Rien n'a été signé.", + "bg.antelope.err.contextFree": "Cette transaction utilise des fonctions que Clip Wallet ne signe pas (actions sans contexte ou extensions).", + "bg.antelope.err.noAbi": "Clip Wallet ne peut pas lire le contrat {contract}, il ne peut donc pas préparer cette demande.", + "bg.antelope.err.permission": "Il faut une permission de votre compte que cette clé ne contrôle pas.", + "bg.antelope.err.otherSigners": "Cette transaction doit aussi être signée par d'autres comptes, Clip Wallet ne peut donc pas l'envoyer seul.", + "bg.antelope.err.noAccountTestnet": "Aucun compte {network} n'utilise encore cette clé. Sur les réseaux Antelope, un compte est un nom créé sur la chaîne : créez un compte de test gratuit pour votre clé publique avec l'outil testnet du réseau, puis revenez.", + "bg.antelope.err.noAccount": "Aucun compte {network} n'utilise encore cette clé. Sur les réseaux Antelope, un compte est un nom que quelqu'un crée sur la chaîne pour votre clé publique : une plateforme d'échange, un ami qui a un compte ou un service de création de comptes peut le faire.", + "bg.antelope.err.notToken": "Ce n'est pas un jeton de ce réseau.", + "bg.antelope.err.badAccount": "Ce n'est pas un nom de compte valide. Les noms font jusqu'à 12 caractères : a–z, 1–5 et des points.", + "bg.antelope.err.cpuPowerup": "Votre compte n'a plus de temps CPU ou NET sur {network}. Louez-en avec PowerUp (un peu d'EOS pour 24 heures), puis réessayez.", + "bg.antelope.err.cpuStake": "Votre compte n'a plus de temps CPU ou NET sur {network}. Mettez un peu de TLOS en staking pour le CPU et le NET, ou louez-les avec PowerUp, puis réessayez.", + "bg.antelope.err.cpuFree": "{network} n'a pas pu prendre en charge le CPU ou le NET pour cela pour l'instant. Patientez un peu et réessayez.", + "bg.antelope.err.ram": "Votre compte n'a pas assez de RAM sur {network} pour cela. Achetez un peu de RAM, puis réessayez.", + "bg.antelope.err.balance": "Vous n'avez pas assez de ce jeton pour cela.", + "bg.antelope.err.expired": "Cette transaction a expiré avant d'être envoyée. Réessayez.", + "bg.antelope.err.auth": "Le réseau indique que cette clé ne peut pas signer pour ce compte. Rien n'a été envoyé.", + "bg.antelope.err.noRecipient": "Le compte du destinataire n'existe pas sur ce réseau. Rien n'a été envoyé.", + "bg.antelope.err.duplicate": "Cette transaction exacte a déjà été envoyée. Consultez l'Activité.", + "bg.antelope.err.rejected": "{network} a refusé cette transaction. Rien n'a été envoyé.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "Récupérer vos récompenses de staking auprès de {validator}", + "bg.multiversx.withdrawFrom": "Retirer auprès de {validator} vos EGLD sortis du staking", + "bg.multiversx.restakeRewardsWith": "Remettre vos récompenses en staking auprès de {validator}", + "bg.multiversx.labelGuardian": "Gardien", + "bg.multiversx.setGuardianTitle": "Faire de {guardian} le gardien de votre compte", + "bg.multiversx.setGuardianWarn": "Une fois un gardien actif, chaque transaction de ce compte nécessite sa cosignature. Si vous n'avez pas choisi {guardian}, quelqu'un pourrait vous bloquer l'accès à votre compte.", + "bg.multiversx.guardAccountTitle": "Activer le gardien de votre compte", + "bg.multiversx.guardAccountWarn": "Désormais, chaque transaction de ce compte nécessite la cosignature du gardien. Clip Wallet ne peut pas la fournir : vous ne pourrez plus envoyer depuis Clip Wallet.", + "bg.multiversx.unguardAccountTitle": "Désactiver le gardien de votre compte", + "bg.multiversx.changeOwnerTitle": "Céder le contrat {contract} à {owner}", + "bg.multiversx.changeOwnerWarn": "{owner} devient propriétaire du contrat {contract}. Le nouveau propriétaire peut modifier son code et prendre ce qu'il contient. Ne le faites que si vous voulez vraiment le céder.", + "bg.multiversx.guardedAccount": "Ce compte a un gardien, et Clip Wallet ne peut pas obtenir sa cosignature. Rien n'a été envoyé.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "Ceci est envoyé à un identifiant de compte, le type d'adresse de dépôt que donnent les plateformes d'échange. Vérifiez qu'il correspond exactement à celui affiché par la plateforme.", + "bg.icp.expired": "Ce transfert a expiré avant que vous ne l'approuviez. Rien n'a été envoyé. Réessayez.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "Postcondition", + "bg.stacks.pcYouSendExactly": "Vous envoyez exactement {amount}", + "bg.stacks.pcYouSendAtMost": "Vous envoyez au plus {amount}", + "bg.stacks.pcYouSendAtLeast": "Vous envoyez au moins {amount}", + "bg.stacks.pcYouSendMoreThan": "Vous envoyez plus de {amount}", + "bg.stacks.pcYouSendLessThan": "Vous envoyez moins de {amount}", + "bg.stacks.pcSendsExactly": "{who} envoie exactement {amount}", + "bg.stacks.pcSendsAtMost": "{who} envoie au plus {amount}", + "bg.stacks.pcSendsAtLeast": "{who} envoie au moins {amount}", + "bg.stacks.pcSendsMoreThan": "{who} envoie plus de {amount}", + "bg.stacks.pcSendsLessThan": "{who} envoie moins de {amount}", + "bg.stacks.pcYouSendNft": "Vous envoyez {item}", + "bg.stacks.pcYouKeepNft": "Vous conservez {item}", + "bg.stacks.pcYouMaySendNft": "Vous enverrez peut-être {item}", + "bg.stacks.pcSendsNft": "{who} envoie {item}", + "bg.stacks.pcKeepsNft": "{who} conserve {item}", + "bg.stacks.pcMaySendNft": "{who} enverra peut-être {item}", + "bg.stacks.pcStakingRule": "Une règle de staking pour {who}", + "bg.stacks.allowMode": "Cela permet au contrat de déplacer n'importe lequel de vos actifs, pas seulement ceux listés. Ne l'approuvez que si vous faites entièrement confiance à {host}.", + "bg.stacks.originatorMode": "Seuls les transferts listés peuvent quitter votre compte. Le contrat peut tout de même déplacer les actifs d'autres comptes.", + "bg.stacks.nothingLeaves": "Aucun de vos actifs ne peut quitter votre compte dans cette transaction, à part les frais de réseau.", + "bg.stacks.highFee": "Les frais de réseau s'élèvent à {fee}, ce qui est anormalement élevé.", + "bg.stacks.sponsorPays": "Un sponsor choisi par {host}", + "bg.stacks.deployNamed": "Créer le smart contract {name}", + "bg.stacks.notYourTx": "Cette transaction est signée par un autre compte Stacks, Clip Wallet ne peut donc pas la signer.", + "bg.stacks.multisig": "Clip Wallet ne peut pas encore signer pour les comptes Stacks partagés (multisignature).", + "bg.stacks.memoTooLong": "Le mémo est trop long. Un mémo Stacks contient au maximum 34 octets.", + "bg.stacks.feeTooLow": "Les frais de réseau étaient trop bas, le réseau n'a donc pas accepté la transaction. Rien n'a été envoyé. Réessayez.", + "bg.stacks.nonceBusy": "Une autre transaction de ce compte est toujours en attente. Attendez qu'elle se termine, puis réessayez.", + "bg.stacks.otherNetworkTx": "Cette transaction est destinée à un autre réseau Stacks, elle n'a donc pas été envoyée.", + "bg.stacks.badContractCall": "L'appel de contrat de l'application ne correspond pas au contrat sur le réseau. Rien n'a été envoyé.", + "bg.stacks.tooManyPending": "Ce compte a trop de transactions en attente. Attendez que certaines se terminent, puis réessayez.", + "bg.stacks.signatureRefused": "Le réseau n'a pas accepté la signature. Rien n'a été envoyé.", + "bg.stacks.testAddressOnMainnet": "C'est une adresse Stacks de réseau de test (ST…). Utilisez une adresse du réseau principal (SP…).", + "bg.stacks.mainAddressOnTestnet": "C'est une adresse Stacks du réseau principal (SP…). Utilisez une adresse de réseau de test (ST…).", + "bg.stacks.notAnAddress": "Cela ne ressemble pas à une adresse Stacks.", + "bg.stacks.needStxForFee": "Il vous faut un peu de STX pour payer les frais de réseau.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "Tout ce qui reste de votre {symbol} après ceci va à {to}, et ne vous revient pas.", + "bg.fuel.leftoverLost": "{amount} n'est envoyé nulle part par cette transaction et serait perdu.", + "bg.fuel.coinsSpent": "Certaines des pièces utilisées par cette transaction ont déjà été dépensées. Demandez à l'app de réessayer.", + "bg.fuel.feeRose": "Les frais de réseau ont augmenté depuis la préparation. Rien n'a été envoyé. Réessayez.", + "bg.fuel.failedOnChain": "Cette transaction a échoué sur le réseau Fuel. Seuls les frais de réseau ont été payés.", + "bg.fuel.tooManyCoins": "Il faut trop de petites pièces à la fois. Envoyez d'abord un montant plus petit.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "Cette application demande à Clip Wallet d'insérer sa clé ou sa signature dans les données d'un smart contract. Clip Wallet ne peut pas encore signer ce type de transaction.", + "bg.bch.spendsTokens": "Ceci envoie des CashTokens depuis votre portefeuille : {tokens}.", + "bg.bch.highFee": "Les frais de réseau s'élèvent à {fee}, ce qui est anormalement élevé.", + "bg.bch.othersInputs": "{count} pièces d'autres propriétaires ou contrats", + "bg.bch.notCashAddr": "Cela ne ressemble pas à une adresse Bitcoin Cash. Utilisez sa forme CashAddr (bitcoincash:q…).", + "bg.bch.testAddressOnMainnet": "C'est une adresse Bitcoin Cash de réseau de test (bchtest:…). Utilisez une adresse du réseau principal (bitcoincash:…).", + "bg.bch.mainAddressOnTestnet": "C'est une adresse Bitcoin Cash du réseau principal (bitcoincash:…). Utilisez une adresse de réseau de test (bchtest:…).", + "bg.bch.tokenSendUnavailable": "L'envoi de CashTokens n'est pas encore disponible.", + "bg.bch.belowDust": "Envoyez au moins {amount}. Les montants inférieurs ne peuvent pas être envoyés sur Bitcoin Cash.", + "bg.bch.feeTooLow": "Les frais étaient trop bas pour le réseau. Rien n'a été envoyé. Réessayez.", + "bg.bch.coinsSpent": "Certaines pièces de cette transaction avaient déjà été dépensées. Rien n'a été envoyé. Actualisez et réessayez.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "Prélèvement du réseau", + "bg.evm.flatFee": "{amount} par transaction, même en cas d'échec", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "Impossible d'envoyer des {symbol} d'un compte Chainflip à un autre. Pour les déplacer, rachetez-les vers une adresse Ethereum, ou approvisionnez l'autre compte depuis Ethereum.", + "bg.chainflip.redeem": "Racheter {amount} vers {to} sur Ethereum", + "bg.chainflip.redeemAll": "Racheter tous vos {symbol} vers {to} sur Ethereum", + "bg.chainflip.allRedeemable": "Tous vos {symbol} non engagés", + "bg.chainflip.label.executor": "Qui peut le finaliser", + "bg.chainflip.anyone": "N'importe qui", + "bg.chainflip.redeemSteps": "Après un délai d'attente, le rachat est finalisé sur Ethereum (cela coûte du gas en ETH). Si personne ne le finalise avant son expiration, il faut le refaire.", + "bg.chainflip.redeemWarn": "Vos {symbol} quittent Chainflip pour {to} sur Ethereum. Vérifiez que vous contrôlez cette adresse Ethereum.", + "bg.chainflip.bindRedeem": "N'autoriser les rachats que vers {to}, pour toujours", + "bg.chainflip.bindRedeemWarn": "C'est irréversible. Ce compte ne pourra plus racheter de {symbol} que vers {to}. Si vous ne contrôlez pas cette adresse Ethereum, vos {symbol} sont bloqués pour de bon.", + "bg.chainflip.bindExecutor": "Seul {to} pourra finaliser vos rachats, pour toujours", + "bg.chainflip.bindExecutorWarn": "C'est irréversible. Seul {to} pourra finaliser les rachats de {symbol} de ce compte sur Ethereum. Si cette clé est perdue, les {symbol} rachetés ne pourront pas être récupérés.", + "bg.chainflip.moveTo": "Déplacer {amount} vers le compte Chainflip {to}", + "bg.chainflip.moveAllTo": "Déplacer tous vos {symbol} vers le compte Chainflip {to}", + "bg.chainflip.otherAccount": "{to} ne fait pas partie de vos comptes. Vérifiez-le avant d'y déplacer quoi que ce soit.", + "bg.chainflip.foreignRecipient": "Cela part vers {to} sur {chain}. Vérifiez l'adresse : Clip Wallet ne peut pas l'annuler.", + "bg.chainflip.registerLp": "S'inscrire comme fournisseur de liquidité Chainflip", + "bg.chainflip.deregisterLp": "Fermer votre compte de fournisseur de liquidité Chainflip", + "bg.chainflip.refundAddress": "Définir votre adresse de remboursement {chain} sur {to}", + "bg.chainflip.depositAddress": "Obtenir une adresse de dépôt pour {asset}", + "bg.chainflip.label.boostFee": "Frais de Boost", + "bg.chainflip.label.refundTo": "Les remboursements vont à", + "bg.chainflip.withdraw": "Retirer {amount} vers {to} sur {chain}", + "bg.chainflip.swap": "Échanger {amount} contre {asset}", + "bg.chainflip.swapChannel": "Ouvrir un canal pour échanger {from} contre {to}", + "bg.chainflip.addBoost": "Boost : ajouter {amount} au pool à {tier} bps", + "bg.chainflip.stopBoost": "Arrêter le boost de {asset} dans le pool à {tier} bps", + "bg.chainflip.lend": "Prêter {amount}", + "bg.chainflip.unlend": "Récupérer {amount} prêtés", + "bg.chainflip.unlendAll": "Récupérer tous les {asset} prêtés", + "bg.chainflip.borrow": "Emprunter {amount}", + "bg.chainflip.repay": "Rembourser une partie du prêt #{id}", + "bg.chainflip.repayAll": "Rembourser entièrement le prêt #{id}", + "bg.chainflip.onSubAccount": "Sur le sous-compte #{n} : {inner}", + "bg.chainflip.orderFee": "Placer ou modifier un ordre exige au moins 1 {symbol} sur ce compte. L'essentiel vous revient, mais modifier plusieurs fois les ordres d'un même pool dans un seul bloc coûte des frais plus élevés à chaque fois.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts new file mode 100644 index 00000000..d0a028d1 --- /dev/null +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Hindi: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../hi.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "IBC चैनल {channel}", + "bg.cosmos.ibcLeaves": "इससे टोकन IBC ({channel}) के ज़रिए किसी दूसरी चेन पर जाते हैं। जाँच लें कि उस चेन के लिए एड्रेस सही है।", + "bg.cosmos.unbonding": "नेटवर्क की अनबॉन्डिंग अवधि खत्म होने तक आपके {symbol} लॉक रहते हैं और कुछ नहीं कमाते।", + "bg.cosmos.moveStake": "{amount} का स्टेक {from} से {to} पर ले जाएँ", + "bg.cosmos.claimRewardsFrom": "{validator} से स्टेकिंग रिवॉर्ड क्लेम करें", + "bg.cosmos.grantAuthz": "{grantee} को आपके अकाउंट की ओर से काम करने दें", + "bg.cosmos.authzTakeAll": "इससे {grantee} बिना दोबारा पूछे आपके फ़ंड ले जा सकता है, जब तक आप अनुमति वापस नहीं लेते। वह सब कुछ ले सकता है।", + "bg.cosmos.authzActs": "इससे {grantee} बिना दोबारा पूछे आपके अकाउंट की ओर से काम कर सकता है, जब तक आप अनुमति वापस नहीं लेते। इसकी अनुमति सिर्फ़ उन्हीं ऐप्स को दें जिन पर आपको भरोसा है।", + "bg.cosmos.revokeAuthz": "{grantee} को आपके अकाउंट की ओर से काम करने से रोकें", + "bg.cosmos.grantFees": "{grantee} को आपके {symbol} से नेटवर्क शुल्क चुकाने दें", + "bg.cosmos.feegrantUpTo": "इससे {grantee} बिना दोबारा पूछे नेटवर्क शुल्क पर आपके फ़ंड में से {amount} तक खर्च कर सकता है।", + "bg.cosmos.feegrantAll": "इससे {grantee} बिना दोबारा पूछे नेटवर्क शुल्क पर आपके {symbol} बिना किसी सीमा के खर्च कर सकता है।", + "bg.cosmos.revokeFees": "{grantee} को आपके फ़ंड से नेटवर्क शुल्क चुकाने से रोकें", + "bg.cosmos.block": "ब्लॉक {height}", + "bg.cosmos.appGetsSigned": "{host} (इसे साइन किया हुआ ट्रांज़ैक्शन मिलता है)", + "bg.cosmos.wrongPrefix": "यह एड्रेस किसी दूसरे नेटवर्क का है। {network} पर एड्रेस {prefix} से शुरू होते हैं।", + "bg.cosmos.noAccount": "आपका अकाउंट अभी {network} पर नहीं है। पहले कुछ {symbol} प्राप्त करें।", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "यह TRON अकाउंट किसी दूसरी कुंजी से नियंत्रित है, इसलिए Clip Wallet इसके लिए साइन नहीं कर सकता।", + "bg.tron.notOpen": "आपका TRON अकाउंट अभी खुला नहीं है। पहले कुछ TRX प्राप्त करें।", + "bg.tron.contractRecipient": "TRX को सीधे किसी स्मार्ट कॉन्ट्रैक्ट पर नहीं भेजा जा सकता।", + "bg.tron.newAccountFee": "यह एड्रेस अभी TRON पर खुला नहीं है। इस पर भेजने में इसे खोलने के लिए {amount} अलग से लगेंगे।", + "bg.tron.getsSigned": "{host} (इसे साइन किया गया ट्रांज़ैक्शन मिलता है)", + "bg.tron.energy": "एनर्जी", + "bg.tron.bandwidth": "बैंडविड्थ", + "bg.tron.tronPower": "वोटिंग पावर", + "bg.tron.stakeFor": "{resource} के लिए {amount} स्टेक करें", + "bg.tron.days": "{count} दिन", + "bg.tron.cancelUnstaking": "अपनी लंबित अनस्टेकिंग रद्द करें और उस TRX को फिर से स्टेक करें", + "bg.tron.lend": "आपके स्टेक किए गए {amount} की {resource} {to} को उधार दें", + "bg.tron.stopLending": "{to} को उधार दिए गए {amount} की {resource} वापस लें", + "bg.tron.lockedHours": "लगभग {hours} घंटे। उससे पहले आप इसे वापस नहीं ले सकते।", + "bg.tron.vote": "{count} Super Representatives को वोट दें", + "bg.tron.votesReplace": "यह आपके पहले के सभी वोट बदल देता है।", + "bg.tron.claimVoteRewards": "अपने वोटिंग रिवॉर्ड क्लेम करें", + "bg.tron.changePermissions": "बदलें कि आपका TRON अकाउंट कौन नियंत्रित करता है", + "bg.tron.keysThreshold": "{keys} ({threshold} ज़रूरी)", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "ज़्यादा से ज़्यादा {amount} दें ताकि {to} को {get} मिले", + "bg.xrpl.atMost": "ज़्यादा से ज़्यादा {amount}", + "bg.xrpl.anyAmount": "कोई भी राशि", + "bg.xrpl.partialPayment": "यह आंशिक पेमेंट है: पाने वाले को {amount} से बहुत कम मिल सकता है। दुकानों और एक्सचेंजों को इसी तरह ठगा गया है।", + "bg.xrpl.xrpToSelf": "अपने ही अकाउंट में XRP भेजने से कुछ नहीं होता, इसलिए XRP Ledger इसे अस्वीकार कर देगा।", + "bg.xrpl.frees": "इससे लॉक किया गया {amount} का रिज़र्व खाली हो जाता है।", + "bg.xrpl.locksReserve": "जब तक यह टोकन जुड़ा है, आपके XRP में से {amount} लॉक रहेगा", + "bg.xrpl.freezesLine": "इससे दूसरी तरफ़ के लिए भी टोकन फ़्रीज़ हो जाता है।", + "bg.xrpl.offer.fillOrKill": "अभी पूरा पूरा करें, नहीं तो रद्द करें", + "bg.xrpl.offer.immediate": "अभी जितना हो सके पूरा करें, बाकी रद्द करें", + "bg.xrpl.offer.standing": "पूरा होने, समय ख़त्म होने या आपके रद्द करने तक खुला रहता है", + "bg.xrpl.cancelOffer": "ऑफ़र रद्द करें", + "bg.xrpl.changeSettings": "अपने अकाउंट की सेटिंग बदलें", + "bg.xrpl.asf.requireDest": "आपको किए जाने वाले पेमेंट पर डेस्टिनेशन टैग ज़रूरी करें", + "bg.xrpl.asf.requireAuth": "अपने टोकन के हर होल्डर को मंज़ूरी दें", + "bg.xrpl.asf.disallowXrp": "लोगों से कहें कि आपको XRP न भेजें", + "bg.xrpl.asf.disableMaster": "अपने अकाउंट की अपनी की बंद करें", + "bg.xrpl.asf.accountTxnId": "अपने पिछले ट्रांज़ैक्शन की आईडी ट्रैक करें", + "bg.xrpl.asf.noFreeze": "अपने टोकन फ़्रीज़ करने का अधिकार हमेशा के लिए छोड़ें", + "bg.xrpl.asf.globalFreeze": "आपके जारी किए सभी टोकन फ़्रीज़ करें", + "bg.xrpl.asf.defaultRipple": "अपने टोकन को होल्डरों के बीच चलने दें (रिपलिंग)", + "bg.xrpl.asf.depositAuth": "सिर्फ़ उन्हीं अकाउंट से पेमेंट लें जिन्हें आप मंज़ूरी दें", + "bg.xrpl.asf.nftMinter": "किसी दूसरे अकाउंट को आपके लिए NFT बनाने दें", + "bg.xrpl.asf.noNftOffers": "आने वाले NFT ऑफ़र ब्लॉक करें", + "bg.xrpl.asf.noChecks": "आने वाले चेक ब्लॉक करें", + "bg.xrpl.asf.noPayChannels": "आने वाले पेमेंट चैनल ब्लॉक करें", + "bg.xrpl.asf.noTrustLines": "आने वाली ट्रस्ट लाइन ब्लॉक करें", + "bg.xrpl.asf.clawback": "आपके जारी किए टोकन वापस लेने दें (क्लॉबैक)", + "bg.xrpl.asf.lockTokens": "होल्डरों को आपके टोकन एस्क्रो में लॉक करने दें", + "bg.xrpl.disableMasterWarning": "इससे आपके अकाउंट की अपनी की बंद हो जाती है। इसके बाद सिर्फ़ कोई दूसरी की या साइनर लिस्ट ही अकाउंट इस्तेमाल कर सकेगी; अगर कोई नहीं है, तो अकाउंट हमेशा के लिए लॉक हो जाएगा।", + "bg.xrpl.issuerSettingWarning": "इससे बदलता है कि आपके जारी किए टोकन कैसे काम करते हैं। इनमें से कुछ बदलाव वापस नहीं लिए जा सकते।", + "bg.xrpl.nftMinterWarning": "इससे {who} आपके अकाउंट के नाम पर NFT बना सकता है।", + "bg.xrpl.cleared": "(हटाया गया)", + "bg.xrpl.removeRegularKey": "अपने अकाउंट से अतिरिक्त की हटाएँ", + "bg.xrpl.regularKeyWarning": "{who} को ऐसी की मिलती है जो आपके अकाउंट के साथ कुछ भी कर सकती है, उसे ख़ाली करना भी।", + "bg.xrpl.removeSigners": "वे अकाउंट हटाएँ जो आपके अकाउंट के लिए साइन कर सकते हैं", + "bg.xrpl.setSigners": "{count} दूसरे अकाउंट को आपके अकाउंट के लिए साइन करने दें", + "bg.xrpl.signersWarning": "ये अकाउंट मिलकर आपके अकाउंट के साथ कुछ भी कर सकते हैं, उसे ख़ाली करना भी।", + "bg.xrpl.closeAccount": "अपना XRP Ledger अकाउंट बंद करें और सब कुछ {to} को भेजें", + "bg.xrpl.closeWarning": "इससे आपका XRP Ledger अकाउंट मिट जाता है और आपका पूरा XRP {to} को चला जाता है। {fee} का शुल्क बर्न हो जाता है।", + "bg.xrpl.mintNft": "एक NFT बनाएँ", + "bg.xrpl.taxon": "टैक्सन {taxon}", + "bg.xrpl.nft.transferable": "बेचा या दिया जा सकता है", + "bg.xrpl.nft.notTransferable": "आगे नहीं दिया जा सकता", + "bg.xrpl.nft.issuerCanBurn": "बनाने वाला इसे बर्न कर सकता है", + "bg.xrpl.nft.mutable": "इसका लिंक बाद में बदला जा सकता है", + "bg.xrpl.burnNft": "एक NFT बर्न करें", + "bg.xrpl.giveNft": "अपना NFT {to} को मुफ़्त में ऑफ़र करें", + "bg.xrpl.freeForAnyone": "कोई भी यह NFT मुफ़्त में ले सकता है।", + "bg.xrpl.sellNft": "अपना NFT {amount} में ऑफ़र करें", + "bg.xrpl.buyNft": "एक NFT के लिए {amount} ऑफ़र करें", + "bg.xrpl.offerHolds": "मालिक के मंज़ूर करने तक कुछ नहीं कटता", + "bg.xrpl.acceptNftOffer": "एक NFT ऑफ़र मंज़ूर करें", + "bg.xrpl.brokered": "एक ख़रीद और एक बिक्री ऑफ़र को मिलाता है (ब्रोकर वाली बिक्री)", + "bg.xrpl.offerUnknown": "Clip Wallet यह ऑफ़र नहीं देख सका, इसलिए यह नहीं दिखा सकता कि आप क्या देंगे या क्या पाएँगे।", + "bg.xrpl.acceptNftSell": "{amount} में एक NFT ख़रीदें", + "bg.xrpl.acceptNftBuy": "अपना NFT {amount} में बेचें", + "bg.xrpl.cancelNftOffers": "{count} NFT ऑफ़र रद्द करें", + "bg.xrpl.escrowCreate": "{to} के लिए {amount} एस्क्रो में लॉक करें", + "bg.xrpl.escrowCondition": "रिलीज़ करने के लिए एक सीक्रेट चाहिए (क्रिप्टो-कंडीशन)", + "bg.xrpl.escrowLocked": "एस्क्रो रिलीज़ या रद्द होने तक ये फ़ंड लॉक रहेंगे।", + "bg.xrpl.escrowFinish": "एक एस्क्रो रिलीज़ करें", + "bg.xrpl.escrowCancel": "एक एस्क्रो रद्द करें", + "bg.xrpl.ammDeposit": "लिक्विडिटी पूल में फ़ंड जोड़ें", + "bg.xrpl.ammWithdraw": "लिक्विडिटी पूल से फ़ंड निकालें", + "bg.xrpl.yourShare": "पूल में आपका हिस्सा", + "bg.xrpl.poolAmounts": "चलने के समय सही राशि पूल तय करता है।", + "bg.xrpl.unknownTx": "एक XRP Ledger {type} ट्रांज़ैक्शन मंज़ूर करें", + "bg.xrpl.recipientUnchecked": "Clip Wallet पाने वाले का अकाउंट जाँच नहीं सका।", + "bg.xrpl.newAccountTooSmall": "यह XRP Ledger अकाउंट अभी मौजूद नहीं है। इसे खोलने के लिए कम से कम {reserve} चाहिए, इसलिए यह फ़ेल हो जाएगा।", + "bg.xrpl.opensAccount": "इससे एक नया XRP Ledger अकाउंट खुलता है; इसमें से {reserve} वहाँ रिज़र्व के तौर पर लॉक रहेगा।", + "bg.xrpl.noRecipientAccount": "यह XRP Ledger अकाउंट मौजूद नहीं है, इसलिए यह टोकन नहीं ले सकता।", + "bg.xrpl.destinationTagNeeded": "इस पाने वाले को डेस्टिनेशन टैग चाहिए और कोई टैग नहीं दिया गया है। इसके बिना फ़ंड खो सकते हैं।", + "bg.xrpl.disallowsXrp": "यह अकाउंट कहता है कि वह XRP नहीं लेना चाहता।", + "bg.xrpl.depositAuth": "यह अकाउंट सिर्फ़ उन्हीं अकाउंट से पेमेंट लेता है जिन्हें उसने मंज़ूरी दी है, इसलिए यह फ़ेल हो सकता है।", + "bg.xrpl.noTrustLine": "पाने वाले ने अपने अकाउंट में {symbol} नहीं जोड़ा है, इसलिए यह पेमेंट फ़ेल हो जाएगा।", + "bg.xrpl.notEnoughXrp": "इसके लिए आपके पास काफ़ी XRP नहीं है: {reserve} रिज़र्व के तौर पर आपके अकाउंट में रहना ज़रूरी है।", + "bg.xrpl.highFee": "नेटवर्क शुल्क असामान्य रूप से ज़्यादा है: {fee}।", + "bg.xrpl.appSends": "{host} (इसे साइन किया हुआ ट्रांज़ैक्शन मिलता है)", + "bg.xrpl.feeCharged": "नेटवर्क शुल्क फिर भी लिया गया।", + "bg.xrpl.label.destinationTag": "डेस्टिनेशन टैग", + "bg.xrpl.label.sourceTag": "सोर्स टैग", + "bg.xrpl.label.deliversAtLeast": "कम से कम पहुँचाता है", + "bg.xrpl.label.issuer": "जारी करने वाला", + "bg.xrpl.label.replacesOffer": "इस ऑफ़र की जगह लेता है", + "bg.xrpl.label.turnsOn": "चालू करता है", + "bg.xrpl.label.turnsOff": "बंद करता है", + "bg.xrpl.label.domain": "डोमेन", + "bg.xrpl.label.transferFee": "ट्रांसफ़र शुल्क", + "bg.xrpl.label.nftMinter": "NFT बनाने वाला", + "bg.xrpl.label.quorum": "ज़रूरी सिग्नेचर", + "bg.xrpl.label.link": "लिंक", + "bg.xrpl.label.royalty": "रॉयल्टी", + "bg.xrpl.label.releaseAfter": "इसके बाद रिलीज़ हो सकता है", + "bg.xrpl.label.cancelAfter": "इसके बाद रद्द हो सकता है", + "bg.xrpl.err.wrongNetwork": "यह ऐप उस XRP Ledger नेटवर्क से अलग नेटवर्क माँग रहा है जिससे वह जुड़ा है। कुछ भी साइन नहीं हुआ।", + "bg.xrpl.err.multisig": "मल्टी-साइनिंग अभी Clip Wallet में उपलब्ध नहीं है।", + "bg.xrpl.err.alreadySigned": "यह ट्रांज़ैक्शन पहले से साइन है, इसलिए Clip Wallet इसे दोबारा साइन नहीं करेगा।", + "bg.xrpl.err.cantRead": "Clip Wallet इस XRP Ledger ट्रांज़ैक्शन का एक हिस्सा ({field}) नहीं पढ़ सकता, इसलिए इसे साइन नहीं कर सकता।", + "bg.xrpl.err.notActivated": "आपका XRP Ledger अकाउंट अभी चालू नहीं है। इसे चालू करने के लिए कम से कम {reserve} XRP पाएँ।", + "bg.xrpl.err.otherKey": "इस अकाउंट की अपनी की बंद है और इसे कोई दूसरी की चलाती है, इसलिए Clip Wallet इसके लिए साइन नहीं कर सकता।", + "bg.xrpl.err.xAddressNetwork": "यह X-एड्रेस किसी दूसरे XRP Ledger नेटवर्क का है।", + "bg.xrpl.err.badAddress": "यह XRP Ledger एड्रेस जैसा नहीं दिखता।", + "bg.xrpl.err.notToken": "यह XRP Ledger का टोकन नहीं है।", + "bg.xrpl.err.tagNeeded": "इस एड्रेस को डेस्टिनेशन टैग चाहिए। पाने वाले से टैग माँगें, या ऐसा X-एड्रेस माँगें जिसमें टैग शामिल हो।", + "bg.xrpl.err.notEnoughXrp": "आपके पास काफ़ी XRP नहीं है। {reserve} XRP आपके अकाउंट में रहना ज़रूरी है, और शुल्क {fee} XRP है।", + "bg.xrpl.err.newAccount": "यह XRP Ledger अकाउंट अभी मौजूद नहीं है। इसे खोलने के लिए कम से कम {reserve} XRP भेजें।", + "bg.xrpl.err.feeOnly": "नेटवर्क शुल्क देने के लिए आपको थोड़ा XRP चाहिए।", + "bg.xrpl.err.noTrustLine": "उन्होंने अभी अपने XRP Ledger अकाउंट में {symbol} नहीं जोड़ा है। पहले उनसे इसे जोड़ने को कहें।", + "bg.xrpl.err.reserveForToken": "टोकन जोड़ने पर {amount} XRP लॉक होता है, साथ में शुल्क भी। पहले थोड़ा XRP जोड़ें।", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "{contract} पर {action} चलाएँ", + "bg.antelope.unknownToken": "{contract} का {symbol} ऐसा टोकन नहीं है जिसे Clip Wallet जानता हो। भेजने से पहले कॉन्ट्रैक्ट जाँच लें।", + "bg.antelope.noRecipient": "इस नेटवर्क पर {to} नाम का कोई अकाउंट नहीं है, इसलिए यह फ़ेल हो जाएगा।", + "bg.antelope.updateAuth": "बदलें कि {account} की {permission} अनुमति कौन चलाता है", + "bg.antelope.deleteAuth": "{account} की {permission} अनुमति मिटाएँ", + "bg.antelope.linkAuth": "{permission} अनुमति को {action} चलाने दें", + "bg.antelope.unlinkAuth": "किसी अनुमति को {action} चलाने से रोकें", + "bg.antelope.authWarning": "इससे बदलता है कि कौन-सी की या अकाउंट {account} को चलाते हैं। अगर वे आपके नहीं हैं, तो वे इसमें से सब कुछ ले सकते हैं।", + "bg.antelope.unlinkWarning": "इससे बदलता है कि आपके अकाउंट के कुछ काम कौन-सी की चला सकती है।", + "bg.antelope.label.alsoSigns": "इनकी भी ज़रूरत है", + "bg.antelope.delayed": "यह भेजने के {seconds} सेकंड बाद ही चलता है, और तब तक रद्द किया जा सकता है।", + "bg.antelope.feeFree": "कोई नहीं (यह नेटवर्क आपके लिए CPU और NET का ख़र्च उठाता है)", + "bg.antelope.feeResources": "टोकन में कोई नहीं: यह आपके अकाउंट का CPU और NET इस्तेमाल करता है", + "bg.antelope.appSends": "{host} (इसे साइन किया हुआ ट्रांज़ैक्शन मिलता है)", + "bg.antelope.err.offline": "{network} से कनेक्ट नहीं हो सका। अपना कनेक्शन जाँचें और दोबारा कोशिश करें।", + "bg.antelope.err.wrongNetwork": "यह रिक्वेस्ट उस Antelope नेटवर्क से अलग नेटवर्क के लिए है जिस पर आप हैं। कुछ भी साइन नहीं हुआ।", + "bg.antelope.err.contextFree": "यह ट्रांज़ैक्शन ऐसी सुविधाएँ इस्तेमाल करता है जिन्हें Clip Wallet साइन नहीं करता (कॉन्टेक्स्ट-फ़्री ऐक्शन या एक्सटेंशन)।", + "bg.antelope.err.noAbi": "Clip Wallet कॉन्ट्रैक्ट {contract} नहीं पढ़ सकता, इसलिए यह रिक्वेस्ट नहीं बना सकता।", + "bg.antelope.err.permission": "इसके लिए आपके अकाउंट की ऐसी अनुमति चाहिए जो यह की नहीं चलाती।", + "bg.antelope.err.otherSigners": "इस ट्रांज़ैक्शन पर दूसरे अकाउंट के सिग्नेचर भी चाहिए, इसलिए Clip Wallet इसे अकेले नहीं भेज सकता।", + "bg.antelope.err.noAccountTestnet": "अभी कोई {network} अकाउंट यह की इस्तेमाल नहीं करता। Antelope नेटवर्क पर अकाउंट चेन पर बनाए गए नाम होते हैं: नेटवर्क के टेस्टनेट टूल से अपनी पब्लिक की के लिए मुफ़्त टेस्ट अकाउंट बनाएँ, फिर वापस आएँ।", + "bg.antelope.err.noAccount": "अभी कोई {network} अकाउंट यह की इस्तेमाल नहीं करता। Antelope नेटवर्क पर अकाउंट ऐसे नाम होते हैं जिन्हें कोई आपकी पब्लिक की के लिए चेन पर बनाता है: कोई एक्सचेंज, अकाउंट वाला कोई दोस्त, या अकाउंट बनाने वाली सर्विस यह कर सकती है।", + "bg.antelope.err.notToken": "यह इस नेटवर्क का टोकन नहीं है।", + "bg.antelope.err.badAccount": "यह सही अकाउंट नाम नहीं है। नाम में ज़्यादा से ज़्यादा 12 कैरेक्टर होते हैं: a–z, 1–5 और डॉट।", + "bg.antelope.err.cpuPowerup": "{network} पर आपके अकाउंट का CPU या NET समय ख़त्म हो गया है। PowerUp से थोड़ा किराए पर लें (24 घंटे के लिए थोड़े EOS लगते हैं), फिर दोबारा कोशिश करें।", + "bg.antelope.err.cpuStake": "{network} पर आपके अकाउंट का CPU या NET समय ख़त्म हो गया है। CPU और NET के लिए थोड़े TLOS स्टेक करें, या PowerUp से किराए पर लें, फिर दोबारा कोशिश करें।", + "bg.antelope.err.cpuFree": "{network} अभी इसके लिए CPU या NET नहीं दे सका। थोड़ा रुककर दोबारा कोशिश करें।", + "bg.antelope.err.ram": "{network} पर आपके अकाउंट में इसके लिए काफ़ी RAM नहीं है। थोड़ी RAM ख़रीदें, फिर दोबारा कोशिश करें।", + "bg.antelope.err.balance": "इसके लिए आपके पास यह टोकन काफ़ी नहीं है।", + "bg.antelope.err.expired": "यह ट्रांज़ैक्शन भेजे जाने से पहले ही एक्सपायर हो गया। दोबारा कोशिश करें।", + "bg.antelope.err.auth": "नेटवर्क के मुताबिक़ यह की उस अकाउंट के लिए साइन नहीं कर सकती। कुछ भी नहीं भेजा गया।", + "bg.antelope.err.noRecipient": "पाने वाले का अकाउंट इस नेटवर्क पर मौजूद नहीं है। कुछ भी नहीं भेजा गया।", + "bg.antelope.err.duplicate": "यही ट्रांज़ैक्शन पहले ही भेजा जा चुका है। ऐक्टिविटी देखें।", + "bg.antelope.err.rejected": "{network} ने यह ट्रांज़ैक्शन अस्वीकार कर दिया। कुछ भी नहीं भेजा गया।", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "{validator} से अपने स्टेकिंग रिवॉर्ड क्लेम करें", + "bg.multiversx.withdrawFrom": "{validator} से अपने अनस्टेक किए गए EGLD निकालें", + "bg.multiversx.restakeRewardsWith": "{validator} के साथ अपने रिवॉर्ड फिर से स्टेक करें", + "bg.multiversx.labelGuardian": "गार्डियन", + "bg.multiversx.setGuardianTitle": "{guardian} को अपने अकाउंट का गार्डियन बनाएं", + "bg.multiversx.setGuardianWarn": "गार्डियन चालू होने के बाद इस अकाउंट के हर ट्रांज़ैक्शन पर उसका सह-हस्ताक्षर ज़रूरी होता है। अगर {guardian} को आपने नहीं चुना है, तो कोई आपको आपके ही अकाउंट से बाहर कर सकता है।", + "bg.multiversx.guardAccountTitle": "अपने अकाउंट का गार्डियन चालू करें", + "bg.multiversx.guardAccountWarn": "अब से इस अकाउंट के हर ट्रांज़ैक्शन पर गार्डियन का सह-हस्ताक्षर ज़रूरी होगा। Clip Wallet यह नहीं दे सकता, इसलिए आप Clip Wallet से और भेज नहीं पाएंगे।", + "bg.multiversx.unguardAccountTitle": "अपने अकाउंट का गार्डियन बंद करें", + "bg.multiversx.changeOwnerTitle": "कॉन्ट्रैक्ट {contract} को {owner} को सौंपें", + "bg.multiversx.changeOwnerWarn": "इससे {owner} कॉन्ट्रैक्ट {contract} का मालिक बन जाता है। नया मालिक इसका कोड बदल सकता है और इसमें रखी चीज़ें ले सकता है। ऐसा तभी करें जब आप सच में इसे सौंपना चाहते हों।", + "bg.multiversx.guardedAccount": "इस अकाउंट का एक गार्डियन है, और Clip Wallet गार्डियन का सह-हस्ताक्षर नहीं ले सकता। कुछ भी नहीं भेजा गया।", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "यह एक अकाउंट ID पर भेजता है, यानी उस तरह का डिपॉज़िट एड्रेस जो एक्सचेंज देते हैं। जांच लें कि यह एक्सचेंज में दिखे एड्रेस से बिल्कुल मेल खाता है।", + "bg.icp.expired": "आपके मंज़ूर करने से पहले ही इस ट्रांसफ़र की समय-सीमा खत्म हो गई। कुछ भी नहीं भेजा गया। फिर से कोशिश करें।", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "पोस्ट-कंडीशन", + "bg.stacks.pcYouSendExactly": "आप ठीक {amount} भेजते हैं", + "bg.stacks.pcYouSendAtMost": "आप ज़्यादा से ज़्यादा {amount} भेजते हैं", + "bg.stacks.pcYouSendAtLeast": "आप कम से कम {amount} भेजते हैं", + "bg.stacks.pcYouSendMoreThan": "आप {amount} से ज़्यादा भेजते हैं", + "bg.stacks.pcYouSendLessThan": "आप {amount} से कम भेजते हैं", + "bg.stacks.pcSendsExactly": "{who} ठीक {amount} भेजता है", + "bg.stacks.pcSendsAtMost": "{who} ज़्यादा से ज़्यादा {amount} भेजता है", + "bg.stacks.pcSendsAtLeast": "{who} कम से कम {amount} भेजता है", + "bg.stacks.pcSendsMoreThan": "{who} {amount} से ज़्यादा भेजता है", + "bg.stacks.pcSendsLessThan": "{who} {amount} से कम भेजता है", + "bg.stacks.pcYouSendNft": "आप {item} भेजते हैं", + "bg.stacks.pcYouKeepNft": "{item} आपके पास रहता है", + "bg.stacks.pcYouMaySendNft": "आप {item} भेज सकते हैं", + "bg.stacks.pcSendsNft": "{who} {item} भेजता है", + "bg.stacks.pcKeepsNft": "{item} {who} के पास रहता है", + "bg.stacks.pcMaySendNft": "{who} {item} भेज सकता है", + "bg.stacks.pcStakingRule": "{who} के लिए एक स्टेकिंग नियम", + "bg.stacks.allowMode": "इससे कॉन्ट्रैक्ट आपका कोई भी एसेट ट्रांसफ़र कर सकता है, सिर्फ़ लिस्ट किए गए एसेट नहीं। इसे सिर्फ़ तभी मंज़ूर करें, जब आपको {host} पर पूरा भरोसा हो।", + "bg.stacks.originatorMode": "सिर्फ़ लिस्ट किए गए ट्रांसफ़र ही आपके अकाउंट से बाहर जा सकते हैं। कॉन्ट्रैक्ट फिर भी दूसरे अकाउंट के एसेट ट्रांसफ़र कर सकता है।", + "bg.stacks.nothingLeaves": "इस ट्रांज़ैक्शन में नेटवर्क शुल्क के अलावा आपका कोई भी एसेट आपके अकाउंट से बाहर नहीं जा सकता।", + "bg.stacks.highFee": "नेटवर्क शुल्क {fee} है, जो असामान्य रूप से ज़्यादा है।", + "bg.stacks.sponsorPays": "{host} का चुना हुआ स्पॉन्सर", + "bg.stacks.deployNamed": "स्मार्ट कॉन्ट्रैक्ट {name} बनाएं", + "bg.stacks.notYourTx": "यह ट्रांज़ैक्शन किसी दूसरे Stacks अकाउंट से साइन होता है, इसलिए Clip Wallet इसे साइन नहीं कर सकता।", + "bg.stacks.multisig": "Clip Wallet अभी शेयर्ड (मल्टी-सिग्नेचर) Stacks अकाउंट के लिए साइन नहीं कर सकता।", + "bg.stacks.memoTooLong": "मेमो बहुत लंबा है। Stacks मेमो में ज़्यादा से ज़्यादा 34 बाइट आ सकते हैं।", + "bg.stacks.feeTooLow": "नेटवर्क शुल्क बहुत कम था, इसलिए नेटवर्क ने इसे स्वीकार नहीं किया। कुछ भी नहीं भेजा गया। दोबारा कोशिश करें।", + "bg.stacks.nonceBusy": "इस अकाउंट का एक और ट्रांज़ैक्शन अभी इंतज़ार में है। उसके पूरा होने तक रुकें, फिर दोबारा कोशिश करें।", + "bg.stacks.otherNetworkTx": "यह ट्रांज़ैक्शन किसी दूसरे Stacks नेटवर्क के लिए है, इसलिए इसे नहीं भेजा गया।", + "bg.stacks.badContractCall": "ऐप का कॉन्ट्रैक्ट कॉल नेटवर्क पर मौजूद कॉन्ट्रैक्ट से मेल नहीं खाता। कुछ भी नहीं भेजा गया।", + "bg.stacks.tooManyPending": "इस अकाउंट के बहुत ज़्यादा ट्रांज़ैक्शन इंतज़ार में हैं। कुछ के पूरा होने तक रुकें, फिर दोबारा कोशिश करें।", + "bg.stacks.signatureRefused": "नेटवर्क ने सिग्नेचर स्वीकार नहीं किया। कुछ भी नहीं भेजा गया।", + "bg.stacks.testAddressOnMainnet": "यह Stacks टेस्ट-नेटवर्क एड्रेस (ST…) है। मेननेट एड्रेस (SP…) इस्तेमाल करें।", + "bg.stacks.mainAddressOnTestnet": "यह Stacks मेननेट एड्रेस (SP…) है। टेस्ट-नेटवर्क एड्रेस (ST…) इस्तेमाल करें।", + "bg.stacks.notAnAddress": "यह Stacks एड्रेस जैसा नहीं लगता।", + "bg.stacks.needStxForFee": "नेटवर्क शुल्क चुकाने के लिए आपको थोड़े STX चाहिए।", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "इसके बाद आपके {symbol} में जो भी बचेगा, वह आपको वापस नहीं, {to} को जाएगा।", + "bg.fuel.leftoverLost": "{amount} यह ट्रांज़ैक्शन कहीं नहीं भेजता, इसलिए यह खो जाएगा।", + "bg.fuel.coinsSpent": "इस ट्रांज़ैक्शन के कुछ कॉइन पहले ही खर्च हो चुके हैं। ऐप से फिर से कोशिश करने को कहें।", + "bg.fuel.feeRose": "इसे तैयार करने के बाद नेटवर्क शुल्क बढ़ गया है। कुछ भी नहीं भेजा गया। फिर से कोशिश करें।", + "bg.fuel.failedOnChain": "यह ट्रांज़ैक्शन Fuel नेटवर्क पर विफल हो गया। सिर्फ़ नेटवर्क शुल्क खर्च हुआ।", + "bg.fuel.tooManyCoins": "इसके लिए एक साथ बहुत सारे छोटे कॉइन चाहिए। पहले कम राशि भेजें।", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "यह ऐप Clip Wallet से उसकी कुंजी या सिग्नेचर किसी स्मार्ट कॉन्ट्रैक्ट के डेटा में डालने को कह रहा है। Clip Wallet अभी इस तरह के ट्रांज़ैक्शन पर साइन नहीं कर सकता।", + "bg.bch.spendsTokens": "इससे आपके वॉलेट से CashTokens भेजे जाएंगे: {tokens}।", + "bg.bch.highFee": "नेटवर्क शुल्क {fee} है, जो असामान्य रूप से ज़्यादा है।", + "bg.bch.othersInputs": "दूसरे मालिकों या कॉन्ट्रैक्ट के {count} कॉइन", + "bg.bch.notCashAddr": "यह Bitcoin Cash एड्रेस जैसा नहीं लगता। इसका CashAddr रूप (bitcoincash:q…) इस्तेमाल करें।", + "bg.bch.testAddressOnMainnet": "यह Bitcoin Cash टेस्ट-नेटवर्क एड्रेस (bchtest:…) है। मेननेट एड्रेस (bitcoincash:…) इस्तेमाल करें।", + "bg.bch.mainAddressOnTestnet": "यह Bitcoin Cash मेननेट एड्रेस (bitcoincash:…) है। टेस्ट-नेटवर्क एड्रेस (bchtest:…) इस्तेमाल करें।", + "bg.bch.tokenSendUnavailable": "CashTokens भेजना अभी उपलब्ध नहीं है।", + "bg.bch.belowDust": "कम से कम {amount} भेजें। Bitcoin Cash पर इससे छोटी रकम नहीं भेजी जा सकती।", + "bg.bch.feeTooLow": "नेटवर्क के लिए शुल्क बहुत कम था। कुछ भी नहीं भेजा गया। दोबारा कोशिश करें।", + "bg.bch.coinsSpent": "इस ट्रांज़ैक्शन के कुछ कॉइन पहले ही खर्च हो चुके थे। कुछ भी नहीं भेजा गया। रीफ़्रेश करें और दोबारा कोशिश करें।", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "नेटवर्क का तय शुल्क", + "bg.evm.flatFee": "हर लेन-देन पर {amount}, विफल होने पर भी", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "{symbol} को एक Chainflip अकाउंट से दूसरे में नहीं भेजा जा सकता। इसे ले जाने के लिए, इसे किसी Ethereum एड्रेस पर रिडीम करें, या दूसरे अकाउंट को Ethereum से फ़ंड करें।", + "bg.chainflip.redeem": "{amount} को Ethereum पर {to} में रिडीम करें", + "bg.chainflip.redeemAll": "अपना सारा {symbol} Ethereum पर {to} में रिडीम करें", + "bg.chainflip.allRedeemable": "आपका सारा {symbol} जो बॉन्ड नहीं है", + "bg.chainflip.label.executor": "इसे कौन पूरा कर सकता है", + "bg.chainflip.anyone": "कोई भी", + "bg.chainflip.redeemSteps": "इंतज़ार की अवधि के बाद, रिडेम्प्शन Ethereum पर पूरा होता है (इसमें ETH गैस लगती है)। अगर समय सीमा से पहले कोई इसे पूरा न करे, तो इसे दोबारा करना होगा।", + "bg.chainflip.redeemWarn": "आपका {symbol} Chainflip छोड़कर Ethereum पर {to} पर जा रहा है। जाँच लें कि यह Ethereum एड्रेस आपके नियंत्रण में है।", + "bg.chainflip.bindRedeem": "रिडेम्प्शन हमेशा के लिए सिर्फ़ {to} पर होने दें", + "bg.chainflip.bindRedeemWarn": "इसे कभी पलटा नहीं जा सकता। यह अकाउंट अब {symbol} सिर्फ़ {to} पर ही रिडीम करेगा। अगर वह Ethereum एड्रेस आपके नियंत्रण में नहीं है, तो आपका {symbol} हमेशा के लिए लॉक हो जाएगा।", + "bg.chainflip.bindExecutor": "हमेशा के लिए सिर्फ़ {to} को आपके रिडेम्प्शन पूरे करने दें", + "bg.chainflip.bindExecutorWarn": "इसे कभी पलटा नहीं जा सकता। इस अकाउंट के {symbol} रिडेम्प्शन Ethereum पर सिर्फ़ {to} पूरे कर पाएगा। अगर वह की खो गई, तो रिडीम किया गया {symbol} क्लेम नहीं किया जा सकेगा।", + "bg.chainflip.moveTo": "{amount} को Chainflip अकाउंट {to} में ले जाएँ", + "bg.chainflip.moveAllTo": "अपना सारा {symbol} Chainflip अकाउंट {to} में ले जाएँ", + "bg.chainflip.otherAccount": "{to} आपके अकाउंट में से नहीं है। वहाँ कुछ भी भेजने से पहले जाँच लें।", + "bg.chainflip.foreignRecipient": "यह {chain} पर {to} को जाएगा। एड्रेस जाँच लें: Clip Wallet इसे वापस नहीं कर सकता।", + "bg.chainflip.registerLp": "Chainflip लिक्विडिटी प्रोवाइडर के रूप में रजिस्टर करें", + "bg.chainflip.deregisterLp": "अपना Chainflip लिक्विडिटी प्रोवाइडर अकाउंट बंद करें", + "bg.chainflip.refundAddress": "अपना {chain} रिफ़ंड एड्रेस {to} पर सेट करें", + "bg.chainflip.depositAddress": "{asset} के लिए डिपॉज़िट एड्रेस पाएँ", + "bg.chainflip.label.boostFee": "Boost शुल्क", + "bg.chainflip.label.refundTo": "रिफ़ंड यहाँ जाएँगे", + "bg.chainflip.withdraw": "{amount} को {chain} पर {to} में निकालें", + "bg.chainflip.swap": "{amount} को {asset} से स्वैप करें", + "bg.chainflip.swapChannel": "{from} को {to} से स्वैप करने के लिए चैनल खोलें", + "bg.chainflip.addBoost": "Boost: {tier} bps पूल में {amount} जोड़ें", + "bg.chainflip.stopBoost": "{tier} bps पूल में {asset} की Boosting बंद करें", + "bg.chainflip.lend": "{amount} उधार दें", + "bg.chainflip.unlend": "उधार दिए गए {amount} वापस लें", + "bg.chainflip.unlendAll": "उधार दिया गया सारा {asset} वापस लें", + "bg.chainflip.borrow": "{amount} उधार लें", + "bg.chainflip.repay": "लोन #{id} का कुछ हिस्सा चुकाएँ", + "bg.chainflip.repayAll": "लोन #{id} पूरा चुकाएँ", + "bg.chainflip.onSubAccount": "सब-अकाउंट #{n} पर: {inner}", + "bg.chainflip.orderFee": "ऑर्डर लगाने या बदलने के लिए इस अकाउंट में कम से कम 1 {symbol} होना ज़रूरी है। इसका ज़्यादातर हिस्सा वापस आ जाता है, लेकिन एक ही ब्लॉक में एक ही पूल के ऑर्डर कई बार बदलने पर हर बार शुल्क बढ़ता है।", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts new file mode 100644 index 00000000..94e72c12 --- /dev/null +++ b/packages/core/src/messages/locales/n87/it.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Italian: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../it.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "Canale IBC {channel}", + "bg.cosmos.ibcLeaves": "Questo invia i token a un'altra chain tramite IBC ({channel}). Controlla che l'indirizzo sia giusto su quella chain.", + "bg.cosmos.unbonding": "I tuoi {symbol} restano bloccati e non rendono nulla finché non termina il periodo di unbonding della rete.", + "bg.cosmos.moveStake": "Sposta {amount} di staking da {from} a {to}", + "bg.cosmos.claimRewardsFrom": "Riscuoti le ricompense di staking da {validator}", + "bg.cosmos.grantAuthz": "Consenti a {grantee} di agire per il tuo account", + "bg.cosmos.authzTakeAll": "Questo consente a {grantee} di spostare i tuoi fondi senza chiedertelo di nuovo, finché non revochi il permesso. Potrebbe prendere tutto.", + "bg.cosmos.authzActs": "Questo consente a {grantee} di agire per il tuo account senza chiedertelo di nuovo, finché non revochi il permesso. Consentilo solo ad app di cui ti fidi.", + "bg.cosmos.revokeAuthz": "Impedisci a {grantee} di agire per il tuo account", + "bg.cosmos.grantFees": "Consenti a {grantee} di pagare commissioni di rete con i tuoi {symbol}", + "bg.cosmos.feegrantUpTo": "Questo consente a {grantee} di spendere fino a {amount} dei tuoi fondi in commissioni di rete, senza chiedertelo di nuovo.", + "bg.cosmos.feegrantAll": "Questo consente a {grantee} di spendere i tuoi {symbol} in commissioni di rete senza limite, senza chiedertelo di nuovo.", + "bg.cosmos.revokeFees": "Impedisci a {grantee} di pagare commissioni di rete con i tuoi fondi", + "bg.cosmos.block": "blocco {height}", + "bg.cosmos.appGetsSigned": "{host} (riceve la transazione firmata)", + "bg.cosmos.wrongPrefix": "Questo indirizzo è di un'altra rete. Gli indirizzi su {network} iniziano con {prefix}.", + "bg.cosmos.noAccount": "Il tuo account non è ancora su {network}. Ricevi prima un po' di {symbol}.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "Questo account TRON è controllato da un'altra chiave, quindi Clip Wallet non può firmare per esso.", + "bg.tron.notOpen": "Il tuo account TRON non è ancora aperto. Ricevi prima un po' di TRX.", + "bg.tron.contractRecipient": "Non è possibile inviare TRX direttamente a uno smart contract.", + "bg.tron.newAccountFee": "Questo indirizzo non è ancora aperto su TRON. Inviargli fondi costa anche {amount} per aprirlo.", + "bg.tron.getsSigned": "{host} (riceve la transazione firmata)", + "bg.tron.energy": "energia", + "bg.tron.bandwidth": "larghezza di banda", + "bg.tron.tronPower": "potere di voto", + "bg.tron.stakeFor": "Metti in staking {amount} per ottenere {resource}", + "bg.tron.days": "{count} giorni", + "bg.tron.cancelUnstaking": "Annulla i prelievi dallo staking in sospeso e rimetti in staking quei TRX", + "bg.tron.lend": "Presta {resource} dei {amount} che hai messo in staking a {to}", + "bg.tron.stopLending": "Riprendi {resource} dei {amount} prestati a {to}", + "bg.tron.lockedHours": "Circa {hours} ore. Non puoi riprenderlo prima.", + "bg.tron.vote": "Vota per {count} Super Representatives", + "bg.tron.votesReplace": "Questo sostituisce tutti i tuoi voti precedenti.", + "bg.tron.claimVoteRewards": "Riscuoti le ricompense per il voto", + "bg.tron.changePermissions": "Cambia chi controlla il tuo account TRON", + "bg.tron.keysThreshold": "{keys} (richiede {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "Paga fino a {amount} perché {to} riceva {get}", + "bg.xrpl.atMost": "Al massimo {amount}", + "bg.xrpl.anyAmount": "Qualsiasi importo", + "bg.xrpl.partialPayment": "È un pagamento parziale: il destinatario potrebbe ricevere molto meno di {amount}. Negozi ed exchange sono stati truffati così.", + "bg.xrpl.xrpToSelf": "Inviare XRP al tuo stesso account non fa nulla, quindi l'XRP Ledger lo rifiuterà.", + "bg.xrpl.frees": "Così si libera la riserva di {amount} che bloccava.", + "bg.xrpl.locksReserve": "{amount} dei tuoi XRP restano bloccati finché questo token è aggiunto", + "bg.xrpl.freezesLine": "Questo congela il token anche per l'altra parte.", + "bg.xrpl.offer.fillOrKill": "Esegui tutto ora, altrimenti annulla", + "bg.xrpl.offer.immediate": "Esegui ora quanto possibile, annulla il resto", + "bg.xrpl.offer.standing": "Resta aperta finché non viene eseguita, scade o la annulli", + "bg.xrpl.cancelOffer": "Annulla un'offerta", + "bg.xrpl.changeSettings": "Cambia le impostazioni del tuo account", + "bg.xrpl.asf.requireDest": "Richiedi un tag di destinazione sui pagamenti che ricevi", + "bg.xrpl.asf.requireAuth": "Approva ogni detentore dei tuoi token", + "bg.xrpl.asf.disallowXrp": "Chiedi di non inviarti XRP", + "bg.xrpl.asf.disableMaster": "Disattiva la chiave propria del tuo account", + "bg.xrpl.asf.accountTxnId": "Tieni traccia dell'id della tua ultima transazione", + "bg.xrpl.asf.noFreeze": "Rinuncia per sempre a congelare i tuoi token", + "bg.xrpl.asf.globalFreeze": "Congela tutti i token che hai emesso", + "bg.xrpl.asf.defaultRipple": "Lascia che i tuoi token passino tra detentori (rippling)", + "bg.xrpl.asf.depositAuth": "Accetta solo pagamenti da account che approvi", + "bg.xrpl.asf.nftMinter": "Consenti a un altro account di creare NFT per te", + "bg.xrpl.asf.noNftOffers": "Blocca le offerte NFT in arrivo", + "bg.xrpl.asf.noChecks": "Blocca gli assegni in arrivo", + "bg.xrpl.asf.noPayChannels": "Blocca i canali di pagamento in arrivo", + "bg.xrpl.asf.noTrustLines": "Blocca le trust line in arrivo", + "bg.xrpl.asf.clawback": "Consentiti di riprendere i token che emetti (clawback)", + "bg.xrpl.asf.lockTokens": "Consenti ai detentori di bloccare i tuoi token in escrow", + "bg.xrpl.disableMasterWarning": "Questo disattiva la chiave propria del tuo account. Dopo, solo un'altra chiave o una lista di firmatari potrà usarlo; se non ce ne sono, resta bloccato per sempre.", + "bg.xrpl.issuerSettingWarning": "Questo cambia il comportamento dei token che emetti. Alcune modifiche non si possono annullare.", + "bg.xrpl.nftMinterWarning": "Questo consente a {who} di creare NFT a nome del tuo account.", + "bg.xrpl.cleared": "(cancellato)", + "bg.xrpl.removeRegularKey": "Rimuovi la chiave aggiuntiva dal tuo account", + "bg.xrpl.regularKeyWarning": "{who} riceve una chiave che può fare qualsiasi cosa con il tuo account, anche svuotarlo.", + "bg.xrpl.removeSigners": "Rimuovi gli account che possono firmare per il tuo", + "bg.xrpl.setSigners": "Consenti a {count} altri account di firmare per il tuo", + "bg.xrpl.signersWarning": "Insieme, questi account possono fare qualsiasi cosa con il tuo account, anche svuotarlo.", + "bg.xrpl.closeAccount": "Chiudi il tuo account XRP Ledger e invia tutto a {to}", + "bg.xrpl.closeWarning": "Questo elimina il tuo account XRP Ledger e invia tutti i tuoi XRP a {to}. La commissione di {fee} viene bruciata.", + "bg.xrpl.mintNft": "Crea un NFT", + "bg.xrpl.taxon": "Taxon {taxon}", + "bg.xrpl.nft.transferable": "Si può vendere o regalare", + "bg.xrpl.nft.notTransferable": "Non si può trasferire", + "bg.xrpl.nft.issuerCanBurn": "Il creatore può bruciarlo", + "bg.xrpl.nft.mutable": "Il suo link potrà essere cambiato", + "bg.xrpl.burnNft": "Brucia un NFT", + "bg.xrpl.giveNft": "Offri il tuo NFT gratis a {to}", + "bg.xrpl.freeForAnyone": "Chiunque può prendere questo NFT senza pagare nulla.", + "bg.xrpl.sellNft": "Offri il tuo NFT per {amount}", + "bg.xrpl.buyNft": "Offri {amount} per un NFT", + "bg.xrpl.offerHolds": "Non viene addebitato nulla finché il proprietario non accetta", + "bg.xrpl.acceptNftOffer": "Accetta un'offerta NFT", + "bg.xrpl.brokered": "Abbina un'offerta di acquisto e una di vendita (vendita intermediata)", + "bg.xrpl.offerUnknown": "Clip Wallet non è riuscito a leggere questa offerta, quindi non può mostrare cosa paghi o ricevi.", + "bg.xrpl.acceptNftSell": "Compra un NFT per {amount}", + "bg.xrpl.acceptNftBuy": "Vendi il tuo NFT per {amount}", + "bg.xrpl.cancelNftOffers": "Annulla {count} offerte NFT", + "bg.xrpl.escrowCreate": "Blocca {amount} in escrow per {to}", + "bg.xrpl.escrowCondition": "Per sbloccarlo serve un segreto (crypto-condition)", + "bg.xrpl.escrowLocked": "Questi fondi restano bloccati finché l'escrow non viene sbloccato o annullato.", + "bg.xrpl.escrowFinish": "Sblocca un escrow", + "bg.xrpl.escrowCancel": "Annulla un escrow", + "bg.xrpl.ammDeposit": "Aggiungi fondi a un pool di liquidità", + "bg.xrpl.ammWithdraw": "Preleva fondi da un pool di liquidità", + "bg.xrpl.yourShare": "La tua quota del pool", + "bg.xrpl.poolAmounts": "Gli importi esatti li decide il pool al momento dell'esecuzione.", + "bg.xrpl.unknownTx": "Approva una transazione XRP Ledger di tipo {type}", + "bg.xrpl.recipientUnchecked": "Clip Wallet non è riuscito a controllare l'account del destinatario.", + "bg.xrpl.newAccountTooSmall": "Questo account XRP Ledger non esiste ancora. Per aprirlo servono almeno {reserve}, quindi l'operazione fallirà.", + "bg.xrpl.opensAccount": "Questo apre un nuovo account XRP Ledger; {reserve} restano bloccati lì come riserva.", + "bg.xrpl.noRecipientAccount": "Questo account XRP Ledger non esiste, quindi non può ricevere token.", + "bg.xrpl.destinationTagNeeded": "Questo destinatario richiede un tag di destinazione e non ne è impostato nessuno. Senza, i fondi potrebbero andare persi.", + "bg.xrpl.disallowsXrp": "Questo account dichiara di non voler ricevere XRP.", + "bg.xrpl.depositAuth": "Questo account accetta solo pagamenti da account che ha approvato, quindi potrebbe fallire.", + "bg.xrpl.noTrustLine": "Il destinatario non ha aggiunto {symbol} al suo account, quindi questo pagamento fallirà.", + "bg.xrpl.notEnoughXrp": "Non hai abbastanza XRP: {reserve} devono restare nel tuo account come riserva.", + "bg.xrpl.highFee": "La commissione di rete è insolitamente alta: {fee}.", + "bg.xrpl.appSends": "{host} (riceve la transazione firmata)", + "bg.xrpl.feeCharged": "La commissione di rete è stata comunque addebitata.", + "bg.xrpl.label.destinationTag": "Tag di destinazione", + "bg.xrpl.label.sourceTag": "Tag di origine", + "bg.xrpl.label.deliversAtLeast": "Consegna almeno", + "bg.xrpl.label.issuer": "Emittente", + "bg.xrpl.label.replacesOffer": "Sostituisce l'offerta", + "bg.xrpl.label.turnsOn": "Attiva", + "bg.xrpl.label.turnsOff": "Disattiva", + "bg.xrpl.label.domain": "Dominio", + "bg.xrpl.label.transferFee": "Commissione di trasferimento", + "bg.xrpl.label.nftMinter": "Creatore di NFT", + "bg.xrpl.label.quorum": "Firme necessarie", + "bg.xrpl.label.link": "Link", + "bg.xrpl.label.royalty": "Royalty", + "bg.xrpl.label.releaseAfter": "Sbloccabile dopo", + "bg.xrpl.label.cancelAfter": "Annullabile dopo", + "bg.xrpl.err.wrongNetwork": "Questa app chiede una rete XRP Ledger diversa da quella a cui è collegata. Non è stato firmato nulla.", + "bg.xrpl.err.multisig": "La firma multipla non è ancora disponibile in Clip Wallet.", + "bg.xrpl.err.alreadySigned": "Questa transazione è già firmata, quindi Clip Wallet non la firmerà di nuovo.", + "bg.xrpl.err.cantRead": "Clip Wallet non riesce a leggere una parte di questa transazione XRP Ledger ({field}), quindi non può firmarla.", + "bg.xrpl.err.notActivated": "Il tuo account XRP Ledger non è ancora attivo. Ricevi almeno {reserve} XRP per attivarlo.", + "bg.xrpl.err.otherKey": "La chiave propria di questo account è disattivata e lo controlla un'altra chiave, quindi Clip Wallet non può firmare per lui.", + "bg.xrpl.err.xAddressNetwork": "Questo indirizzo X è di un'altra rete XRP Ledger.", + "bg.xrpl.err.badAddress": "Non sembra un indirizzo XRP Ledger.", + "bg.xrpl.err.notToken": "Non è un token dell'XRP Ledger.", + "bg.xrpl.err.tagNeeded": "Questo indirizzo richiede un tag di destinazione. Chiedilo al destinatario, oppure chiedi un indirizzo X che lo includa.", + "bg.xrpl.err.notEnoughXrp": "Non hai abbastanza XRP. {reserve} XRP devono restare nel tuo account e la commissione è di {fee} XRP.", + "bg.xrpl.err.newAccount": "Questo account XRP Ledger non esiste ancora. Invia almeno {reserve} XRP per aprirlo.", + "bg.xrpl.err.feeOnly": "Ti serve un po' di XRP per pagare la commissione di rete.", + "bg.xrpl.err.noTrustLine": "Non hanno ancora aggiunto {symbol} al loro account XRP Ledger. Chiedi di aggiungerlo prima.", + "bg.xrpl.err.reserveForToken": "Aggiungere un token blocca {amount} XRP, più la commissione. Aggiungi prima un po' di XRP.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "Chiama {action} su {contract}", + "bg.antelope.unknownToken": "{symbol} di {contract} non è un token che Clip Wallet conosce. Controlla il contratto prima di inviarlo.", + "bg.antelope.noRecipient": "Su questa rete non esiste un account chiamato {to}, quindi l'operazione fallirà.", + "bg.antelope.updateAuth": "Cambia chi controlla il permesso {permission} di {account}", + "bg.antelope.deleteAuth": "Elimina il permesso {permission} di {account}", + "bg.antelope.linkAuth": "Consenti al permesso {permission} di eseguire {action}", + "bg.antelope.unlinkAuth": "Impedisci a un permesso di eseguire {action}", + "bg.antelope.authWarning": "Questo cambia quali chiavi o account controllano {account}. Se non sono tuoi, potrebbero prendere tutto ciò che contiene.", + "bg.antelope.unlinkWarning": "Questo cambia quale chiave può eseguire alcune azioni del tuo account.", + "bg.antelope.label.alsoSigns": "Serve anche", + "bg.antelope.delayed": "Viene eseguito solo {seconds} secondi dopo l'invio e fino ad allora si può annullare.", + "bg.antelope.feeFree": "Nessuna (questa rete copre CPU e NET per te)", + "bg.antelope.feeResources": "Nessuna in token: usa CPU e NET del tuo account", + "bg.antelope.appSends": "{host} (riceve la transazione firmata)", + "bg.antelope.err.offline": "Impossibile raggiungere {network}. Controlla la connessione e riprova.", + "bg.antelope.err.wrongNetwork": "Questa richiesta è per una rete Antelope diversa da quella in uso. Non è stato firmato nulla.", + "bg.antelope.err.contextFree": "Questa transazione usa funzioni che Clip Wallet non firma (azioni senza contesto o estensioni).", + "bg.antelope.err.noAbi": "Clip Wallet non riesce a leggere il contratto {contract}, quindi non può preparare questa richiesta.", + "bg.antelope.err.permission": "Serve un permesso del tuo account che questa chiave non controlla.", + "bg.antelope.err.otherSigners": "Questa transazione richiede anche la firma di altri account, quindi Clip Wallet non può inviarla da solo.", + "bg.antelope.err.noAccountTestnet": "Nessun account {network} usa ancora questa chiave. Sulle reti Antelope gli account sono nomi creati sulla chain: crea un account di prova gratuito per la tua chiave pubblica con lo strumento testnet della rete, poi torna qui.", + "bg.antelope.err.noAccount": "Nessun account {network} usa ancora questa chiave. Sulle reti Antelope gli account sono nomi che qualcuno crea sulla chain per la tua chiave pubblica: un exchange, un amico con un account o un servizio di creazione account può farlo.", + "bg.antelope.err.notToken": "Non è un token di questa rete.", + "bg.antelope.err.badAccount": "Non è un nome di account valido. I nomi hanno fino a 12 caratteri: a–z, 1–5 e punti.", + "bg.antelope.err.cpuPowerup": "Il tuo account ha esaurito il tempo di CPU o NET su {network}. Noleggiane un po' con PowerUp (costa poco EOS per 24 ore), poi riprova.", + "bg.antelope.err.cpuStake": "Il tuo account ha esaurito il tempo di CPU o NET su {network}. Metti un po' di TLOS in staking per CPU e NET, o noleggiali con PowerUp, poi riprova.", + "bg.antelope.err.cpuFree": "{network} non è riuscita a coprire CPU o NET per questo al momento. Attendi un attimo e riprova.", + "bg.antelope.err.ram": "Il tuo account non ha abbastanza RAM su {network} per questo. Compra un po' di RAM, poi riprova.", + "bg.antelope.err.balance": "Non hai abbastanza di questo token per farlo.", + "bg.antelope.err.expired": "Questa transazione è scaduta prima dell'invio. Riprova.", + "bg.antelope.err.auth": "La rete dice che questa chiave non può firmare per quell'account. Non è stato inviato nulla.", + "bg.antelope.err.noRecipient": "L'account del destinatario non esiste su questa rete. Non è stato inviato nulla.", + "bg.antelope.err.duplicate": "Questa stessa transazione è già stata inviata. Controlla le Attività.", + "bg.antelope.err.rejected": "{network} ha rifiutato questa transazione. Non è stato inviato nulla.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "Riscuoti le tue ricompense di staking da {validator}", + "bg.multiversx.withdrawFrom": "Preleva da {validator} i tuoi EGLD tolti dallo staking", + "bg.multiversx.restakeRewardsWith": "Rimetti in staking le tue ricompense con {validator}", + "bg.multiversx.labelGuardian": "Guardiano", + "bg.multiversx.setGuardianTitle": "Rendi {guardian} il guardiano del tuo account", + "bg.multiversx.setGuardianWarn": "Quando un guardiano è attivo, ogni transazione da questo account richiede la sua cofirma. Se non hai scelto tu {guardian}, qualcuno potrebbe bloccarti fuori dal tuo account.", + "bg.multiversx.guardAccountTitle": "Attiva il guardiano del tuo account", + "bg.multiversx.guardAccountWarn": "Da ora in poi ogni transazione da questo account richiede la cofirma del guardiano. Clip Wallet non può fornirla, quindi non potrai più inviare da Clip Wallet.", + "bg.multiversx.unguardAccountTitle": "Disattiva il guardiano del tuo account", + "bg.multiversx.changeOwnerTitle": "Cedi il contratto {contract} a {owner}", + "bg.multiversx.changeOwnerWarn": "Questo rende {owner} proprietario del contratto {contract}. Il nuovo proprietario può cambiarne il codice e prendere ciò che contiene. Fallo solo se vuoi davvero cederlo.", + "bg.multiversx.guardedAccount": "Questo account ha un guardiano e Clip Wallet non può ottenere la sua cofirma. Non è stato inviato nulla.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "Questo invia a un ID account, il tipo di indirizzo di deposito che danno gli exchange. Controlla che corrisponda esattamente a quello mostrato dall'exchange.", + "bg.icp.expired": "Questo trasferimento è scaduto prima che lo approvassi. Non è stato inviato nulla. Riprova.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "Postcondizione", + "bg.stacks.pcYouSendExactly": "Invii esattamente {amount}", + "bg.stacks.pcYouSendAtMost": "Invii al massimo {amount}", + "bg.stacks.pcYouSendAtLeast": "Invii almeno {amount}", + "bg.stacks.pcYouSendMoreThan": "Invii più di {amount}", + "bg.stacks.pcYouSendLessThan": "Invii meno di {amount}", + "bg.stacks.pcSendsExactly": "{who} invia esattamente {amount}", + "bg.stacks.pcSendsAtMost": "{who} invia al massimo {amount}", + "bg.stacks.pcSendsAtLeast": "{who} invia almeno {amount}", + "bg.stacks.pcSendsMoreThan": "{who} invia più di {amount}", + "bg.stacks.pcSendsLessThan": "{who} invia meno di {amount}", + "bg.stacks.pcYouSendNft": "Invii {item}", + "bg.stacks.pcYouKeepNft": "Mantieni {item}", + "bg.stacks.pcYouMaySendNft": "Potresti inviare {item}", + "bg.stacks.pcSendsNft": "{who} invia {item}", + "bg.stacks.pcKeepsNft": "{who} mantiene {item}", + "bg.stacks.pcMaySendNft": "{who} potrebbe inviare {item}", + "bg.stacks.pcStakingRule": "Una regola di staking per {who}", + "bg.stacks.allowMode": "Così il contratto può spostare qualsiasi tuo asset, non solo quelli elencati. Approvalo solo se ti fidi completamente di {host}.", + "bg.stacks.originatorMode": "Solo i trasferimenti elencati possono lasciare il tuo account. Il contratto può comunque spostare gli asset di altri account.", + "bg.stacks.nothingLeaves": "In questa transazione nessuno dei tuoi asset può lasciare il tuo account, a parte la commissione di rete.", + "bg.stacks.highFee": "La commissione di rete è {fee}, un valore insolitamente alto.", + "bg.stacks.sponsorPays": "Uno sponsor scelto da {host}", + "bg.stacks.deployNamed": "Crea lo smart contract {name}", + "bg.stacks.notYourTx": "Questa transazione è firmata da un altro account Stacks, quindi Clip Wallet non può firmarla.", + "bg.stacks.multisig": "Clip Wallet non può ancora firmare per gli account Stacks condivisi (multifirma).", + "bg.stacks.memoTooLong": "Il memo è troppo lungo. Un memo Stacks contiene al massimo 34 byte.", + "bg.stacks.feeTooLow": "La commissione di rete era troppo bassa, quindi la rete non ha accettato la transazione. Non è stato inviato nulla. Riprova.", + "bg.stacks.nonceBusy": "Un'altra transazione di questo account è ancora in attesa. Aspetta che finisca, poi riprova.", + "bg.stacks.otherNetworkTx": "Questa transazione è per un'altra rete Stacks, quindi non è stata inviata.", + "bg.stacks.badContractCall": "La chiamata al contratto dell'app non corrisponde al contratto sulla rete. Non è stato inviato nulla.", + "bg.stacks.tooManyPending": "Questo account ha troppe transazioni in attesa. Aspetta che alcune finiscano, poi riprova.", + "bg.stacks.signatureRefused": "La rete non ha accettato la firma. Non è stato inviato nulla.", + "bg.stacks.testAddressOnMainnet": "Questo è un indirizzo Stacks di una rete di test (ST…). Usa un indirizzo della rete principale (SP…).", + "bg.stacks.mainAddressOnTestnet": "Questo è un indirizzo Stacks della rete principale (SP…). Usa un indirizzo di una rete di test (ST…).", + "bg.stacks.notAnAddress": "Non sembra un indirizzo Stacks.", + "bg.stacks.needStxForFee": "Ti serve un po' di STX per pagare la commissione di rete.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "Tutto ciò che resta dei tuoi {symbol} dopo questa operazione va a {to}, non torna a te.", + "bg.fuel.leftoverLost": "{amount} non viene inviato da nessuna parte da questa transazione e andrebbe perso.", + "bg.fuel.coinsSpent": "Alcune delle monete usate da questa transazione sono già state spese. Chiedi all'app di riprovare.", + "bg.fuel.feeRose": "La commissione di rete è aumentata da quando è stata preparata. Non è stato inviato nulla. Riprova.", + "bg.fuel.failedOnChain": "Questa transazione non è andata a buon fine sulla rete Fuel. È stata pagata solo la commissione di rete.", + "bg.fuel.tooManyCoins": "Servono troppe monete piccole tutte insieme. Invia prima un importo più piccolo.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "Questa app chiede a Clip Wallet di inserire la sua chiave o la sua firma nei dati di uno smart contract. Clip Wallet non può ancora firmare questo tipo di transazione.", + "bg.bch.spendsTokens": "Questa operazione invia CashTokens dal tuo wallet: {tokens}.", + "bg.bch.highFee": "La commissione di rete è {fee}, un valore insolitamente alto.", + "bg.bch.othersInputs": "{count} monete di altri proprietari o contratti", + "bg.bch.notCashAddr": "Non sembra un indirizzo Bitcoin Cash. Usa il formato CashAddr (bitcoincash:q…).", + "bg.bch.testAddressOnMainnet": "Questo è un indirizzo Bitcoin Cash di una rete di test (bchtest:…). Usa un indirizzo della rete principale (bitcoincash:…).", + "bg.bch.mainAddressOnTestnet": "Questo è un indirizzo Bitcoin Cash della rete principale (bitcoincash:…). Usa un indirizzo di una rete di test (bchtest:…).", + "bg.bch.tokenSendUnavailable": "L'invio di CashTokens non è ancora disponibile.", + "bg.bch.belowDust": "Invia almeno {amount}. Su Bitcoin Cash non si possono inviare importi inferiori.", + "bg.bch.feeTooLow": "La commissione era troppo bassa per la rete. Non è stato inviato nulla. Riprova.", + "bg.bch.coinsSpent": "Alcune monete di questa transazione erano già state spese. Non è stato inviato nulla. Aggiorna e riprova.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "Addebito della rete", + "bg.evm.flatFee": "{amount} per transazione, anche se non va a buon fine", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "Non puoi inviare {symbol} da un account Chainflip a un altro. Per spostarlo, riscattalo verso un indirizzo Ethereum oppure finanzia l'altro account da Ethereum.", + "bg.chainflip.redeem": "Riscatta {amount} verso {to} su Ethereum", + "bg.chainflip.redeemAll": "Riscatta tutti i tuoi {symbol} verso {to} su Ethereum", + "bg.chainflip.allRedeemable": "Tutti i tuoi {symbol} non vincolati", + "bg.chainflip.label.executor": "Chi può completarlo", + "bg.chainflip.anyone": "Chiunque", + "bg.chainflip.redeemSteps": "Dopo un periodo di attesa, il riscatto si completa su Ethereum (costa gas in ETH). Se nessuno lo completa prima della scadenza, va rifatto.", + "bg.chainflip.redeemWarn": "I tuoi {symbol} lasciano Chainflip verso {to} su Ethereum. Verifica di controllare questo indirizzo Ethereum.", + "bg.chainflip.bindRedeem": "Consenti riscatti solo verso {to}, per sempre", + "bg.chainflip.bindRedeemWarn": "Non si potrà mai annullare. Questo account riscatterà {symbol} solo verso {to}. Se non controlli quell'indirizzo Ethereum, i tuoi {symbol} restano bloccati per sempre.", + "bg.chainflip.bindExecutor": "Solo {to} potrà completare i tuoi riscatti, per sempre", + "bg.chainflip.bindExecutorWarn": "Non si potrà mai annullare. Solo {to} potrà completare i riscatti di {symbol} di questo account su Ethereum. Se quella chiave va persa, i {symbol} riscattati non si potranno ritirare.", + "bg.chainflip.moveTo": "Sposta {amount} sull'account Chainflip {to}", + "bg.chainflip.moveAllTo": "Sposta tutti i tuoi {symbol} sull'account Chainflip {to}", + "bg.chainflip.otherAccount": "{to} non è uno dei tuoi account. Controllalo prima di spostarci qualcosa.", + "bg.chainflip.foreignRecipient": "Questo va a {to} su {chain}. Controlla l'indirizzo: Clip Wallet non può annullarlo.", + "bg.chainflip.registerLp": "Registrati come fornitore di liquidità Chainflip", + "bg.chainflip.deregisterLp": "Chiudi il tuo account di fornitore di liquidità Chainflip", + "bg.chainflip.refundAddress": "Imposta il tuo indirizzo di rimborso {chain} su {to}", + "bg.chainflip.depositAddress": "Ottieni un indirizzo di deposito per {asset}", + "bg.chainflip.label.boostFee": "Commissione Boost", + "bg.chainflip.label.refundTo": "I rimborsi vanno a", + "bg.chainflip.withdraw": "Preleva {amount} verso {to} su {chain}", + "bg.chainflip.swap": "Scambia {amount} con {asset}", + "bg.chainflip.swapChannel": "Apri un canale per scambiare {from} con {to}", + "bg.chainflip.addBoost": "Boost: aggiungi {amount} al pool da {tier} bps", + "bg.chainflip.stopBoost": "Interrompi il boost di {asset} nel pool da {tier} bps", + "bg.chainflip.lend": "Presta {amount}", + "bg.chainflip.unlend": "Ritira {amount} che hai prestato", + "bg.chainflip.unlendAll": "Ritira tutti i {asset} che hai prestato", + "bg.chainflip.borrow": "Prendi in prestito {amount}", + "bg.chainflip.repay": "Rimborsa parte del prestito #{id}", + "bg.chainflip.repayAll": "Rimborsa per intero il prestito #{id}", + "bg.chainflip.onSubAccount": "Sul sotto-account #{n}: {inner}", + "bg.chainflip.orderFee": "Inserire o modificare un ordine richiede almeno 1 {symbol} in questo account. Quasi tutto torna indietro, ma modificare più volte gli ordini dello stesso pool in un blocco fa salire la commissione ogni volta.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts new file mode 100644 index 00000000..6ec6ec25 --- /dev/null +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Japanese: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../ja.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "IBC チャネル {channel}", + "bg.cosmos.ibcLeaves": "トークンを IBC({channel})で別のチェーンに送ります。そのチェーンで正しいアドレスか確認してください。", + "bg.cosmos.unbonding": "ネットワークのアンボンディング期間が終わるまで、あなたの{symbol}はロックされ、報酬も得られません。", + "bg.cosmos.moveStake": "{amount}のステーキングを{from}から{to}に移す", + "bg.cosmos.claimRewardsFrom": "{validator}からステーキング報酬を受け取る", + "bg.cosmos.grantAuthz": "{grantee}にあなたのアカウントでの操作を許可する", + "bg.cosmos.authzTakeAll": "これにより、{grantee}は許可を取り消すまで、改めて確認することなくあなたの資金を移動できます。すべて持ち出される可能性があります。", + "bg.cosmos.authzActs": "これにより、{grantee}は許可を取り消すまで、改めて確認することなくあなたのアカウントで操作できます。信頼できるアプリにのみ許可してください。", + "bg.cosmos.revokeAuthz": "{grantee}によるあなたのアカウントでの操作をやめさせる", + "bg.cosmos.grantFees": "{grantee}にあなたの{symbol}でネットワーク手数料を支払わせる", + "bg.cosmos.feegrantUpTo": "これにより、{grantee}は改めて確認することなく、あなたの資金から最大{amount}をネットワーク手数料に使えます。", + "bg.cosmos.feegrantAll": "これにより、{grantee}は改めて確認することなく、あなたの{symbol}を上限なくネットワーク手数料に使えます。", + "bg.cosmos.revokeFees": "{grantee}があなたの資金でネットワーク手数料を支払うのをやめさせる", + "bg.cosmos.block": "ブロック {height}", + "bg.cosmos.appGetsSigned": "{host}(署名済みトランザクションを受け取ります)", + "bg.cosmos.wrongPrefix": "このアドレスは別のネットワークのものです。{network}のアドレスは{prefix}で始まります。", + "bg.cosmos.noAccount": "あなたのアカウントはまだ{network}にありません。先に{symbol}を受け取ってください。", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "この TRON アカウントは別の鍵で管理されているため、Clip Wallet は署名できません。", + "bg.tron.notOpen": "TRON アカウントはまだ開設されていません。まず TRX を受け取ってください。", + "bg.tron.contractRecipient": "TRX をスマートコントラクトに直接送ることはできません。", + "bg.tron.newAccountFee": "このアドレスは TRON でまだ開設されていません。送金すると、開設のために {amount} が追加でかかります。", + "bg.tron.getsSigned": "{host}(署名済みの取引を受け取ります)", + "bg.tron.energy": "エネルギー", + "bg.tron.bandwidth": "帯域幅", + "bg.tron.tronPower": "投票権", + "bg.tron.stakeFor": "{amount}をステーキングして{resource}を得る", + "bg.tron.days": "{count}日", + "bg.tron.cancelUnstaking": "保留中のステーキング解除をキャンセルして、その TRX を再びステーキング", + "bg.tron.lend": "ステーキングした{amount}の{resource}を{to}に貸し出す", + "bg.tron.stopLending": "{to}に貸した{amount}の{resource}を取り戻す", + "bg.tron.lockedHours": "約{hours}時間。それまでは取り戻せません。", + "bg.tron.vote": "{count}名の Super Representative に投票", + "bg.tron.votesReplace": "これまでの投票はすべて置き換えられます。", + "bg.tron.claimVoteRewards": "投票報酬を受け取る", + "bg.tron.changePermissions": "TRON アカウントを管理できる人を変更", + "bg.tron.keysThreshold": "{keys}(必要: {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "{to}が{get}を受け取れるよう最大{amount}を支払う", + "bg.xrpl.atMost": "最大{amount}", + "bg.xrpl.anyAmount": "任意の金額", + "bg.xrpl.partialPayment": "これは部分支払いです。受取人が受け取る額は{amount}よりずっと少ない可能性があります。この方法で店舗や取引所がだまされた例があります。", + "bg.xrpl.xrpToSelf": "自分のアカウントにXRPを送っても何も起きないため、XRP Ledgerはこれを拒否します。", + "bg.xrpl.frees": "ロックされていた{amount}の準備金が解放されます。", + "bg.xrpl.locksReserve": "このトークンを追加している間、XRPのうち{amount}がロックされます", + "bg.xrpl.freezesLine": "相手側のトークンも凍結されます。", + "bg.xrpl.offer.fillOrKill": "今すぐ全量を約定、できなければキャンセル", + "bg.xrpl.offer.immediate": "今約定できる分だけ約定し、残りはキャンセル", + "bg.xrpl.offer.standing": "約定・期限切れ・キャンセルまで有効", + "bg.xrpl.cancelOffer": "オファーをキャンセル", + "bg.xrpl.changeSettings": "アカウントの設定を変更", + "bg.xrpl.asf.requireDest": "自分への支払いに宛先タグを必須にする", + "bg.xrpl.asf.requireAuth": "自分のトークンの保有者を一人ずつ承認する", + "bg.xrpl.asf.disallowXrp": "XRPを送らないよう求める", + "bg.xrpl.asf.disableMaster": "アカウント自身の鍵を無効にする", + "bg.xrpl.asf.accountTxnId": "直前のトランザクションIDを記録する", + "bg.xrpl.asf.noFreeze": "トークンを凍結する権限を永久に放棄する", + "bg.xrpl.asf.globalFreeze": "発行したすべてのトークンを凍結する", + "bg.xrpl.asf.defaultRipple": "トークンが保有者間を移動できるようにする(リップリング)", + "bg.xrpl.asf.depositAuth": "承認したアカウントからの支払いだけを受け取る", + "bg.xrpl.asf.nftMinter": "別のアカウントに自分の代わりにNFTを作成させる", + "bg.xrpl.asf.noNftOffers": "届くNFTオファーをブロックする", + "bg.xrpl.asf.noChecks": "届く小切手をブロックする", + "bg.xrpl.asf.noPayChannels": "届くペイメントチャネルをブロックする", + "bg.xrpl.asf.noTrustLines": "届くトラストラインをブロックする", + "bg.xrpl.asf.clawback": "発行したトークンを回収できるようにする(クローバック)", + "bg.xrpl.asf.lockTokens": "保有者がトークンをエスクローでロックできるようにする", + "bg.xrpl.disableMasterWarning": "アカウント自身の鍵が無効になります。以後は別の鍵か署名者リストでしかアカウントを使えず、どちらもなければ永久にロックされます。", + "bg.xrpl.issuerSettingWarning": "発行したトークンの動作が変わります。元に戻せない変更もあります。", + "bg.xrpl.nftMinterWarning": "{who}があなたのアカウント名義でNFTを作成できるようになります。", + "bg.xrpl.cleared": "(消去)", + "bg.xrpl.removeRegularKey": "アカウントから追加の鍵を削除", + "bg.xrpl.regularKeyWarning": "{who}は、アカウントを空にすることも含め、あなたのアカウントで何でもできる鍵を受け取ります。", + "bg.xrpl.removeSigners": "あなたのアカウントに代わって署名できるアカウントを削除", + "bg.xrpl.setSigners": "他の{count}個のアカウントにあなたのアカウントの署名を許可", + "bg.xrpl.signersWarning": "これらのアカウントが揃えば、アカウントを空にすることも含め、あなたのアカウントで何でもできます。", + "bg.xrpl.closeAccount": "XRP Ledgerのアカウントを閉じてすべてを{to}に送金", + "bg.xrpl.closeWarning": "XRP Ledgerのアカウントを削除し、XRPをすべて{to}に送ります。{fee}の手数料は焼却されます。", + "bg.xrpl.mintNft": "NFTを作成", + "bg.xrpl.taxon": "タクソン {taxon}", + "bg.xrpl.nft.transferable": "売却や譲渡ができます", + "bg.xrpl.nft.notTransferable": "譲渡できません", + "bg.xrpl.nft.issuerCanBurn": "作成者がバーンできます", + "bg.xrpl.nft.mutable": "リンクは後から変更できます", + "bg.xrpl.burnNft": "NFTをバーン", + "bg.xrpl.giveNft": "あなたのNFTを{to}に無料で提供", + "bg.xrpl.freeForAnyone": "誰でもこのNFTを無料で受け取れます。", + "bg.xrpl.sellNft": "あなたのNFTを{amount}で出品", + "bg.xrpl.buyNft": "NFTに{amount}をオファー", + "bg.xrpl.offerHolds": "所有者が受け入れるまで何も引き落とされません", + "bg.xrpl.acceptNftOffer": "NFTのオファーを受け入れる", + "bg.xrpl.brokered": "買いオファーと売りオファーを組み合わせます(仲介販売)", + "bg.xrpl.offerUnknown": "Clip Walletはこのオファーを確認できなかったため、支払う額や受け取る額を表示できません。", + "bg.xrpl.acceptNftSell": "{amount}でNFTを購入", + "bg.xrpl.acceptNftBuy": "あなたのNFTを{amount}で売却", + "bg.xrpl.cancelNftOffers": "{count}件のNFTオファーをキャンセル", + "bg.xrpl.escrowCreate": "{to}のために{amount}をエスクローでロック", + "bg.xrpl.escrowCondition": "解放には秘密情報(クリプトコンディション)が必要です", + "bg.xrpl.escrowLocked": "エスクローが解放またはキャンセルされるまで、この資金はロックされたままです。", + "bg.xrpl.escrowFinish": "エスクローを解放", + "bg.xrpl.escrowCancel": "エスクローをキャンセル", + "bg.xrpl.ammDeposit": "流動性プールに資金を追加", + "bg.xrpl.ammWithdraw": "流動性プールから資金を引き出す", + "bg.xrpl.yourShare": "プール内のあなたの持ち分", + "bg.xrpl.poolAmounts": "正確な金額は実行時にプールが決めます。", + "bg.xrpl.unknownTx": "XRP Ledgerの{type}トランザクションを承認", + "bg.xrpl.recipientUnchecked": "Clip Walletは受取人のアカウントを確認できませんでした。", + "bg.xrpl.newAccountTooSmall": "このXRP Ledgerアカウントはまだ存在しません。開設には少なくとも{reserve}が必要なため、これは失敗します。", + "bg.xrpl.opensAccount": "新しいXRP Ledgerアカウントが開設され、そのうち{reserve}は準備金としてロックされます。", + "bg.xrpl.noRecipientAccount": "このXRP Ledgerアカウントは存在しないため、トークンを受け取れません。", + "bg.xrpl.destinationTagNeeded": "この受取人には宛先タグが必要ですが、設定されていません。タグがないと資金を失うおそれがあります。", + "bg.xrpl.disallowsXrp": "このアカウントはXRPを受け取りたくないと表明しています。", + "bg.xrpl.depositAuth": "このアカウントは承認済みのアカウントからの支払いだけを受け取るため、失敗する可能性があります。", + "bg.xrpl.noTrustLine": "受取人はアカウントに{symbol}を追加していないため、この支払いは失敗します。", + "bg.xrpl.notEnoughXrp": "XRPが足りません。{reserve}は準備金としてアカウントに残す必要があります。", + "bg.xrpl.highFee": "ネットワーク手数料が異常に高くなっています: {fee}。", + "bg.xrpl.appSends": "{host}(署名済みトランザクションを受け取ります)", + "bg.xrpl.feeCharged": "ネットワーク手数料はそれでも差し引かれました。", + "bg.xrpl.label.destinationTag": "宛先タグ", + "bg.xrpl.label.sourceTag": "送信元タグ", + "bg.xrpl.label.deliversAtLeast": "最低受取額", + "bg.xrpl.label.issuer": "発行者", + "bg.xrpl.label.replacesOffer": "置き換えるオファー", + "bg.xrpl.label.turnsOn": "有効にする", + "bg.xrpl.label.turnsOff": "無効にする", + "bg.xrpl.label.domain": "ドメイン", + "bg.xrpl.label.transferFee": "送金手数料", + "bg.xrpl.label.nftMinter": "NFT作成者", + "bg.xrpl.label.quorum": "必要な署名数", + "bg.xrpl.label.link": "リンク", + "bg.xrpl.label.royalty": "ロイヤリティ", + "bg.xrpl.label.releaseAfter": "解放可能になる日時", + "bg.xrpl.label.cancelAfter": "キャンセル可能になる日時", + "bg.xrpl.err.wrongNetwork": "このアプリは接続中とは別のXRP Ledgerネットワークを求めています。何も署名されていません。", + "bg.xrpl.err.multisig": "マルチシグはClip Walletではまだ利用できません。", + "bg.xrpl.err.alreadySigned": "このトランザクションは署名済みのため、Clip Walletは再度署名しません。", + "bg.xrpl.err.cantRead": "Clip WalletはこのXRP Ledgerトランザクションの一部({field})を読み取れないため、署名できません。", + "bg.xrpl.err.notActivated": "XRP Ledgerのアカウントはまだ有効化されていません。有効化するには{reserve} XRP以上を受け取ってください。", + "bg.xrpl.err.otherKey": "このアカウント自身の鍵は無効になっていて別の鍵が管理しているため、Clip Walletは署名できません。", + "bg.xrpl.err.xAddressNetwork": "このXアドレスは別のXRP Ledgerネットワーク用です。", + "bg.xrpl.err.badAddress": "XRP Ledgerのアドレスではないようです。", + "bg.xrpl.err.notToken": "XRP Ledgerのトークンではありません。", + "bg.xrpl.err.tagNeeded": "このアドレスには宛先タグが必要です。受取人にタグか、タグを含むXアドレスを尋ねてください。", + "bg.xrpl.err.notEnoughXrp": "XRPが足りません。{reserve} XRPはアカウントに残す必要があり、手数料は{fee} XRPです。", + "bg.xrpl.err.newAccount": "このXRP Ledgerアカウントはまだ存在しません。開設するには{reserve} XRP以上を送ってください。", + "bg.xrpl.err.feeOnly": "ネットワーク手数料の支払いに少しXRPが必要です。", + "bg.xrpl.err.noTrustLine": "相手はまだXRP Ledgerのアカウントに{symbol}を追加していません。先に追加してもらってください。", + "bg.xrpl.err.reserveForToken": "トークンを追加すると{amount} XRPと手数料がロックされます。先にXRPを追加してください。", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "{contract}の{action}を呼び出す", + "bg.antelope.unknownToken": "{contract}の{symbol}はClip Walletが知らないトークンです。送る前にコントラクトを確認してください。", + "bg.antelope.noRecipient": "このネットワークに{to}という名前のアカウントはないため、これは失敗します。", + "bg.antelope.updateAuth": "{account}の{permission}権限を管理する人を変更", + "bg.antelope.deleteAuth": "{account}の{permission}権限を削除", + "bg.antelope.linkAuth": "{permission}権限に{action}の実行を許可", + "bg.antelope.unlinkAuth": "権限による{action}の実行を止める", + "bg.antelope.authWarning": "{account}を管理する鍵やアカウントが変わります。あなたのものでなければ、中身をすべて持ち去られるおそれがあります。", + "bg.antelope.unlinkWarning": "アカウントの一部の操作を実行できる鍵が変わります。", + "bg.antelope.label.alsoSigns": "ほかに必要な署名", + "bg.antelope.delayed": "送信の{seconds}秒後に実行され、それまではキャンセルできます。", + "bg.antelope.feeFree": "なし(CPUとNETはこのネットワークが負担します)", + "bg.antelope.feeResources": "トークンでの支払いなし:アカウントのCPUとNETを使います", + "bg.antelope.appSends": "{host}(署名済みトランザクションを受け取ります)", + "bg.antelope.err.offline": "{network}に接続できませんでした。接続を確認してもう一度お試しください。", + "bg.antelope.err.wrongNetwork": "このリクエストは、今使っているのとは別のAntelopeネットワーク向けです。何も署名されていません。", + "bg.antelope.err.contextFree": "このトランザクションはClip Walletが署名しない機能(コンテキストフリーアクションや拡張)を使っています。", + "bg.antelope.err.noAbi": "Clip Walletはコントラクト{contract}を読み取れないため、このリクエストを作成できません。", + "bg.antelope.err.permission": "この鍵では管理できないアカウントの権限が必要です。", + "bg.antelope.err.otherSigners": "このトランザクションには他のアカウントの署名も必要なため、Clip Walletだけでは送信できません。", + "bg.antelope.err.noAccountTestnet": "この鍵を使う{network}のアカウントはまだありません。Antelopeネットワークのアカウントはチェーン上で作られる名前です。ネットワークのテストネット用ツールで公開鍵の無料テストアカウントを作成してから戻ってください。", + "bg.antelope.err.noAccount": "この鍵を使う{network}のアカウントはまだありません。Antelopeネットワークのアカウントは、誰かがあなたの公開鍵のためにチェーン上で作る名前です。取引所、アカウントを持つ友人、アカウント作成サービスが作成できます。", + "bg.antelope.err.notToken": "このネットワークのトークンではありません。", + "bg.antelope.err.badAccount": "有効なアカウント名ではありません。名前は最大12文字で、a–z、1–5、ドットが使えます。", + "bg.antelope.err.cpuPowerup": "{network}でアカウントのCPUまたはNET時間が足りなくなりました。PowerUpで借りてから(24時間分で少額のEOS)、もう一度お試しください。", + "bg.antelope.err.cpuStake": "{network}でアカウントのCPUまたはNET時間が足りなくなりました。CPUとNET用にTLOSをステーキングするか、PowerUpで借りてから、もう一度お試しください。", + "bg.antelope.err.cpuFree": "{network}は今これに必要なCPUまたはNETを負担できませんでした。少し待ってからもう一度お試しください。", + "bg.antelope.err.ram": "{network}でアカウントのRAMが足りません。RAMを少し購入してから、もう一度お試しください。", + "bg.antelope.err.balance": "このトークンが足りません。", + "bg.antelope.err.expired": "このトランザクションは送信前に期限切れになりました。もう一度お試しください。", + "bg.antelope.err.auth": "ネットワークによると、この鍵はそのアカウントの代わりに署名できません。何も送信されていません。", + "bg.antelope.err.noRecipient": "受取人のアカウントはこのネットワークにありません。何も送信されていません。", + "bg.antelope.err.duplicate": "まったく同じトランザクションがすでに送信されています。アクティビティを確認してください。", + "bg.antelope.err.rejected": "{network}がこのトランザクションを拒否しました。何も送信されていません。", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "{validator}からステーキング報酬を受け取る", + "bg.multiversx.withdrawFrom": "{validator}からステーキング解除済みのEGLDを引き出す", + "bg.multiversx.restakeRewardsWith": "{validator}で報酬を再ステーキング", + "bg.multiversx.labelGuardian": "ガーディアン", + "bg.multiversx.setGuardianTitle": "{guardian}をアカウントのガーディアンにする", + "bg.multiversx.setGuardianWarn": "ガーディアンが有効になると、このアカウントからのすべての取引にその共同署名が必要になります。{guardian}を自分で選んでいない場合、誰かにアカウントから締め出されるおそれがあります。", + "bg.multiversx.guardAccountTitle": "アカウントのガーディアンをオンにする", + "bg.multiversx.guardAccountWarn": "今後、このアカウントからのすべての取引にガーディアンの共同署名が必要になります。Clip Walletはそれを用意できないため、Clip Walletからは送信できなくなります。", + "bg.multiversx.unguardAccountTitle": "アカウントのガーディアンをオフにする", + "bg.multiversx.changeOwnerTitle": "コントラクト{contract}を{owner}に譲渡", + "bg.multiversx.changeOwnerWarn": "これにより{owner}がコントラクト{contract}の所有者になります。新しい所有者はコードを変更でき、中の資産を持ち出せます。本当に譲渡するつもりのときだけ実行してください。", + "bg.multiversx.guardedAccount": "このアカウントにはガーディアンが設定されていて、Clip Walletはガーディアンの共同署名を取得できません。何も送信されていません。", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "これはアカウントIDあての送金です。取引所が入金先として示す形式のアドレスです。取引所に表示されているものと完全に一致するか確認してください。", + "bg.icp.expired": "この送金は承認前に有効期限が切れました。何も送信されていません。もう一度お試しください。", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "ポストコンディション", + "bg.stacks.pcYouSendExactly": "あなたはちょうど{amount}を送信します", + "bg.stacks.pcYouSendAtMost": "あなたは最大{amount}を送信します", + "bg.stacks.pcYouSendAtLeast": "あなたは最低{amount}を送信します", + "bg.stacks.pcYouSendMoreThan": "あなたは{amount}を超える額を送信します", + "bg.stacks.pcYouSendLessThan": "あなたは{amount}未満の額を送信します", + "bg.stacks.pcSendsExactly": "{who}はちょうど{amount}を送信します", + "bg.stacks.pcSendsAtMost": "{who}は最大{amount}を送信します", + "bg.stacks.pcSendsAtLeast": "{who}は最低{amount}を送信します", + "bg.stacks.pcSendsMoreThan": "{who}は{amount}を超える額を送信します", + "bg.stacks.pcSendsLessThan": "{who}は{amount}未満の額を送信します", + "bg.stacks.pcYouSendNft": "あなたは{item}を送信します", + "bg.stacks.pcYouKeepNft": "あなたは{item}を保持します", + "bg.stacks.pcYouMaySendNft": "あなたは{item}を送信する可能性があります", + "bg.stacks.pcSendsNft": "{who}は{item}を送信します", + "bg.stacks.pcKeepsNft": "{who}は{item}を保持します", + "bg.stacks.pcMaySendNft": "{who}は{item}を送信する可能性があります", + "bg.stacks.pcStakingRule": "{who}のステーキングルール", + "bg.stacks.allowMode": "これにより、コントラクトはリストにあるものだけでなく、あなたのどの資産でも動かせるようになります。{host}を完全に信頼できる場合のみ承認してください。", + "bg.stacks.originatorMode": "あなたのアカウントから出ていけるのは、リストにある送金だけです。ただし、コントラクトは他のアカウントの資産を動かす可能性があります。", + "bg.stacks.nothingLeaves": "この取引では、ネットワーク手数料を除き、あなたの資産がアカウントから出ていくことはありません。", + "bg.stacks.highFee": "ネットワーク手数料は{fee}で、異常に高額です。", + "bg.stacks.sponsorPays": "{host}が選んだスポンサー", + "bg.stacks.deployNamed": "スマートコントラクト{name}を作成", + "bg.stacks.notYourTx": "この取引は別の Stacks アカウントが署名するものなので、Clip Wallet では署名できません。", + "bg.stacks.multisig": "Clip Wallet は、共有(マルチシグ)Stacks アカウントでの署名にはまだ対応していません。", + "bg.stacks.memoTooLong": "メモが長すぎます。Stacks のメモは最大34バイトです。", + "bg.stacks.feeTooLow": "ネットワーク手数料が低すぎたため、ネットワークに受け付けられませんでした。何も送信されていません。もう一度お試しください。", + "bg.stacks.nonceBusy": "このアカウントの別の取引がまだ処理待ちです。完了するまで待ってから、もう一度お試しください。", + "bg.stacks.otherNetworkTx": "この取引は別の Stacks ネットワーク用のため、送信されませんでした。", + "bg.stacks.badContractCall": "アプリのコントラクト呼び出しが、ネットワーク上のコントラクトと一致しません。何も送信されていません。", + "bg.stacks.tooManyPending": "このアカウントには処理待ちの取引が多すぎます。いくつか完了するまで待ってから、もう一度お試しください。", + "bg.stacks.signatureRefused": "ネットワークが署名を受け付けませんでした。何も送信されていません。", + "bg.stacks.testAddressOnMainnet": "これは Stacks のテストネットワークのアドレス(ST…)です。メインネットのアドレス(SP…)を使用してください。", + "bg.stacks.mainAddressOnTestnet": "これは Stacks のメインネットのアドレス(SP…)です。テストネットワークのアドレス(ST…)を使用してください。", + "bg.stacks.notAnAddress": "Stacks のアドレスではないようです。", + "bg.stacks.needStxForFee": "ネットワーク手数料を支払うには、少額の STX が必要です。", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "この後に残るあなたの{symbol}はすべて、あなたではなく{to}に送られます。", + "bg.fuel.leftoverLost": "{amount}はこの取引でどこにも送られず、失われます。", + "bg.fuel.coinsSpent": "この取引が使うコインの一部はすでに使われています。アプリにもう一度試すよう依頼してください。", + "bg.fuel.feeRose": "準備した後にネットワーク手数料が上がりました。何も送信されていません。もう一度お試しください。", + "bg.fuel.failedOnChain": "この取引は Fuel ネットワークで失敗しました。ネットワーク手数料のみが支払われました。", + "bg.fuel.tooManyCoins": "一度に小さなコインが多すぎます。先に少ない金額を送ってください。", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "このアプリは、Clip Wallet の鍵または署名をスマートコントラクトのデータに入れるよう求めています。Clip Wallet はこの種類の取引への署名にはまだ対応していません。", + "bg.bch.spendsTokens": "ウォレットから CashTokens を送信します:{tokens}。", + "bg.bch.highFee": "ネットワーク手数料は{fee}で、異常に高額です。", + "bg.bch.othersInputs": "他の所有者またはコントラクトのコイン {count} 個", + "bg.bch.notCashAddr": "Bitcoin Cash のアドレスではないようです。CashAddr 形式(bitcoincash:q…)を使用してください。", + "bg.bch.testAddressOnMainnet": "これは Bitcoin Cash のテストネットワークのアドレス(bchtest:…)です。メインネットのアドレス(bitcoincash:…)を使用してください。", + "bg.bch.mainAddressOnTestnet": "これは Bitcoin Cash のメインネットのアドレス(bitcoincash:…)です。テストネットワークのアドレス(bchtest:…)を使用してください。", + "bg.bch.tokenSendUnavailable": "CashTokens の送信にはまだ対応していません。", + "bg.bch.belowDust": "{amount} 以上を送信してください。Bitcoin Cash ではこれより少ない金額は送信できません。", + "bg.bch.feeTooLow": "手数料がネットワークの基準より低すぎました。何も送信されていません。もう一度お試しください。", + "bg.bch.coinsSpent": "この取引の一部のコインはすでに使用済みでした。何も送信されていません。更新してから、もう一度お試しください。", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "ネットワークの定額料金", + "bg.evm.flatFee": "1件の取引ごとに{amount}(失敗しても請求)", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "{symbol} は Chainflip アカウント間で送ることはできません。移すには、Ethereum アドレスへ償還するか、Ethereum から相手のアカウントに入金してください。", + "bg.chainflip.redeem": "{amount} を Ethereum の {to} へ償還", + "bg.chainflip.redeemAll": "すべての {symbol} を Ethereum の {to} へ償還", + "bg.chainflip.allRedeemable": "ボンドされていないすべての {symbol}", + "bg.chainflip.label.executor": "完了できる人", + "bg.chainflip.anyone": "誰でも", + "bg.chainflip.redeemSteps": "待機期間の後、償還は Ethereum 上で完了します(ETH のガス代がかかります)。期限までに誰も完了しなければ、やり直しになります。", + "bg.chainflip.redeemWarn": "{symbol} は Chainflip から Ethereum の {to} へ移ります。この Ethereum アドレスを自分で管理していることを確認してください。", + "bg.chainflip.bindRedeem": "償還先を {to} だけに永久に固定", + "bg.chainflip.bindRedeemWarn": "これは二度と取り消せません。このアカウントは今後 {symbol} を {to} にしか償還できません。その Ethereum アドレスを管理していない場合、{symbol} は永久にロックされます。", + "bg.chainflip.bindExecutor": "償還を完了できるのを {to} だけに永久に固定", + "bg.chainflip.bindExecutorWarn": "これは二度と取り消せません。このアカウントの {symbol} の償還を Ethereum 上で完了できるのは {to} だけになります。その鍵を失うと、償還した {symbol} を受け取れません。", + "bg.chainflip.moveTo": "{amount} を Chainflip アカウント {to} へ移動", + "bg.chainflip.moveAllTo": "すべての {symbol} を Chainflip アカウント {to} へ移動", + "bg.chainflip.otherAccount": "{to} はあなたのアカウントではありません。移動する前に確認してください。", + "bg.chainflip.foreignRecipient": "これは {chain} の {to} へ送られます。アドレスを確認してください。Clip Wallet では取り消せません。", + "bg.chainflip.registerLp": "Chainflip の流動性プロバイダーとして登録", + "bg.chainflip.deregisterLp": "Chainflip の流動性プロバイダーアカウントを閉じる", + "bg.chainflip.refundAddress": "{chain} の返金先アドレスを {to} に設定", + "bg.chainflip.depositAddress": "{asset} の入金アドレスを取得", + "bg.chainflip.label.boostFee": "Boost 手数料", + "bg.chainflip.label.refundTo": "返金先", + "bg.chainflip.withdraw": "{amount} を {chain} の {to} へ出金", + "bg.chainflip.swap": "{amount} を {asset} にスワップ", + "bg.chainflip.swapChannel": "{from} を {to} にスワップするチャネルを開く", + "bg.chainflip.addBoost": "Boost:{tier} bps プールに {amount} を追加", + "bg.chainflip.stopBoost": "{tier} bps プールでの {asset} の Boost を停止", + "bg.chainflip.lend": "{amount} を貸し出す", + "bg.chainflip.unlend": "貸し出した {amount} を引き出す", + "bg.chainflip.unlendAll": "貸し出したすべての {asset} を引き出す", + "bg.chainflip.borrow": "{amount} を借りる", + "bg.chainflip.repay": "ローン #{id} の一部を返済", + "bg.chainflip.repayAll": "ローン #{id} を全額返済", + "bg.chainflip.onSubAccount": "サブアカウント #{n}:{inner}", + "bg.chainflip.orderFee": "注文の作成や変更には、このアカウントに少なくとも 1 {symbol} が必要です。ほとんどは戻ってきますが、同じプールの注文を 1 ブロック内で何度も変更すると、そのたびに手数料が上がります。", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts new file mode 100644 index 00000000..5ace0aaf --- /dev/null +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Korean: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../ko.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "IBC 채널 {channel}", + "bg.cosmos.ibcLeaves": "토큰을 IBC({channel})로 다른 체인에 보내요. 그 체인에 맞는 주소인지 확인하세요.", + "bg.cosmos.unbonding": "네트워크의 언본딩 기간이 끝날 때까지 내 {symbol}은(는) 잠겨 있고 보상도 받지 못해요.", + "bg.cosmos.moveStake": "{amount} 스테이킹을 {from}에서 {to}(으)로 옮기기", + "bg.cosmos.claimRewardsFrom": "{validator}에서 스테이킹 보상 받기", + "bg.cosmos.grantAuthz": "{grantee}이(가) 내 계정으로 작업하도록 허용", + "bg.cosmos.authzTakeAll": "권한을 회수할 때까지 {grantee}이(가) 다시 묻지 않고 내 자금을 옮길 수 있게 돼요. 전부 가져갈 수도 있어요.", + "bg.cosmos.authzActs": "권한을 회수할 때까지 {grantee}이(가) 다시 묻지 않고 내 계정으로 작업할 수 있게 돼요. 신뢰하는 앱에만 허용하세요.", + "bg.cosmos.revokeAuthz": "{grantee}이(가) 내 계정으로 작업하지 못하게 하기", + "bg.cosmos.grantFees": "{grantee}이(가) 내 {symbol}(으)로 네트워크 수수료를 내도록 허용", + "bg.cosmos.feegrantUpTo": "{grantee}이(가) 다시 묻지 않고 내 자금에서 최대 {amount}을(를) 네트워크 수수료로 쓸 수 있게 돼요.", + "bg.cosmos.feegrantAll": "{grantee}이(가) 다시 묻지 않고 내 {symbol}을(를) 한도 없이 네트워크 수수료로 쓸 수 있게 돼요.", + "bg.cosmos.revokeFees": "{grantee}이(가) 내 자금으로 네트워크 수수료를 내지 못하게 하기", + "bg.cosmos.block": "블록 {height}", + "bg.cosmos.appGetsSigned": "{host} (서명된 트랜잭션을 받아요)", + "bg.cosmos.wrongPrefix": "다른 네트워크의 주소예요. {network} 주소는 {prefix}(으)로 시작해요.", + "bg.cosmos.noAccount": "내 계정이 아직 {network}에 없어요. 먼저 {symbol}을(를) 받으세요.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "이 TRON 계정은 다른 키가 관리하고 있어서 Clip Wallet이 서명할 수 없어요.", + "bg.tron.notOpen": "TRON 계정이 아직 열리지 않았어요. 먼저 TRX를 받아 주세요.", + "bg.tron.contractRecipient": "TRX는 스마트 컨트랙트로 바로 보낼 수 없어요.", + "bg.tron.newAccountFee": "이 주소는 아직 TRON에서 열리지 않았어요. 여기로 보내면 여는 데 {amount}이(가) 추가로 들어요.", + "bg.tron.getsSigned": "{host} (서명된 트랜잭션을 받아요)", + "bg.tron.energy": "에너지", + "bg.tron.bandwidth": "대역폭", + "bg.tron.tronPower": "투표권", + "bg.tron.stakeFor": "{resource}을(를) 위해 {amount} 스테이킹", + "bg.tron.days": "{count}일", + "bg.tron.cancelUnstaking": "대기 중인 스테이킹 해제를 취소하고 그 TRX를 다시 스테이킹", + "bg.tron.lend": "스테이킹한 {amount}의 {resource}을(를) {to}에게 빌려주기", + "bg.tron.stopLending": "{to}에게 빌려준 {amount}의 {resource} 회수", + "bg.tron.lockedHours": "약 {hours}시간이에요. 그 전에는 되돌릴 수 없어요.", + "bg.tron.vote": "Super Representative {count}명에게 투표", + "bg.tron.votesReplace": "이전 투표가 모두 대체돼요.", + "bg.tron.claimVoteRewards": "투표 보상 받기", + "bg.tron.changePermissions": "TRON 계정을 관리하는 사람 변경", + "bg.tron.keysThreshold": "{keys} ({threshold} 필요)", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "{to}에게 {get}이 가도록 최대 {amount} 지불", + "bg.xrpl.atMost": "최대 {amount}", + "bg.xrpl.anyAmount": "금액 제한 없음", + "bg.xrpl.partialPayment": "부분 결제예요. 받는 사람이 {amount}보다 훨씬 적게 받을 수 있어요. 상점과 거래소가 이런 식으로 속은 적이 있어요.", + "bg.xrpl.xrpToSelf": "내 계정으로 XRP를 보내면 아무 일도 일어나지 않아서 XRP Ledger가 거부해요.", + "bg.xrpl.frees": "잠겨 있던 {amount} 준비금이 풀려요.", + "bg.xrpl.locksReserve": "이 토큰이 추가되어 있는 동안 내 XRP 중 {amount}이 잠겨요", + "bg.xrpl.freezesLine": "상대방 쪽 토큰도 동결돼요.", + "bg.xrpl.offer.fillOrKill": "지금 전부 체결, 안 되면 취소", + "bg.xrpl.offer.immediate": "지금 가능한 만큼 체결하고 나머지는 취소", + "bg.xrpl.offer.standing": "체결되거나 만료되거나 취소할 때까지 유지", + "bg.xrpl.cancelOffer": "오퍼 취소", + "bg.xrpl.changeSettings": "내 계정 설정 변경", + "bg.xrpl.asf.requireDest": "나에게 오는 결제에 도착 태그 필수", + "bg.xrpl.asf.requireAuth": "내 토큰 보유자를 한 명씩 승인", + "bg.xrpl.asf.disallowXrp": "XRP를 보내지 말라고 요청", + "bg.xrpl.asf.disableMaster": "내 계정 자체 키 끄기", + "bg.xrpl.asf.accountTxnId": "마지막 트랜잭션 ID 기록", + "bg.xrpl.asf.noFreeze": "토큰 동결 권한을 영구히 포기", + "bg.xrpl.asf.globalFreeze": "내가 발행한 모든 토큰 동결", + "bg.xrpl.asf.defaultRipple": "내 토큰이 보유자 사이를 오가게 허용(리플링)", + "bg.xrpl.asf.depositAuth": "내가 승인한 계정의 결제만 받기", + "bg.xrpl.asf.nftMinter": "다른 계정이 나 대신 NFT를 만들도록 허용", + "bg.xrpl.asf.noNftOffers": "들어오는 NFT 오퍼 차단", + "bg.xrpl.asf.noChecks": "들어오는 수표 차단", + "bg.xrpl.asf.noPayChannels": "들어오는 결제 채널 차단", + "bg.xrpl.asf.noTrustLines": "들어오는 트러스트 라인 차단", + "bg.xrpl.asf.clawback": "내가 발행한 토큰을 회수할 수 있게 허용(클로백)", + "bg.xrpl.asf.lockTokens": "보유자가 내 토큰을 에스크로에 잠글 수 있게 허용", + "bg.xrpl.disableMasterWarning": "내 계정 자체 키가 꺼져요. 이후에는 다른 키나 서명자 목록만 계정을 쓸 수 있고, 둘 다 없으면 계정이 영구히 잠겨요.", + "bg.xrpl.issuerSettingWarning": "내가 발행한 토큰의 동작이 바뀌어요. 일부는 되돌릴 수 없어요.", + "bg.xrpl.nftMinterWarning": "{who}가 내 계정 이름으로 NFT를 만들 수 있게 돼요.", + "bg.xrpl.cleared": "(지움)", + "bg.xrpl.removeRegularKey": "내 계정에서 추가 키 제거", + "bg.xrpl.regularKeyWarning": "{who}가 내 계정으로 무엇이든 할 수 있는 키를 받게 돼요. 계정을 비우는 것도 포함이에요.", + "bg.xrpl.removeSigners": "내 계정 대신 서명할 수 있는 계정 제거", + "bg.xrpl.setSigners": "다른 계정 {count}개가 내 계정 대신 서명하도록 허용", + "bg.xrpl.signersWarning": "이 계정들이 함께라면 내 계정으로 무엇이든 할 수 있어요. 계정을 비우는 것도 포함이에요.", + "bg.xrpl.closeAccount": "내 XRP Ledger 계정을 닫고 전부 {to}에게 보내기", + "bg.xrpl.closeWarning": "내 XRP Ledger 계정이 삭제되고 XRP 전부가 {to}에게 가요. {fee} 수수료는 소각돼요.", + "bg.xrpl.mintNft": "NFT 만들기", + "bg.xrpl.taxon": "택손 {taxon}", + "bg.xrpl.nft.transferable": "판매하거나 선물할 수 있어요", + "bg.xrpl.nft.notTransferable": "다른 사람에게 넘길 수 없어요", + "bg.xrpl.nft.issuerCanBurn": "만든 사람이 소각할 수 있어요", + "bg.xrpl.nft.mutable": "링크를 나중에 바꿀 수 있어요", + "bg.xrpl.burnNft": "NFT 소각", + "bg.xrpl.giveNft": "내 NFT를 {to}에게 무료로 제안", + "bg.xrpl.freeForAnyone": "누구나 이 NFT를 공짜로 가져갈 수 있어요.", + "bg.xrpl.sellNft": "내 NFT를 {amount}에 제안", + "bg.xrpl.buyNft": "NFT에 {amount} 제안", + "bg.xrpl.offerHolds": "소유자가 수락할 때까지 아무것도 빠져나가지 않아요", + "bg.xrpl.acceptNftOffer": "NFT 오퍼 수락", + "bg.xrpl.brokered": "구매 오퍼와 판매 오퍼를 연결해요(중개 판매)", + "bg.xrpl.offerUnknown": "Clip Wallet이 이 오퍼를 조회하지 못해서 내가 내거나 받는 금액을 보여줄 수 없어요.", + "bg.xrpl.acceptNftSell": "{amount}에 NFT 구매", + "bg.xrpl.acceptNftBuy": "내 NFT를 {amount}에 판매", + "bg.xrpl.cancelNftOffers": "NFT 오퍼 {count}개 취소", + "bg.xrpl.escrowCreate": "{to}를 위해 {amount}을 에스크로에 잠그기", + "bg.xrpl.escrowCondition": "풀려면 비밀 값이 필요해요(크립토 컨디션)", + "bg.xrpl.escrowLocked": "에스크로가 풀리거나 취소될 때까지 이 자금은 잠겨 있어요.", + "bg.xrpl.escrowFinish": "에스크로 풀기", + "bg.xrpl.escrowCancel": "에스크로 취소", + "bg.xrpl.ammDeposit": "유동성 풀에 자금 추가", + "bg.xrpl.ammWithdraw": "유동성 풀에서 자금 인출", + "bg.xrpl.yourShare": "풀에서 내 지분", + "bg.xrpl.poolAmounts": "정확한 금액은 실행될 때 풀이 정해요.", + "bg.xrpl.unknownTx": "XRP Ledger {type} 트랜잭션 승인", + "bg.xrpl.recipientUnchecked": "Clip Wallet이 받는 사람의 계정을 확인하지 못했어요.", + "bg.xrpl.newAccountTooSmall": "이 XRP Ledger 계정은 아직 없어요. 계정을 열려면 최소 {reserve}이 필요해서 실패할 거예요.", + "bg.xrpl.opensAccount": "새 XRP Ledger 계정이 열리고, 그중 {reserve}은 준비금으로 그 계정에 잠겨요.", + "bg.xrpl.noRecipientAccount": "이 XRP Ledger 계정은 없어서 토큰을 받을 수 없어요.", + "bg.xrpl.destinationTagNeeded": "이 받는 사람은 도착 태그가 필요한데 설정되지 않았어요. 태그가 없으면 자금을 잃을 수 있어요.", + "bg.xrpl.disallowsXrp": "이 계정은 XRP를 받고 싶지 않다고 밝혔어요.", + "bg.xrpl.depositAuth": "이 계정은 승인한 계정의 결제만 받아서 실패할 수 있어요.", + "bg.xrpl.noTrustLine": "받는 사람이 계정에 {symbol}을 추가하지 않아서 이 결제는 실패할 거예요.", + "bg.xrpl.notEnoughXrp": "XRP가 부족해요. {reserve}은 준비금으로 계정에 남아 있어야 해요.", + "bg.xrpl.highFee": "네트워크 수수료가 평소보다 훨씬 높아요: {fee}.", + "bg.xrpl.appSends": "{host}(서명된 트랜잭션을 받아요)", + "bg.xrpl.feeCharged": "네트워크 수수료는 그래도 청구됐어요.", + "bg.xrpl.label.destinationTag": "도착 태그", + "bg.xrpl.label.sourceTag": "출발 태그", + "bg.xrpl.label.deliversAtLeast": "최소 전달액", + "bg.xrpl.label.issuer": "발행자", + "bg.xrpl.label.replacesOffer": "대체하는 오퍼", + "bg.xrpl.label.turnsOn": "켜기", + "bg.xrpl.label.turnsOff": "끄기", + "bg.xrpl.label.domain": "도메인", + "bg.xrpl.label.transferFee": "전송 수수료", + "bg.xrpl.label.nftMinter": "NFT 발행자", + "bg.xrpl.label.quorum": "필요한 서명 수", + "bg.xrpl.label.link": "링크", + "bg.xrpl.label.royalty": "로열티", + "bg.xrpl.label.releaseAfter": "이후 풀 수 있음", + "bg.xrpl.label.cancelAfter": "이후 취소할 수 있음", + "bg.xrpl.err.wrongNetwork": "이 앱이 연결된 것과 다른 XRP Ledger 네트워크를 요청하고 있어요. 아무것도 서명하지 않았어요.", + "bg.xrpl.err.multisig": "다중 서명은 Clip Wallet에서 아직 지원하지 않아요.", + "bg.xrpl.err.alreadySigned": "이미 서명된 트랜잭션이라서 Clip Wallet이 다시 서명하지 않아요.", + "bg.xrpl.err.cantRead": "Clip Wallet이 이 XRP Ledger 트랜잭션의 일부({field})를 읽을 수 없어서 서명할 수 없어요.", + "bg.xrpl.err.notActivated": "내 XRP Ledger 계정이 아직 활성화되지 않았어요. 활성화하려면 최소 {reserve} XRP를 받으세요.", + "bg.xrpl.err.otherKey": "이 계정의 자체 키가 꺼져 있고 다른 키가 관리하고 있어서 Clip Wallet이 서명할 수 없어요.", + "bg.xrpl.err.xAddressNetwork": "이 X 주소는 다른 XRP Ledger 네트워크용이에요.", + "bg.xrpl.err.badAddress": "XRP Ledger 주소가 아닌 것 같아요.", + "bg.xrpl.err.notToken": "XRP Ledger 토큰이 아니에요.", + "bg.xrpl.err.tagNeeded": "이 주소에는 도착 태그가 필요해요. 받는 사람에게 태그나, 태그가 포함된 X 주소를 요청하세요.", + "bg.xrpl.err.notEnoughXrp": "XRP가 부족해요. {reserve} XRP는 계정에 남아 있어야 하고, 수수료는 {fee} XRP예요.", + "bg.xrpl.err.newAccount": "이 XRP Ledger 계정은 아직 없어요. 계정을 열려면 최소 {reserve} XRP를 보내세요.", + "bg.xrpl.err.feeOnly": "네트워크 수수료를 낼 XRP가 조금 필요해요.", + "bg.xrpl.err.noTrustLine": "받는 사람이 아직 XRP Ledger 계정에 {symbol}을 추가하지 않았어요. 먼저 추가해 달라고 하세요.", + "bg.xrpl.err.reserveForToken": "토큰을 추가하면 {amount} XRP와 수수료가 잠겨요. 먼저 XRP를 조금 추가하세요.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "{contract}에서 {action} 호출", + "bg.antelope.unknownToken": "{contract}의 {symbol}은 Clip Wallet이 아는 토큰이 아니에요. 보내기 전에 컨트랙트를 확인하세요.", + "bg.antelope.noRecipient": "이 네트워크에 {to}라는 계정이 없어서 실패할 거예요.", + "bg.antelope.updateAuth": "{account}의 {permission} 권한을 누가 관리할지 변경", + "bg.antelope.deleteAuth": "{account}의 {permission} 권한 삭제", + "bg.antelope.linkAuth": "{permission} 권한이 {action}을 실행하도록 허용", + "bg.antelope.unlinkAuth": "권한이 {action}을 실행하지 못하게 하기", + "bg.antelope.authWarning": "{account}를 관리하는 키나 계정이 바뀌어요. 내 것이 아니라면 그 안의 모든 것을 가져갈 수 있어요.", + "bg.antelope.unlinkWarning": "내 계정의 일부 작업을 실행할 수 있는 키가 바뀌어요.", + "bg.antelope.label.alsoSigns": "추가로 필요한 서명", + "bg.antelope.delayed": "보낸 뒤 {seconds}초가 지나야 실행되고, 그때까지는 취소할 수 있어요.", + "bg.antelope.feeFree": "없음(이 네트워크가 CPU와 NET을 대신 부담해요)", + "bg.antelope.feeResources": "토큰으로 내는 수수료 없음: 내 계정의 CPU와 NET을 사용해요", + "bg.antelope.appSends": "{host}(서명된 트랜잭션을 받아요)", + "bg.antelope.err.offline": "{network}에 연결하지 못했어요. 연결을 확인하고 다시 시도하세요.", + "bg.antelope.err.wrongNetwork": "이 요청은 지금 쓰는 것과 다른 Antelope 네트워크용이에요. 아무것도 서명하지 않았어요.", + "bg.antelope.err.contextFree": "이 트랜잭션은 Clip Wallet이 서명하지 않는 기능(컨텍스트 없는 액션이나 확장)을 사용해요.", + "bg.antelope.err.noAbi": "Clip Wallet이 컨트랙트 {contract}를 읽을 수 없어서 이 요청을 만들 수 없어요.", + "bg.antelope.err.permission": "이 키가 관리하지 않는 내 계정의 권한이 필요해요.", + "bg.antelope.err.otherSigners": "이 트랜잭션에는 다른 계정의 서명도 필요해서 Clip Wallet 혼자서는 보낼 수 없어요.", + "bg.antelope.err.noAccountTestnet": "아직 이 키를 쓰는 {network} 계정이 없어요. Antelope 네트워크의 계정은 체인에 만들어지는 이름이에요. 네트워크의 테스트넷 도구로 내 공개 키용 무료 테스트 계정을 만든 다음 돌아오세요.", + "bg.antelope.err.noAccount": "아직 이 키를 쓰는 {network} 계정이 없어요. Antelope 네트워크의 계정은 누군가 내 공개 키를 위해 체인에 만들어 주는 이름이에요. 거래소, 계정이 있는 친구, 계정 생성 서비스가 만들어 줄 수 있어요.", + "bg.antelope.err.notToken": "이 네트워크의 토큰이 아니에요.", + "bg.antelope.err.badAccount": "올바른 계정 이름이 아니에요. 이름은 최대 12자이고 a–z, 1–5, 점을 쓸 수 있어요.", + "bg.antelope.err.cpuPowerup": "{network}에서 내 계정의 CPU 또는 NET 시간이 바닥났어요. PowerUp으로 조금 빌린 뒤(24시간에 EOS 약간) 다시 시도하세요.", + "bg.antelope.err.cpuStake": "{network}에서 내 계정의 CPU 또는 NET 시간이 바닥났어요. CPU와 NET용으로 TLOS를 조금 스테이킹하거나 PowerUp으로 빌린 뒤 다시 시도하세요.", + "bg.antelope.err.cpuFree": "{network}가 지금은 이 작업의 CPU나 NET을 부담하지 못했어요. 잠시 후 다시 시도하세요.", + "bg.antelope.err.ram": "{network}에서 내 계정의 RAM이 부족해요. RAM을 조금 산 뒤 다시 시도하세요.", + "bg.antelope.err.balance": "이 토큰이 부족해요.", + "bg.antelope.err.expired": "이 트랜잭션은 보내기 전에 만료됐어요. 다시 시도하세요.", + "bg.antelope.err.auth": "네트워크에 따르면 이 키는 그 계정을 대신해 서명할 수 없어요. 아무것도 보내지 않았어요.", + "bg.antelope.err.noRecipient": "받는 사람 계정이 이 네트워크에 없어요. 아무것도 보내지 않았어요.", + "bg.antelope.err.duplicate": "똑같은 트랜잭션을 이미 보냈어요. 활동 내역을 확인하세요.", + "bg.antelope.err.rejected": "{network}가 이 트랜잭션을 거부했어요. 아무것도 보내지 않았어요.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "{validator}에서 스테이킹 보상 받기", + "bg.multiversx.withdrawFrom": "{validator}에서 스테이킹 해제된 EGLD 인출", + "bg.multiversx.restakeRewardsWith": "{validator}에 보상 다시 스테이킹", + "bg.multiversx.labelGuardian": "가디언", + "bg.multiversx.setGuardianTitle": "{guardian}을(를) 내 계정의 가디언으로 지정", + "bg.multiversx.setGuardianWarn": "가디언이 활성화되면 이 계정의 모든 거래에 가디언의 공동 서명이 필요해요. {guardian}을(를) 직접 고르지 않았다면 누군가 내 계정을 잠가 버릴 수 있어요.", + "bg.multiversx.guardAccountTitle": "내 계정의 가디언 켜기", + "bg.multiversx.guardAccountWarn": "이제부터 이 계정의 모든 거래에 가디언의 공동 서명이 필요해요. Clip Wallet은 이 서명을 제공할 수 없어서 Clip Wallet에서는 더 이상 보낼 수 없어요.", + "bg.multiversx.unguardAccountTitle": "내 계정의 가디언 끄기", + "bg.multiversx.changeOwnerTitle": "{contract} 컨트랙트를 {owner}에게 넘기기", + "bg.multiversx.changeOwnerWarn": "{owner}이(가) {contract} 컨트랙트의 소유자가 돼요. 새 소유자는 코드를 바꾸고 안에 있는 자산을 가져갈 수 있어요. 정말 넘길 생각일 때만 진행하세요.", + "bg.multiversx.guardedAccount": "이 계정에는 가디언이 있고 Clip Wallet은 가디언의 공동 서명을 받을 수 없어요. 아무것도 전송되지 않았어요.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "거래소가 입금 주소로 주는 형태인 계정 ID로 보내요. 거래소에 표시된 것과 정확히 같은지 확인하세요.", + "bg.icp.expired": "승인하기 전에 이 송금의 유효 시간이 지났어요. 아무것도 전송되지 않았어요. 다시 시도해 주세요.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "사후 조건", + "bg.stacks.pcYouSendExactly": "내가 정확히 {amount}을(를) 보내요", + "bg.stacks.pcYouSendAtMost": "내가 최대 {amount}을(를) 보내요", + "bg.stacks.pcYouSendAtLeast": "내가 최소 {amount}을(를) 보내요", + "bg.stacks.pcYouSendMoreThan": "내가 {amount}보다 많이 보내요", + "bg.stacks.pcYouSendLessThan": "내가 {amount}보다 적게 보내요", + "bg.stacks.pcSendsExactly": "{who}이(가) 정확히 {amount}을(를) 보내요", + "bg.stacks.pcSendsAtMost": "{who}이(가) 최대 {amount}을(를) 보내요", + "bg.stacks.pcSendsAtLeast": "{who}이(가) 최소 {amount}을(를) 보내요", + "bg.stacks.pcSendsMoreThan": "{who}이(가) {amount}보다 많이 보내요", + "bg.stacks.pcSendsLessThan": "{who}이(가) {amount}보다 적게 보내요", + "bg.stacks.pcYouSendNft": "내가 {item}을(를) 보내요", + "bg.stacks.pcYouKeepNft": "내가 {item}을(를) 계속 보유해요", + "bg.stacks.pcYouMaySendNft": "내가 {item}을(를) 보낼 수도 있어요", + "bg.stacks.pcSendsNft": "{who}이(가) {item}을(를) 보내요", + "bg.stacks.pcKeepsNft": "{who}이(가) {item}을(를) 계속 보유해요", + "bg.stacks.pcMaySendNft": "{who}이(가) {item}을(를) 보낼 수도 있어요", + "bg.stacks.pcStakingRule": "{who}의 스테이킹 규칙", + "bg.stacks.allowMode": "이렇게 하면 컨트랙트가 목록에 있는 자산뿐 아니라 내 자산을 무엇이든 옮길 수 있어요. {host}을(를) 완전히 신뢰할 때만 승인하세요.", + "bg.stacks.originatorMode": "목록에 있는 전송만 내 계정에서 나갈 수 있어요. 하지만 컨트랙트가 다른 계정의 자산은 여전히 옮길 수 있어요.", + "bg.stacks.nothingLeaves": "이 트랜잭션에서는 네트워크 수수료 외에 내 자산이 계정에서 나가지 않아요.", + "bg.stacks.highFee": "네트워크 수수료가 {fee}(으)로 비정상적으로 높아요.", + "bg.stacks.sponsorPays": "{host}이(가) 선택한 스폰서", + "bg.stacks.deployNamed": "스마트 컨트랙트 {name} 만들기", + "bg.stacks.notYourTx": "이 트랜잭션은 다른 Stacks 계정이 서명하는 것이라서 Clip Wallet이 서명할 수 없어요.", + "bg.stacks.multisig": "Clip Wallet은 아직 공유(다중 서명) Stacks 계정으로 서명할 수 없어요.", + "bg.stacks.memoTooLong": "메모가 너무 길어요. Stacks 메모에는 최대 34바이트까지 넣을 수 있어요.", + "bg.stacks.feeTooLow": "네트워크 수수료가 너무 낮아서 네트워크가 트랜잭션을 받지 않았어요. 아무것도 전송되지 않았어요. 다시 시도하세요.", + "bg.stacks.nonceBusy": "이 계정의 다른 트랜잭션이 아직 대기 중이에요. 완료될 때까지 기다린 뒤 다시 시도하세요.", + "bg.stacks.otherNetworkTx": "이 트랜잭션은 다른 Stacks 네트워크용이라서 전송되지 않았어요.", + "bg.stacks.badContractCall": "앱의 컨트랙트 호출이 네트워크에 있는 컨트랙트와 일치하지 않아요. 아무것도 전송되지 않았어요.", + "bg.stacks.tooManyPending": "이 계정에 대기 중인 트랜잭션이 너무 많아요. 일부가 완료될 때까지 기다린 뒤 다시 시도하세요.", + "bg.stacks.signatureRefused": "네트워크가 서명을 받아들이지 않았어요. 아무것도 전송되지 않았어요.", + "bg.stacks.testAddressOnMainnet": "Stacks 테스트 네트워크 주소(ST…)예요. 메인넷 주소(SP…)를 사용하세요.", + "bg.stacks.mainAddressOnTestnet": "Stacks 메인넷 주소(SP…)예요. 테스트 네트워크 주소(ST…)를 사용하세요.", + "bg.stacks.notAnAddress": "Stacks 주소가 아닌 것 같아요.", + "bg.stacks.needStxForFee": "네트워크 수수료를 내려면 STX가 조금 필요해요.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "이후 남는 {symbol}은(는) 모두 나에게 돌아오지 않고 {to}(으)로 가요.", + "bg.fuel.leftoverLost": "{amount}은(는) 이 트랜잭션에서 어디로도 보내지지 않아 사라져요.", + "bg.fuel.coinsSpent": "이 트랜잭션이 사용하는 코인 일부가 이미 사용됐어요. 앱에 다시 시도해 달라고 요청하세요.", + "bg.fuel.feeRose": "준비한 뒤 네트워크 수수료가 올랐어요. 아무것도 전송되지 않았어요. 다시 시도하세요.", + "bg.fuel.failedOnChain": "이 트랜잭션은 Fuel 네트워크에서 실패했어요. 네트워크 수수료만 지불됐어요.", + "bg.fuel.tooManyCoins": "한 번에 너무 많은 작은 코인이 필요해요. 먼저 더 적은 금액을 보내세요.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "이 앱이 Clip Wallet에 키나 서명을 스마트 컨트랙트 데이터에 넣어 달라고 요청해요. Clip Wallet은 아직 이런 종류의 트랜잭션에 서명할 수 없어요.", + "bg.bch.spendsTokens": "지갑에서 CashTokens를 보내요: {tokens}.", + "bg.bch.highFee": "네트워크 수수료가 {fee}(으)로 비정상적으로 높아요.", + "bg.bch.othersInputs": "다른 소유자나 컨트랙트의 코인 {count}개", + "bg.bch.notCashAddr": "Bitcoin Cash 주소가 아닌 것 같아요. CashAddr 형식(bitcoincash:q…)을 사용하세요.", + "bg.bch.testAddressOnMainnet": "Bitcoin Cash 테스트 네트워크 주소(bchtest:…)예요. 메인넷 주소(bitcoincash:…)를 사용하세요.", + "bg.bch.mainAddressOnTestnet": "Bitcoin Cash 메인넷 주소(bitcoincash:…)예요. 테스트 네트워크 주소(bchtest:…)를 사용하세요.", + "bg.bch.tokenSendUnavailable": "CashTokens 보내기는 아직 지원되지 않아요.", + "bg.bch.belowDust": "{amount} 이상 보내세요. Bitcoin Cash에서는 이보다 적은 금액을 보낼 수 없어요.", + "bg.bch.feeTooLow": "수수료가 네트워크 기준보다 너무 낮았어요. 아무것도 전송되지 않았어요. 다시 시도하세요.", + "bg.bch.coinsSpent": "이 트랜잭션의 일부 코인은 이미 사용되었어요. 아무것도 전송되지 않았어요. 새로고침한 뒤 다시 시도하세요.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "네트워크 정액 요금", + "bg.evm.flatFee": "거래당 {amount}, 실패해도 부과됨", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "{symbol}은(는) Chainflip 계정 간에 보낼 수 없습니다. 옮기려면 Ethereum 주소로 상환하거나, Ethereum에서 다른 계정에 입금하세요.", + "bg.chainflip.redeem": "{amount}을(를) Ethereum의 {to}(으)로 상환", + "bg.chainflip.redeemAll": "모든 {symbol}을(를) Ethereum의 {to}(으)로 상환", + "bg.chainflip.allRedeemable": "본딩되지 않은 모든 {symbol}", + "bg.chainflip.label.executor": "완료할 수 있는 사람", + "bg.chainflip.anyone": "누구나", + "bg.chainflip.redeemSteps": "대기 기간이 지나면 상환은 Ethereum에서 완료됩니다(ETH 가스비가 듭니다). 만료 전에 아무도 완료하지 않으면 다시 해야 합니다.", + "bg.chainflip.redeemWarn": "{symbol}이(가) Chainflip을 떠나 Ethereum의 {to}(으)로 갑니다. 이 Ethereum 주소를 직접 관리하는지 확인하세요.", + "bg.chainflip.bindRedeem": "상환을 {to}(으)로만 영구히 허용", + "bg.chainflip.bindRedeemWarn": "절대 되돌릴 수 없습니다. 이 계정은 앞으로 {symbol}을(를) {to}(으)로만 상환할 수 있습니다. 그 Ethereum 주소를 관리하지 않는다면 {symbol}은(는) 영원히 잠깁니다.", + "bg.chainflip.bindExecutor": "상환 완료를 {to}만 할 수 있도록 영구히 설정", + "bg.chainflip.bindExecutorWarn": "절대 되돌릴 수 없습니다. 이 계정의 {symbol} 상환은 Ethereum에서 {to}만 완료할 수 있습니다. 그 키를 잃으면 상환된 {symbol}을(를) 받을 수 없습니다.", + "bg.chainflip.moveTo": "{amount}을(를) Chainflip 계정 {to}(으)로 이동", + "bg.chainflip.moveAllTo": "모든 {symbol}을(를) Chainflip 계정 {to}(으)로 이동", + "bg.chainflip.otherAccount": "{to}은(는) 내 계정이 아닙니다. 옮기기 전에 확인하세요.", + "bg.chainflip.foreignRecipient": "{chain}의 {to}(으)로 보냅니다. 주소를 확인하세요. Clip Wallet은 이를 되돌릴 수 없습니다.", + "bg.chainflip.registerLp": "Chainflip 유동성 공급자로 등록", + "bg.chainflip.deregisterLp": "Chainflip 유동성 공급자 계정 닫기", + "bg.chainflip.refundAddress": "{chain} 환불 주소를 {to}(으)로 설정", + "bg.chainflip.depositAddress": "{asset} 입금 주소 받기", + "bg.chainflip.label.boostFee": "Boost 수수료", + "bg.chainflip.label.refundTo": "환불 받는 곳", + "bg.chainflip.withdraw": "{amount}을(를) {chain}의 {to}(으)로 출금", + "bg.chainflip.swap": "{amount}을(를) {asset}(으)로 스왑", + "bg.chainflip.swapChannel": "{from}을(를) {to}(으)로 스왑하는 채널 열기", + "bg.chainflip.addBoost": "Boost: {tier} bps 풀에 {amount} 추가", + "bg.chainflip.stopBoost": "{tier} bps 풀에서 {asset} Boost 중지", + "bg.chainflip.lend": "{amount} 빌려주기", + "bg.chainflip.unlend": "빌려준 {amount} 회수", + "bg.chainflip.unlendAll": "빌려준 {asset} 모두 회수", + "bg.chainflip.borrow": "{amount} 빌리기", + "bg.chainflip.repay": "대출 #{id} 일부 상환", + "bg.chainflip.repayAll": "대출 #{id} 전액 상환", + "bg.chainflip.onSubAccount": "하위 계정 #{n}: {inner}", + "bg.chainflip.orderFee": "주문을 넣거나 바꾸려면 이 계정에 최소 1 {symbol}이(가) 있어야 합니다. 대부분은 돌려받지만, 같은 풀의 주문을 한 블록 안에서 여러 번 바꾸면 그때마다 수수료가 올라갑니다.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts new file mode 100644 index 00000000..42e62ef8 --- /dev/null +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Brazilian Portuguese: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../pt-BR.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "Canal IBC {channel}", + "bg.cosmos.ibcLeaves": "Isto envia os tokens para outra chain por IBC ({channel}). Confira se o endereço está certo nessa chain.", + "bg.cosmos.unbonding": "Seus {symbol} ficam bloqueados e não rendem nada até o fim do período de desvinculação da rede.", + "bg.cosmos.moveStake": "Mover {amount} de staking de {from} para {to}", + "bg.cosmos.claimRewardsFrom": "Resgatar recompensas de staking de {validator}", + "bg.cosmos.grantAuthz": "Deixar {grantee} agir pela sua conta", + "bg.cosmos.authzTakeAll": "Isto permite que {grantee} mova seus fundos sem pedir de novo, até você retirar a permissão. Pode levar tudo.", + "bg.cosmos.authzActs": "Isto permite que {grantee} aja pela sua conta sem pedir de novo, até você retirar a permissão. Só permita isso para apps em que você confia.", + "bg.cosmos.revokeAuthz": "Impedir que {grantee} aja pela sua conta", + "bg.cosmos.grantFees": "Deixar {grantee} pagar taxas de rede com seus {symbol}", + "bg.cosmos.feegrantUpTo": "Isto permite que {grantee} gaste até {amount} dos seus fundos em taxas de rede, sem pedir de novo.", + "bg.cosmos.feegrantAll": "Isto permite que {grantee} gaste seus {symbol} em taxas de rede sem limite, sem pedir de novo.", + "bg.cosmos.revokeFees": "Impedir que {grantee} pague taxas de rede com seus fundos", + "bg.cosmos.block": "bloco {height}", + "bg.cosmos.appGetsSigned": "{host} (recebe a transação assinada)", + "bg.cosmos.wrongPrefix": "Esse endereço é de outra rede. Os endereços em {network} começam com {prefix}.", + "bg.cosmos.noAccount": "Sua conta ainda não está em {network}. Receba um pouco de {symbol} primeiro.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "Esta conta TRON é controlada por outra chave, então a Clip Wallet não pode assinar por ela.", + "bg.tron.notOpen": "Sua conta TRON ainda não foi aberta. Receba um pouco de TRX primeiro.", + "bg.tron.contractRecipient": "Não é possível enviar TRX diretamente para um contrato inteligente.", + "bg.tron.newAccountFee": "Este endereço ainda não está aberto na TRON. Enviar para ele também custa {amount} para abri-lo.", + "bg.tron.getsSigned": "{host} (recebe a transação assinada)", + "bg.tron.energy": "energia", + "bg.tron.bandwidth": "largura de banda", + "bg.tron.tronPower": "poder de voto", + "bg.tron.stakeFor": "Fazer staking de {amount} para obter {resource}", + "bg.tron.days": "{count} dias", + "bg.tron.cancelUnstaking": "Cancelar suas retiradas pendentes do staking e fazer staking desses TRX de novo", + "bg.tron.lend": "Emprestar {resource} dos {amount} que você colocou em staking para {to}", + "bg.tron.stopLending": "Recuperar {resource} dos {amount} emprestados para {to}", + "bg.tron.lockedHours": "Cerca de {hours} horas. Você não pode pegar de volta antes disso.", + "bg.tron.vote": "Votar em {count} Super Representatives", + "bg.tron.votesReplace": "Isso substitui todos os seus votos anteriores.", + "bg.tron.claimVoteRewards": "Resgatar suas recompensas de votação", + "bg.tron.changePermissions": "Mudar quem controla sua conta TRON", + "bg.tron.keysThreshold": "{keys} (exige {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "Pagar até {amount} para que {to} receba {get}", + "bg.xrpl.atMost": "No máximo {amount}", + "bg.xrpl.anyAmount": "Qualquer valor", + "bg.xrpl.partialPayment": "Este é um pagamento parcial: o destinatário pode receber bem menos que {amount}. Lojas e corretoras já foram enganadas assim.", + "bg.xrpl.xrpToSelf": "Enviar XRP para sua própria conta não faz nada, então o XRP Ledger vai recusar.", + "bg.xrpl.frees": "Isso libera a reserva de {amount} que estava bloqueada.", + "bg.xrpl.locksReserve": "{amount} do seu XRP fica bloqueado enquanto este token estiver adicionado", + "bg.xrpl.freezesLine": "Isso também congela o token para o outro lado.", + "bg.xrpl.offer.fillOrKill": "Executar tudo agora ou cancelar", + "bg.xrpl.offer.immediate": "Executar o que der agora e cancelar o resto", + "bg.xrpl.offer.standing": "Fica aberta até ser executada, expirar ou você cancelar", + "bg.xrpl.cancelOffer": "Cancelar uma oferta", + "bg.xrpl.changeSettings": "Alterar as configurações da sua conta", + "bg.xrpl.asf.requireDest": "Exigir uma tag de destino nos pagamentos para você", + "bg.xrpl.asf.requireAuth": "Aprovar cada detentor dos seus tokens", + "bg.xrpl.asf.disallowXrp": "Pedir que não enviem XRP para você", + "bg.xrpl.asf.disableMaster": "Desativar a chave própria da sua conta", + "bg.xrpl.asf.accountTxnId": "Acompanhar o id da sua última transação", + "bg.xrpl.asf.noFreeze": "Abrir mão de congelar seus tokens, para sempre", + "bg.xrpl.asf.globalFreeze": "Congelar todos os tokens que você emitiu", + "bg.xrpl.asf.defaultRipple": "Deixar seus tokens passarem entre detentores (rippling)", + "bg.xrpl.asf.depositAuth": "Aceitar só pagamentos de contas que você aprovar", + "bg.xrpl.asf.nftMinter": "Permitir que outra conta crie NFTs por você", + "bg.xrpl.asf.noNftOffers": "Bloquear ofertas de NFT recebidas", + "bg.xrpl.asf.noChecks": "Bloquear cheques recebidos", + "bg.xrpl.asf.noPayChannels": "Bloquear canais de pagamento recebidos", + "bg.xrpl.asf.noTrustLines": "Bloquear trust lines recebidas", + "bg.xrpl.asf.clawback": "Permitir que você recupere os tokens que emite (clawback)", + "bg.xrpl.asf.lockTokens": "Permitir que detentores bloqueiem seus tokens em escrow", + "bg.xrpl.disableMasterWarning": "Isso desativa a chave própria da sua conta. Depois, só outra chave ou uma lista de signatários poderá usá-la; se não houver, ela fica bloqueada para sempre.", + "bg.xrpl.issuerSettingWarning": "Isso muda como os tokens que você emite funcionam. Algumas mudanças não podem ser desfeitas.", + "bg.xrpl.nftMinterWarning": "Isso permite que {who} crie NFTs em nome da sua conta.", + "bg.xrpl.cleared": "(apagado)", + "bg.xrpl.removeRegularKey": "Remover a chave extra da sua conta", + "bg.xrpl.regularKeyWarning": "{who} recebe uma chave que pode fazer qualquer coisa com sua conta, até esvaziá-la.", + "bg.xrpl.removeSigners": "Remover as contas que podem assinar pela sua", + "bg.xrpl.setSigners": "Permitir que outras {count} contas assinem pela sua", + "bg.xrpl.signersWarning": "Juntas, essas contas podem fazer qualquer coisa com sua conta, até esvaziá-la.", + "bg.xrpl.closeAccount": "Encerrar sua conta do XRP Ledger e enviar tudo para {to}", + "bg.xrpl.closeWarning": "Isso apaga sua conta do XRP Ledger e envia todo o seu XRP para {to}. A taxa de {fee} é queimada.", + "bg.xrpl.mintNft": "Criar um NFT", + "bg.xrpl.taxon": "Táxon {taxon}", + "bg.xrpl.nft.transferable": "Pode ser vendido ou doado", + "bg.xrpl.nft.notTransferable": "Não pode ser repassado", + "bg.xrpl.nft.issuerCanBurn": "O criador pode queimá-lo", + "bg.xrpl.nft.mutable": "O link pode ser alterado depois", + "bg.xrpl.burnNft": "Queimar um NFT", + "bg.xrpl.giveNft": "Oferecer seu NFT de graça para {to}", + "bg.xrpl.freeForAnyone": "Qualquer pessoa pode pegar este NFT sem pagar nada.", + "bg.xrpl.sellNft": "Oferecer seu NFT por {amount}", + "bg.xrpl.buyNft": "Oferecer {amount} por um NFT", + "bg.xrpl.offerHolds": "Nada é cobrado até o dono aceitar", + "bg.xrpl.acceptNftOffer": "Aceitar uma oferta de NFT", + "bg.xrpl.brokered": "Junta uma oferta de compra e uma de venda (venda intermediada)", + "bg.xrpl.offerUnknown": "A Clip Wallet não conseguiu consultar esta oferta, então não pode mostrar o que você paga ou recebe.", + "bg.xrpl.acceptNftSell": "Comprar um NFT por {amount}", + "bg.xrpl.acceptNftBuy": "Vender seu NFT por {amount}", + "bg.xrpl.cancelNftOffers": "Cancelar {count} ofertas de NFT", + "bg.xrpl.escrowCreate": "Bloquear {amount} em escrow para {to}", + "bg.xrpl.escrowCondition": "Liberar exige um segredo (crypto-condition)", + "bg.xrpl.escrowLocked": "Esses fundos ficam bloqueados até o escrow ser liberado ou cancelado.", + "bg.xrpl.escrowFinish": "Liberar um escrow", + "bg.xrpl.escrowCancel": "Cancelar um escrow", + "bg.xrpl.ammDeposit": "Adicionar fundos a um pool de liquidez", + "bg.xrpl.ammWithdraw": "Retirar fundos de um pool de liquidez", + "bg.xrpl.yourShare": "Sua parte do pool", + "bg.xrpl.poolAmounts": "O pool define os valores exatos na hora da execução.", + "bg.xrpl.unknownTx": "Aprovar uma transação {type} do XRP Ledger", + "bg.xrpl.recipientUnchecked": "A Clip Wallet não conseguiu verificar a conta do destinatário.", + "bg.xrpl.newAccountTooSmall": "Esta conta do XRP Ledger ainda não existe. Abri-la exige pelo menos {reserve}, então isso vai falhar.", + "bg.xrpl.opensAccount": "Isso abre uma nova conta do XRP Ledger; {reserve} ficam bloqueados nela como reserva.", + "bg.xrpl.noRecipientAccount": "Esta conta do XRP Ledger não existe, então não pode receber tokens.", + "bg.xrpl.destinationTagNeeded": "Este destinatário precisa de uma tag de destino e nenhuma foi definida. Sem ela, os fundos podem ser perdidos.", + "bg.xrpl.disallowsXrp": "Esta conta diz que não quer receber XRP.", + "bg.xrpl.depositAuth": "Esta conta só aceita pagamentos de contas que aprovou, então isso pode falhar.", + "bg.xrpl.noTrustLine": "O destinatário não adicionou {symbol} à conta dele, então este pagamento vai falhar.", + "bg.xrpl.notEnoughXrp": "Você não tem XRP suficiente para isso: {reserve} precisa ficar na sua conta como reserva.", + "bg.xrpl.highFee": "A taxa de rede está alta demais: {fee}.", + "bg.xrpl.appSends": "{host} (recebe a transação assinada)", + "bg.xrpl.feeCharged": "A taxa de rede foi cobrada mesmo assim.", + "bg.xrpl.label.destinationTag": "Tag de destino", + "bg.xrpl.label.sourceTag": "Tag de origem", + "bg.xrpl.label.deliversAtLeast": "Entrega pelo menos", + "bg.xrpl.label.issuer": "Emissor", + "bg.xrpl.label.replacesOffer": "Substitui a oferta", + "bg.xrpl.label.turnsOn": "Ativa", + "bg.xrpl.label.turnsOff": "Desativa", + "bg.xrpl.label.domain": "Domínio", + "bg.xrpl.label.transferFee": "Taxa de transferência", + "bg.xrpl.label.nftMinter": "Criador de NFT", + "bg.xrpl.label.quorum": "Assinaturas necessárias", + "bg.xrpl.label.link": "Link", + "bg.xrpl.label.royalty": "Royalty", + "bg.xrpl.label.releaseAfter": "Pode ser liberado depois de", + "bg.xrpl.label.cancelAfter": "Pode ser cancelado depois de", + "bg.xrpl.err.wrongNetwork": "Este app está pedindo uma rede do XRP Ledger diferente da que está conectada. Nada foi assinado.", + "bg.xrpl.err.multisig": "A assinatura múltipla ainda não está disponível na Clip Wallet.", + "bg.xrpl.err.alreadySigned": "Esta transação já está assinada, então a Clip Wallet não vai assiná-la de novo.", + "bg.xrpl.err.cantRead": "A Clip Wallet não consegue ler parte desta transação do XRP Ledger ({field}), então não pode assiná-la.", + "bg.xrpl.err.notActivated": "Sua conta do XRP Ledger ainda não está ativada. Receba pelo menos {reserve} XRP para ativá-la.", + "bg.xrpl.err.otherKey": "A chave própria desta conta está desativada e outra chave a controla, então a Clip Wallet não pode assinar por ela.", + "bg.xrpl.err.xAddressNetwork": "Este endereço X é de outra rede do XRP Ledger.", + "bg.xrpl.err.badAddress": "Isso não parece um endereço do XRP Ledger.", + "bg.xrpl.err.notToken": "Isso não é um token do XRP Ledger.", + "bg.xrpl.err.tagNeeded": "Este endereço precisa de uma tag de destino. Peça ao destinatário, ou peça um endereço X que já a inclua.", + "bg.xrpl.err.notEnoughXrp": "Você não tem XRP suficiente. {reserve} XRP precisam ficar na sua conta, e a taxa é de {fee} XRP.", + "bg.xrpl.err.newAccount": "Esta conta do XRP Ledger ainda não existe. Envie pelo menos {reserve} XRP para abri-la.", + "bg.xrpl.err.feeOnly": "Você precisa de um pouco de XRP para pagar a taxa de rede.", + "bg.xrpl.err.noTrustLine": "Ainda não adicionaram {symbol} à conta do XRP Ledger deles. Peça que adicionem primeiro.", + "bg.xrpl.err.reserveForToken": "Adicionar um token bloqueia {amount} XRP, mais a taxa. Adicione um pouco de XRP primeiro.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "Chamar {action} em {contract}", + "bg.antelope.unknownToken": "{symbol} de {contract} não é um token que a Clip Wallet conhece. Confira o contrato antes de enviar.", + "bg.antelope.noRecipient": "Não existe nenhuma conta chamada {to} nesta rede, então isso vai falhar.", + "bg.antelope.updateAuth": "Mudar quem controla a permissão {permission} de {account}", + "bg.antelope.deleteAuth": "Excluir a permissão {permission} de {account}", + "bg.antelope.linkAuth": "Permitir que a permissão {permission} execute {action}", + "bg.antelope.unlinkAuth": "Impedir que uma permissão execute {action}", + "bg.antelope.authWarning": "Isso muda quais chaves ou contas controlam {account}. Se não forem suas, podem levar tudo o que há nela.", + "bg.antelope.unlinkWarning": "Isso muda qual chave pode executar algumas ações da sua conta.", + "bg.antelope.label.alsoSigns": "Também precisa de", + "bg.antelope.delayed": "Isso só é executado {seconds} segundos depois de enviado e pode ser cancelado até lá.", + "bg.antelope.feeFree": "Nenhuma (esta rede cobre CPU e NET para você)", + "bg.antelope.feeResources": "Nenhuma em tokens: usa a CPU e a NET da sua conta", + "bg.antelope.appSends": "{host} (recebe a transação assinada)", + "bg.antelope.err.offline": "Não foi possível acessar {network}. Verifique sua conexão e tente de novo.", + "bg.antelope.err.wrongNetwork": "Esta solicitação é para uma rede Antelope diferente da que você está usando. Nada foi assinado.", + "bg.antelope.err.contextFree": "Esta transação usa recursos que a Clip Wallet não assina (ações sem contexto ou extensões).", + "bg.antelope.err.noAbi": "A Clip Wallet não consegue ler o contrato {contract}, então não pode montar esta solicitação.", + "bg.antelope.err.permission": "Isso precisa de uma permissão da sua conta que esta chave não controla.", + "bg.antelope.err.otherSigners": "Esta transação também precisa da assinatura de outras contas, então a Clip Wallet não pode enviá-la sozinha.", + "bg.antelope.err.noAccountTestnet": "Nenhuma conta da {network} usa esta chave ainda. Nas redes Antelope, contas são nomes criados na blockchain: crie uma conta de teste grátis para sua chave pública com a ferramenta de testnet da rede e volte aqui.", + "bg.antelope.err.noAccount": "Nenhuma conta da {network} usa esta chave ainda. Nas redes Antelope, contas são nomes que alguém cria na blockchain para sua chave pública: uma corretora, um amigo que tenha conta ou um serviço de criação de contas pode fazer isso.", + "bg.antelope.err.notToken": "Isso não é um token desta rede.", + "bg.antelope.err.badAccount": "Esse não é um nome de conta válido. Nomes têm até 12 caracteres: a–z, 1–5 e pontos.", + "bg.antelope.err.cpuPowerup": "Sua conta ficou sem tempo de CPU ou NET na {network}. Alugue um pouco com PowerUp (custa pouco EOS por 24 horas) e tente de novo.", + "bg.antelope.err.cpuStake": "Sua conta ficou sem tempo de CPU ou NET na {network}. Coloque um pouco de TLOS em staking para CPU e NET, ou alugue com PowerUp, e tente de novo.", + "bg.antelope.err.cpuFree": "A {network} não conseguiu cobrir a CPU ou a NET disto agora. Espere um pouco e tente de novo.", + "bg.antelope.err.ram": "Sua conta não tem RAM suficiente na {network} para isso. Compre um pouco de RAM e tente de novo.", + "bg.antelope.err.balance": "Você não tem o suficiente deste token para isso.", + "bg.antelope.err.expired": "Esta transação expirou antes de ser enviada. Tente de novo.", + "bg.antelope.err.auth": "A rede diz que esta chave não pode assinar por essa conta. Nada foi enviado.", + "bg.antelope.err.noRecipient": "A conta do destinatário não existe nesta rede. Nada foi enviado.", + "bg.antelope.err.duplicate": "Esta mesma transação já foi enviada. Confira a Atividade.", + "bg.antelope.err.rejected": "A {network} recusou esta transação. Nada foi enviado.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "Resgatar suas recompensas de staking de {validator}", + "bg.multiversx.withdrawFrom": "Sacar de {validator} seu EGLD retirado do staking", + "bg.multiversx.restakeRewardsWith": "Colocar suas recompensas de volta em staking com {validator}", + "bg.multiversx.labelGuardian": "Guardião", + "bg.multiversx.setGuardianTitle": "Tornar {guardian} o guardião da sua conta", + "bg.multiversx.setGuardianWarn": "Com um guardião ativo, toda transação desta conta precisa da assinatura conjunta dele. Se você não escolheu {guardian}, alguém pode bloquear seu acesso à sua conta.", + "bg.multiversx.guardAccountTitle": "Ativar o guardião da sua conta", + "bg.multiversx.guardAccountWarn": "A partir de agora, toda transação desta conta precisa da assinatura conjunta do guardião. A Clip Wallet não pode fornecê-la, então você não vai mais conseguir enviar pela Clip Wallet.", + "bg.multiversx.unguardAccountTitle": "Desativar o guardião da sua conta", + "bg.multiversx.changeOwnerTitle": "Passar o contrato {contract} para {owner}", + "bg.multiversx.changeOwnerWarn": "Isto torna {owner} o proprietário do contrato {contract}. O novo proprietário pode mudar o código dele e levar o que ele guarda. Só faça isso se quiser mesmo entregá-lo.", + "bg.multiversx.guardedAccount": "Esta conta tem um guardião, e a Clip Wallet não consegue a assinatura conjunta dele. Nada foi enviado.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "Isto vai para um ID de conta, o tipo de endereço de depósito que as corretoras fornecem. Confira se ele é exatamente igual ao que a corretora mostra.", + "bg.icp.expired": "Esta transferência expirou antes de você aprová-la. Nada foi enviado. Tente de novo.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "Pós-condição", + "bg.stacks.pcYouSendExactly": "Você envia exatamente {amount}", + "bg.stacks.pcYouSendAtMost": "Você envia no máximo {amount}", + "bg.stacks.pcYouSendAtLeast": "Você envia no mínimo {amount}", + "bg.stacks.pcYouSendMoreThan": "Você envia mais de {amount}", + "bg.stacks.pcYouSendLessThan": "Você envia menos de {amount}", + "bg.stacks.pcSendsExactly": "{who} envia exatamente {amount}", + "bg.stacks.pcSendsAtMost": "{who} envia no máximo {amount}", + "bg.stacks.pcSendsAtLeast": "{who} envia no mínimo {amount}", + "bg.stacks.pcSendsMoreThan": "{who} envia mais de {amount}", + "bg.stacks.pcSendsLessThan": "{who} envia menos de {amount}", + "bg.stacks.pcYouSendNft": "Você envia {item}", + "bg.stacks.pcYouKeepNft": "Você mantém {item}", + "bg.stacks.pcYouMaySendNft": "Você pode enviar {item}", + "bg.stacks.pcSendsNft": "{who} envia {item}", + "bg.stacks.pcKeepsNft": "{who} mantém {item}", + "bg.stacks.pcMaySendNft": "{who} pode enviar {item}", + "bg.stacks.pcStakingRule": "Uma regra de staking para {who}", + "bg.stacks.allowMode": "Isto permite que o contrato mova qualquer um dos seus ativos, não só os listados. Só aprove se você confia totalmente em {host}.", + "bg.stacks.originatorMode": "Só as transferências listadas podem sair da sua conta. O contrato ainda pode mover ativos de outras contas.", + "bg.stacks.nothingLeaves": "Nenhum dos seus ativos pode sair da sua conta nesta transação, exceto a taxa de rede.", + "bg.stacks.highFee": "A taxa de rede é de {fee}, o que é muito alto.", + "bg.stacks.sponsorPays": "Um patrocinador escolhido por {host}", + "bg.stacks.deployNamed": "Criar o contrato inteligente {name}", + "bg.stacks.notYourTx": "Esta transação é assinada por outra conta Stacks, então a Clip Wallet não pode assiná-la.", + "bg.stacks.multisig": "A Clip Wallet ainda não consegue assinar por contas Stacks compartilhadas (multiassinatura).", + "bg.stacks.memoTooLong": "O memo é longo demais. Um memo Stacks comporta até 34 bytes.", + "bg.stacks.feeTooLow": "A taxa de rede era baixa demais, então a rede não aceitou a transação. Nada foi enviado. Tente de novo.", + "bg.stacks.nonceBusy": "Outra transação desta conta ainda está pendente. Espere ela terminar e tente de novo.", + "bg.stacks.otherNetworkTx": "Esta transação é para outra rede Stacks, então não foi enviada.", + "bg.stacks.badContractCall": "A chamada de contrato do app não corresponde ao contrato na rede. Nada foi enviado.", + "bg.stacks.tooManyPending": "Esta conta tem transações pendentes demais. Espere algumas terminarem e tente de novo.", + "bg.stacks.signatureRefused": "A rede não aceitou a assinatura. Nada foi enviado.", + "bg.stacks.testAddressOnMainnet": "Esse é um endereço Stacks de rede de teste (ST…). Use um endereço da rede principal (SP…).", + "bg.stacks.mainAddressOnTestnet": "Esse é um endereço Stacks da rede principal (SP…). Use um endereço de rede de teste (ST…).", + "bg.stacks.notAnAddress": "Isso não parece um endereço Stacks.", + "bg.stacks.needStxForFee": "Você precisa de um pouco de STX para pagar a taxa de rede.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "Tudo o que sobrar do seu {symbol} depois disto vai para {to}, e não volta para você.", + "bg.fuel.leftoverLost": "{amount} não é enviado para lugar nenhum por esta transação e seria perdido.", + "bg.fuel.coinsSpent": "Algumas das moedas que esta transação usa já foram gastas. Peça ao app para tentar de novo.", + "bg.fuel.feeRose": "A taxa de rede subiu desde que isto foi preparado. Nada foi enviado. Tente de novo.", + "bg.fuel.failedOnChain": "Esta transação falhou na rede Fuel. Só a taxa de rede foi gasta.", + "bg.fuel.tooManyCoins": "Isto precisa de moedas pequenas demais de uma vez. Envie um valor menor primeiro.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "Este app pede que a Clip Wallet coloque a chave ou a assinatura dela nos dados de um contrato inteligente. A Clip Wallet ainda não consegue assinar esse tipo de transação.", + "bg.bch.spendsTokens": "Isto envia CashTokens da sua carteira: {tokens}.", + "bg.bch.highFee": "A taxa de rede é de {fee}, o que é muito alto.", + "bg.bch.othersInputs": "{count} moedas de outros donos ou contratos", + "bg.bch.notCashAddr": "Isso não parece um endereço Bitcoin Cash. Use o formato CashAddr (bitcoincash:q…).", + "bg.bch.testAddressOnMainnet": "Esse é um endereço Bitcoin Cash de rede de teste (bchtest:…). Use um endereço da rede principal (bitcoincash:…).", + "bg.bch.mainAddressOnTestnet": "Esse é um endereço Bitcoin Cash da rede principal (bitcoincash:…). Use um endereço de rede de teste (bchtest:…).", + "bg.bch.tokenSendUnavailable": "O envio de CashTokens ainda não está disponível.", + "bg.bch.belowDust": "Envie pelo menos {amount}. Valores menores não podem ser enviados na rede Bitcoin Cash.", + "bg.bch.feeTooLow": "A taxa estava baixa demais para a rede. Nada foi enviado. Tente de novo.", + "bg.bch.coinsSpent": "Algumas moedas desta transação já tinham sido gastas. Nada foi enviado. Atualize e tente de novo.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "Cobrança da rede", + "bg.evm.flatFee": "{amount} por transação, mesmo se ela falhar", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "Não é possível enviar {symbol} de uma conta Chainflip para outra. Para movê-lo, resgate-o para um endereço Ethereum ou financie a outra conta a partir do Ethereum.", + "bg.chainflip.redeem": "Resgatar {amount} para {to} no Ethereum", + "bg.chainflip.redeemAll": "Resgatar todo o seu {symbol} para {to} no Ethereum", + "bg.chainflip.allRedeemable": "Todo o seu {symbol} que não está vinculado", + "bg.chainflip.label.executor": "Quem pode concluir", + "bg.chainflip.anyone": "Qualquer pessoa", + "bg.chainflip.redeemSteps": "Após um período de espera, o resgate é concluído no Ethereum (isso custa gas em ETH). Se ninguém concluir antes de expirar, ele precisa ser refeito.", + "bg.chainflip.redeemWarn": "Seu {symbol} sai da Chainflip para {to} no Ethereum. Confira se você controla este endereço Ethereum.", + "bg.chainflip.bindRedeem": "Permitir resgates só para {to}, para sempre", + "bg.chainflip.bindRedeemWarn": "Isso nunca pode ser desfeito. Esta conta só vai resgatar {symbol} para {to}. Se você não controla esse endereço Ethereum, seu {symbol} fica bloqueado para sempre.", + "bg.chainflip.bindExecutor": "Só {to} poderá concluir seus resgates, para sempre", + "bg.chainflip.bindExecutorWarn": "Isso nunca pode ser desfeito. Só {to} poderá concluir os resgates de {symbol} desta conta no Ethereum. Se essa chave for perdida, o {symbol} resgatado não poderá ser retirado.", + "bg.chainflip.moveTo": "Mover {amount} para a conta Chainflip {to}", + "bg.chainflip.moveAllTo": "Mover todo o seu {symbol} para a conta Chainflip {to}", + "bg.chainflip.otherAccount": "{to} não é uma das suas contas. Confira antes de mover qualquer coisa para lá.", + "bg.chainflip.foreignRecipient": "Isso vai para {to} na rede {chain}. Confira o endereço: a Clip Wallet não pode desfazer isso.", + "bg.chainflip.registerLp": "Registrar-se como provedor de liquidez da Chainflip", + "bg.chainflip.deregisterLp": "Encerrar sua conta de provedor de liquidez da Chainflip", + "bg.chainflip.refundAddress": "Definir seu endereço de reembolso na rede {chain} como {to}", + "bg.chainflip.depositAddress": "Obter um endereço de depósito para {asset}", + "bg.chainflip.label.boostFee": "Taxa de Boost", + "bg.chainflip.label.refundTo": "Reembolsos vão para", + "bg.chainflip.withdraw": "Sacar {amount} para {to} na rede {chain}", + "bg.chainflip.swap": "Trocar {amount} por {asset}", + "bg.chainflip.swapChannel": "Abrir um canal para trocar {from} por {to}", + "bg.chainflip.addBoost": "Boost: adicionar {amount} ao pool de {tier} bps", + "bg.chainflip.stopBoost": "Parar o boost de {asset} no pool de {tier} bps", + "bg.chainflip.lend": "Emprestar {amount}", + "bg.chainflip.unlend": "Retirar {amount} que você emprestou", + "bg.chainflip.unlendAll": "Retirar todo o {asset} que você emprestou", + "bg.chainflip.borrow": "Tomar {amount} emprestado", + "bg.chainflip.repay": "Pagar parte do empréstimo #{id}", + "bg.chainflip.repayAll": "Quitar o empréstimo #{id}", + "bg.chainflip.onSubAccount": "Na subconta #{n}: {inner}", + "bg.chainflip.orderFee": "Criar ou alterar uma ordem exige pelo menos 1 {symbol} nesta conta. A maior parte volta, mas alterar ordens do mesmo pool muitas vezes em um bloco aumenta a taxa a cada vez.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts new file mode 100644 index 00000000..4454debe --- /dev/null +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Turkish: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../tr.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "IBC kanalı {channel}", + "bg.cosmos.ibcLeaves": "Bu, token'ları IBC ({channel}) üzerinden başka bir zincire gönderir. Adresin o zincir için doğru olduğunu kontrol edin.", + "bg.cosmos.unbonding": "{symbol} bakiyeniz, ağın unbonding süresi bitene kadar kilitli kalır ve kazanç getirmez.", + "bg.cosmos.moveStake": "{amount} stake'i {from} doğrulayıcısından {to} doğrulayıcısına taşı", + "bg.cosmos.claimRewardsFrom": "{validator} doğrulayıcısından stake ödüllerini al", + "bg.cosmos.grantAuthz": "{grantee} adresinin hesabınız adına işlem yapmasına izin ver", + "bg.cosmos.authzTakeAll": "Bu, {grantee} adresinin, izni geri alana kadar size tekrar sormadan paranızı taşımasına izin verir. Her şeyi alabilir.", + "bg.cosmos.authzActs": "Bu, {grantee} adresinin, izni geri alana kadar size tekrar sormadan hesabınız adına işlem yapmasına izin verir. Buna yalnızca güvendiğiniz uygulamalar için izin verin.", + "bg.cosmos.revokeAuthz": "{grantee} adresinin hesabınız adına işlem yapmasını durdur", + "bg.cosmos.grantFees": "{grantee} adresinin ağ ücretlerini {symbol} bakiyenizle ödemesine izin ver", + "bg.cosmos.feegrantUpTo": "Bu, {grantee} adresinin size tekrar sormadan ağ ücretleri için paranızdan en fazla {amount} harcamasına izin verir.", + "bg.cosmos.feegrantAll": "Bu, {grantee} adresinin size tekrar sormadan {symbol} bakiyenizi ağ ücretleri için limitsiz harcamasına izin verir.", + "bg.cosmos.revokeFees": "{grantee} adresinin ağ ücretlerini paranızla ödemesini durdur", + "bg.cosmos.block": "blok {height}", + "bg.cosmos.appGetsSigned": "{host} (imzalı işlemi alır)", + "bg.cosmos.wrongPrefix": "Bu adres başka bir ağa ait. {network} üzerindeki adresler {prefix} ile başlar.", + "bg.cosmos.noAccount": "Hesabınız henüz {network} üzerinde değil. Önce biraz {symbol} alın.", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "Bu TRON hesabı başka bir anahtar tarafından kontrol ediliyor, bu yüzden Clip Wallet onun için imzalayamıyor.", + "bg.tron.notOpen": "TRON hesabınız henüz açılmadı. Önce biraz TRX alın.", + "bg.tron.contractRecipient": "TRX doğrudan bir akıllı sözleşmeye gönderilemez.", + "bg.tron.newAccountFee": "Bu adres TRON'da henüz açılmadı. Buraya göndermenin, adresi açmak için ayrıca {amount} maliyeti var.", + "bg.tron.getsSigned": "{host} (imzalı işlemi alır)", + "bg.tron.energy": "enerji", + "bg.tron.bandwidth": "bant genişliği", + "bg.tron.tronPower": "oy gücü", + "bg.tron.stakeFor": "{resource} için {amount} stake et", + "bg.tron.days": "{count} gün", + "bg.tron.cancelUnstaking": "Bekleyen stake'ten çıkarma işlemlerinizi iptal edin ve bu TRX'i yeniden stake edin", + "bg.tron.lend": "Stake ettiğiniz {amount} tutarının {resource} kaynağını {to} adresine ödünç ver", + "bg.tron.stopLending": "{to} adresine ödünç verdiğiniz {amount} tutarının {resource} kaynağını geri al", + "bg.tron.lockedHours": "Yaklaşık {hours} saat. Bundan önce geri alamazsınız.", + "bg.tron.vote": "{count} Super Representative'e oy ver", + "bg.tron.votesReplace": "Bu, önceki tüm oylarınızın yerini alır.", + "bg.tron.claimVoteRewards": "Oylama ödüllerinizi alın", + "bg.tron.changePermissions": "TRON hesabınızı kimin kontrol ettiğini değiştirin", + "bg.tron.keysThreshold": "{keys} ({threshold} gerekli)", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "{to} alıcısı {get} alsın diye en fazla {amount} öde", + "bg.xrpl.atMost": "En fazla {amount}", + "bg.xrpl.anyAmount": "Herhangi bir tutar", + "bg.xrpl.partialPayment": "Bu kısmi bir ödeme: alıcı {amount} tutarından çok daha azını alabilir. Mağazalar ve borsalar bu yolla kandırıldı.", + "bg.xrpl.xrpToSelf": "Kendi hesabınıza XRP göndermek hiçbir şey yapmaz, bu yüzden XRP Ledger bunu reddeder.", + "bg.xrpl.frees": "Bu, kilitlediği {amount} rezervi serbest bırakır.", + "bg.xrpl.locksReserve": "Bu token ekli kaldığı sürece XRP'nizin {amount} kadarı kilitli kalır", + "bg.xrpl.freezesLine": "Bu, token'ı karşı taraf için de dondurur.", + "bg.xrpl.offer.fillOrKill": "Şimdi tamamen gerçekleştir ya da iptal et", + "bg.xrpl.offer.immediate": "Şimdi gerçekleşebilen kısmı yap, kalanını iptal et", + "bg.xrpl.offer.standing": "Gerçekleşene, süresi dolana ya da siz iptal edene kadar açık kalır", + "bg.xrpl.cancelOffer": "Bir teklifi iptal et", + "bg.xrpl.changeSettings": "Hesap ayarlarınızı değiştir", + "bg.xrpl.asf.requireDest": "Size yapılan ödemelerde hedef etiketi iste", + "bg.xrpl.asf.requireAuth": "Token'larınızın her sahibini tek tek onayla", + "bg.xrpl.asf.disallowXrp": "Size XRP gönderilmemesini iste", + "bg.xrpl.asf.disableMaster": "Hesabınızın kendi anahtarını kapat", + "bg.xrpl.asf.accountTxnId": "Son işleminizin kimliğini takip et", + "bg.xrpl.asf.noFreeze": "Token'larınızı dondurma hakkından kalıcı olarak vazgeç", + "bg.xrpl.asf.globalFreeze": "Çıkardığınız tüm token'ları dondur", + "bg.xrpl.asf.defaultRipple": "Token'larınızın sahipler arasında geçmesine izin ver (rippling)", + "bg.xrpl.asf.depositAuth": "Yalnızca onayladığınız hesaplardan ödeme kabul et", + "bg.xrpl.asf.nftMinter": "Başka bir hesabın sizin için NFT oluşturmasına izin ver", + "bg.xrpl.asf.noNftOffers": "Gelen NFT tekliflerini engelle", + "bg.xrpl.asf.noChecks": "Gelen çekleri engelle", + "bg.xrpl.asf.noPayChannels": "Gelen ödeme kanallarını engelle", + "bg.xrpl.asf.noTrustLines": "Gelen trust line'ları engelle", + "bg.xrpl.asf.clawback": "Çıkardığınız token'ları geri almanıza izin ver (clawback)", + "bg.xrpl.asf.lockTokens": "Sahiplerin token'larınızı emanette kilitlemesine izin ver", + "bg.xrpl.disableMasterWarning": "Bu, hesabınızın kendi anahtarını kapatır. Bundan sonra hesabı yalnızca başka bir anahtar ya da imzacı listesi kullanabilir; hiçbiri yoksa hesap kalıcı olarak kilitlenir.", + "bg.xrpl.issuerSettingWarning": "Bu, çıkardığınız token'ların nasıl çalıştığını değiştirir. Bazıları geri alınamaz.", + "bg.xrpl.nftMinterWarning": "Bu, {who} adresinin hesabınız adına NFT oluşturmasına izin verir.", + "bg.xrpl.cleared": "(temizlendi)", + "bg.xrpl.removeRegularKey": "Ek anahtarı hesabınızdan kaldır", + "bg.xrpl.regularKeyWarning": "{who} hesabınızla her şeyi yapabilen bir anahtar alır; hesabı boşaltmak dahil.", + "bg.xrpl.removeSigners": "Hesabınız adına imza atabilen hesapları kaldır", + "bg.xrpl.setSigners": "{count} başka hesabın sizin hesabınız adına imza atmasına izin ver", + "bg.xrpl.signersWarning": "Bu hesaplar birlikte hesabınızla her şeyi yapabilir; hesabı boşaltmak dahil.", + "bg.xrpl.closeAccount": "XRP Ledger hesabınızı kapatıp her şeyi {to} adresine gönder", + "bg.xrpl.closeWarning": "Bu, XRP Ledger hesabınızı siler ve tüm XRP'nizi {to} adresine gönderir. {fee} tutarındaki ücret yakılır.", + "bg.xrpl.mintNft": "Bir NFT oluştur", + "bg.xrpl.taxon": "Takson {taxon}", + "bg.xrpl.nft.transferable": "Satılabilir ya da hediye edilebilir", + "bg.xrpl.nft.notTransferable": "Başkasına aktarılamaz", + "bg.xrpl.nft.issuerCanBurn": "Oluşturan kişi yakabilir", + "bg.xrpl.nft.mutable": "Bağlantısı sonradan değiştirilebilir", + "bg.xrpl.burnNft": "Bir NFT'yi yak", + "bg.xrpl.giveNft": "NFT'nizi {to} adresine ücretsiz teklif et", + "bg.xrpl.freeForAnyone": "Bu NFT'yi herkes karşılıksız alabilir.", + "bg.xrpl.sellNft": "NFT'nizi {amount} karşılığında teklif et", + "bg.xrpl.buyNft": "Bir NFT için {amount} teklif et", + "bg.xrpl.offerHolds": "Sahibi kabul edene kadar hiçbir şey alınmaz", + "bg.xrpl.acceptNftOffer": "Bir NFT teklifini kabul et", + "bg.xrpl.brokered": "Bir alış ve bir satış teklifini eşleştirir (aracılı satış)", + "bg.xrpl.offerUnknown": "Clip Wallet bu teklifi okuyamadı, bu yüzden ne ödeyeceğinizi ya da ne alacağınızı gösteremiyor.", + "bg.xrpl.acceptNftSell": "{amount} karşılığında bir NFT satın al", + "bg.xrpl.acceptNftBuy": "NFT'nizi {amount} karşılığında sat", + "bg.xrpl.cancelNftOffers": "{count} NFT teklifini iptal et", + "bg.xrpl.escrowCreate": "{to} için {amount} tutarını emanette kilitle", + "bg.xrpl.escrowCondition": "Serbest bırakmak için bir sır gerekir (crypto-condition)", + "bg.xrpl.escrowLocked": "Bu fonlar emanet serbest bırakılana ya da iptal edilene kadar kilitli kalır.", + "bg.xrpl.escrowFinish": "Bir emaneti serbest bırak", + "bg.xrpl.escrowCancel": "Bir emaneti iptal et", + "bg.xrpl.ammDeposit": "Bir likidite havuzuna fon ekle", + "bg.xrpl.ammWithdraw": "Bir likidite havuzundan fon çek", + "bg.xrpl.yourShare": "Havuzdaki payınız", + "bg.xrpl.poolAmounts": "Kesin tutarları işlem sırasında havuz belirler.", + "bg.xrpl.unknownTx": "Bir XRP Ledger {type} işlemini onayla", + "bg.xrpl.recipientUnchecked": "Clip Wallet alıcının hesabını kontrol edemedi.", + "bg.xrpl.newAccountTooSmall": "Bu XRP Ledger hesabı henüz yok. Açmak için en az {reserve} gerekir, bu yüzden bu işlem başarısız olur.", + "bg.xrpl.opensAccount": "Bu, yeni bir XRP Ledger hesabı açar; {reserve} tutarı orada rezerv olarak kilitli kalır.", + "bg.xrpl.noRecipientAccount": "Bu XRP Ledger hesabı yok, bu yüzden token alamaz.", + "bg.xrpl.destinationTagNeeded": "Bu alıcı bir hedef etiketi istiyor ve hiç etiket yok. Etiket olmadan fonlar kaybolabilir.", + "bg.xrpl.disallowsXrp": "Bu hesap XRP almak istemediğini belirtiyor.", + "bg.xrpl.depositAuth": "Bu hesap yalnızca onayladığı hesaplardan ödeme kabul ediyor, bu yüzden işlem başarısız olabilir.", + "bg.xrpl.noTrustLine": "Alıcı {symbol} varlığını hesabına eklememiş, bu yüzden bu ödeme başarısız olur.", + "bg.xrpl.notEnoughXrp": "Bunun için yeterli XRP'niz yok: {reserve} tutarı rezerv olarak hesabınızda kalmalı.", + "bg.xrpl.highFee": "Ağ ücreti alışılmadık derecede yüksek: {fee}.", + "bg.xrpl.appSends": "{host} (imzalı işlemi o alır)", + "bg.xrpl.feeCharged": "Ağ ücreti yine de alındı.", + "bg.xrpl.label.destinationTag": "Hedef etiketi", + "bg.xrpl.label.sourceTag": "Kaynak etiketi", + "bg.xrpl.label.deliversAtLeast": "En az teslim eder", + "bg.xrpl.label.issuer": "Çıkaran", + "bg.xrpl.label.replacesOffer": "Yerine geçtiği teklif", + "bg.xrpl.label.turnsOn": "Açar", + "bg.xrpl.label.turnsOff": "Kapatır", + "bg.xrpl.label.domain": "Alan adı", + "bg.xrpl.label.transferFee": "Transfer ücreti", + "bg.xrpl.label.nftMinter": "NFT oluşturucu", + "bg.xrpl.label.quorum": "Gereken imza sayısı", + "bg.xrpl.label.link": "Bağlantı", + "bg.xrpl.label.royalty": "Telif payı", + "bg.xrpl.label.releaseAfter": "Şu tarihten sonra serbest bırakılabilir", + "bg.xrpl.label.cancelAfter": "Şu tarihten sonra iptal edilebilir", + "bg.xrpl.err.wrongNetwork": "Bu uygulama bağlı olduğu ağdan farklı bir XRP Ledger ağı istiyor. Hiçbir şey imzalanmadı.", + "bg.xrpl.err.multisig": "Çoklu imza Clip Wallet'ta henüz yok.", + "bg.xrpl.err.alreadySigned": "Bu işlem zaten imzalı, bu yüzden Clip Wallet onu tekrar imzalamaz.", + "bg.xrpl.err.cantRead": "Clip Wallet bu XRP Ledger işleminin bir kısmını ({field}) okuyamıyor, bu yüzden imzalayamaz.", + "bg.xrpl.err.notActivated": "XRP Ledger hesabınız henüz etkin değil. Etkinleştirmek için en az {reserve} XRP alın.", + "bg.xrpl.err.otherKey": "Bu hesabın kendi anahtarı kapalı ve hesabı başka bir anahtar yönetiyor, bu yüzden Clip Wallet onun için imza atamaz.", + "bg.xrpl.err.xAddressNetwork": "Bu X adresi farklı bir XRP Ledger ağına ait.", + "bg.xrpl.err.badAddress": "Bu bir XRP Ledger adresine benzemiyor.", + "bg.xrpl.err.notToken": "Bu bir XRP Ledger token'ı değil.", + "bg.xrpl.err.tagNeeded": "Bu adres bir hedef etiketi istiyor. Alıcıdan etiketi ya da onu içeren bir X adresini isteyin.", + "bg.xrpl.err.notEnoughXrp": "Yeterli XRP'niz yok. {reserve} XRP hesabınızda kalmalı ve ücret {fee} XRP.", + "bg.xrpl.err.newAccount": "Bu XRP Ledger hesabı henüz yok. Açmak için en az {reserve} XRP gönderin.", + "bg.xrpl.err.feeOnly": "Ağ ücretini ödemek için biraz XRP gerekiyor.", + "bg.xrpl.err.noTrustLine": "{symbol} henüz alıcının XRP Ledger hesabına eklenmemiş. Önce eklemesini isteyin.", + "bg.xrpl.err.reserveForToken": "Bir token eklemek {amount} XRP kilitler, buna ücret de eklenir. Önce biraz XRP ekleyin.", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "{contract} üzerinde {action} çağır", + "bg.antelope.unknownToken": "{contract} sözleşmesindeki {symbol}, Clip Wallet'ın tanıdığı bir token değil. Göndermeden önce sözleşmeyi kontrol edin.", + "bg.antelope.noRecipient": "Bu ağda {to} adlı bir hesap yok, bu yüzden işlem başarısız olur.", + "bg.antelope.updateAuth": "{account} hesabının {permission} iznini kimin yöneteceğini değiştir", + "bg.antelope.deleteAuth": "{account} hesabının {permission} iznini sil", + "bg.antelope.linkAuth": "{permission} izninin {action} çalıştırmasına izin ver", + "bg.antelope.unlinkAuth": "Bir iznin {action} çalıştırmasını durdur", + "bg.antelope.authWarning": "Bu, {account} hesabını hangi anahtarların ya da hesapların yönettiğini değiştirir. Size ait değillerse içindeki her şeyi alabilirler.", + "bg.antelope.unlinkWarning": "Bu, hesabınızın bazı işlemlerini hangi anahtarın çalıştırabileceğini değiştirir.", + "bg.antelope.label.alsoSigns": "Şunlar da gerekli", + "bg.antelope.delayed": "Bu, gönderildikten {seconds} saniye sonra çalışır ve o zamana kadar iptal edilebilir.", + "bg.antelope.feeFree": "Yok (bu ağ CPU ve NET'i sizin için karşılıyor)", + "bg.antelope.feeResources": "Token olarak yok: hesabınızın CPU ve NET'ini kullanır", + "bg.antelope.appSends": "{host} (imzalı işlemi o alır)", + "bg.antelope.err.offline": "{network} ağına ulaşılamadı. Bağlantınızı kontrol edip tekrar deneyin.", + "bg.antelope.err.wrongNetwork": "Bu istek, kullandığınızdan farklı bir Antelope ağı için. Hiçbir şey imzalanmadı.", + "bg.antelope.err.contextFree": "Bu işlem Clip Wallet'ın imzalamadığı özellikler kullanıyor (bağlamsız eylemler ya da uzantılar).", + "bg.antelope.err.noAbi": "Clip Wallet {contract} sözleşmesini okuyamıyor, bu yüzden bu isteği hazırlayamaz.", + "bg.antelope.err.permission": "Bunun için hesabınızın bu anahtarın yönetmediği bir izni gerekiyor.", + "bg.antelope.err.otherSigners": "Bu işlem başka hesapların da imzasını gerektiriyor, bu yüzden Clip Wallet onu tek başına gönderemez.", + "bg.antelope.err.noAccountTestnet": "Henüz hiçbir {network} hesabı bu anahtarı kullanmıyor. Antelope ağlarında hesaplar zincirde oluşturulan adlardır: ağın test ağı aracıyla açık anahtarınız için ücretsiz bir test hesabı oluşturun, sonra geri gelin.", + "bg.antelope.err.noAccount": "Henüz hiçbir {network} hesabı bu anahtarı kullanmıyor. Antelope ağlarında hesaplar, birinin açık anahtarınız için zincirde oluşturduğu adlardır: bir borsa, hesabı olan bir arkadaş ya da bir hesap oluşturma hizmeti bunu yapabilir.", + "bg.antelope.err.notToken": "Bu, bu ağda bir token değil.", + "bg.antelope.err.badAccount": "Bu geçerli bir hesap adı değil. Adlar en fazla 12 karakterdir: a–z, 1–5 ve nokta.", + "bg.antelope.err.cpuPowerup": "Hesabınızın {network} üzerindeki CPU ya da NET süresi bitti. PowerUp ile biraz kiralayın (24 saat için biraz EOS tutar), sonra tekrar deneyin.", + "bg.antelope.err.cpuStake": "Hesabınızın {network} üzerindeki CPU ya da NET süresi bitti. CPU ve NET için biraz TLOS stake edin ya da PowerUp ile kiralayın, sonra tekrar deneyin.", + "bg.antelope.err.cpuFree": "{network} şu anda bunun CPU ya da NET'ini karşılayamadı. Biraz bekleyip tekrar deneyin.", + "bg.antelope.err.ram": "Hesabınızın {network} üzerinde bunun için yeterli RAM'i yok. Biraz RAM satın alıp tekrar deneyin.", + "bg.antelope.err.balance": "Bunun için bu token'dan yeterince yok.", + "bg.antelope.err.expired": "Bu işlemin süresi gönderilmeden doldu. Tekrar deneyin.", + "bg.antelope.err.auth": "Ağa göre bu anahtar o hesap adına imza atamaz. Hiçbir şey gönderilmedi.", + "bg.antelope.err.noRecipient": "Alıcı hesap bu ağda yok. Hiçbir şey gönderilmedi.", + "bg.antelope.err.duplicate": "Bu işlemin aynısı zaten gönderildi. Etkinlik bölümüne bakın.", + "bg.antelope.err.rejected": "{network} bu işlemi reddetti. Hiçbir şey gönderilmedi.", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "{validator} doğrulayıcısındaki stake ödüllerini al", + "bg.multiversx.withdrawFrom": "{validator} doğrulayıcısından stake'ten çıkarılmış EGLD'ni çek", + "bg.multiversx.restakeRewardsWith": "Ödüllerini {validator} ile yeniden stake et", + "bg.multiversx.labelGuardian": "Koruyucu", + "bg.multiversx.setGuardianTitle": "{guardian} adresini hesabının koruyucusu yap", + "bg.multiversx.setGuardianWarn": "Bir koruyucu etkinleştiğinde bu hesaptan yapılan her işlem onun ortak imzasını gerektirir. {guardian} adresini sen seçmediysen biri seni kendi hesabından kilitleyebilir.", + "bg.multiversx.guardAccountTitle": "Hesabının koruyucusunu aç", + "bg.multiversx.guardAccountWarn": "Bundan sonra bu hesaptan yapılan her işlem koruyucunun ortak imzasını gerektirir. Clip Wallet bunu sağlayamaz, bu yüzden artık Clip Wallet'tan gönderim yapamazsın.", + "bg.multiversx.unguardAccountTitle": "Hesabının koruyucusunu kapat", + "bg.multiversx.changeOwnerTitle": "{contract} sözleşmesini {owner} adresine devret", + "bg.multiversx.changeOwnerWarn": "Bu, {owner} adresini {contract} sözleşmesinin sahibi yapar. Yeni sahip kodunu değiştirebilir ve içindekileri alabilir. Bunu yalnızca gerçekten devretmek istiyorsan yap.", + "bg.multiversx.guardedAccount": "Bu hesabın bir koruyucusu var ve Clip Wallet koruyucunun ortak imzasını alamıyor. Hiçbir şey gönderilmedi.", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "Bu, borsaların para yatırma adresi olarak verdiği türden bir hesap kimliğine gönderir. Borsanın gösterdiğiyle tam olarak aynı olduğunu kontrol et.", + "bg.icp.expired": "Bu transfer sen onaylamadan önce süresi doldu. Hiçbir şey gönderilmedi. Tekrar dene.", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "Son koşul", + "bg.stacks.pcYouSendExactly": "Tam olarak {amount} gönderiyorsunuz", + "bg.stacks.pcYouSendAtMost": "En fazla {amount} gönderiyorsunuz", + "bg.stacks.pcYouSendAtLeast": "En az {amount} gönderiyorsunuz", + "bg.stacks.pcYouSendMoreThan": "{amount} miktarından fazlasını gönderiyorsunuz", + "bg.stacks.pcYouSendLessThan": "{amount} miktarından azını gönderiyorsunuz", + "bg.stacks.pcSendsExactly": "{who} tam olarak {amount} gönderiyor", + "bg.stacks.pcSendsAtMost": "{who} en fazla {amount} gönderiyor", + "bg.stacks.pcSendsAtLeast": "{who} en az {amount} gönderiyor", + "bg.stacks.pcSendsMoreThan": "{who}, {amount} miktarından fazlasını gönderiyor", + "bg.stacks.pcSendsLessThan": "{who}, {amount} miktarından azını gönderiyor", + "bg.stacks.pcYouSendNft": "{item} gönderiyorsunuz", + "bg.stacks.pcYouKeepNft": "{item} sizde kalıyor", + "bg.stacks.pcYouMaySendNft": "{item} gönderebilirsiniz", + "bg.stacks.pcSendsNft": "{who}, {item} gönderiyor", + "bg.stacks.pcKeepsNft": "{item}, {who} üzerinde kalıyor", + "bg.stacks.pcMaySendNft": "{who}, {item} gönderebilir", + "bg.stacks.pcStakingRule": "{who} için bir stake kuralı", + "bg.stacks.allowMode": "Bu, sözleşmenin yalnızca listelenenleri değil, varlıklarınızdan herhangi birini taşımasına izin verir. Yalnızca şu siteye tamamen güveniyorsanız onaylayın: {host}.", + "bg.stacks.originatorMode": "Hesabınızdan yalnızca listelenen transferler çıkabilir. Sözleşme yine de diğer hesapların varlıklarını taşıyabilir.", + "bg.stacks.nothingLeaves": "Bu işlemde ağ ücreti dışında hiçbir varlığınız hesabınızdan çıkamaz.", + "bg.stacks.highFee": "Ağ ücreti {fee}; bu alışılmadık derecede yüksek.", + "bg.stacks.sponsorPays": "{host} tarafından seçilen bir sponsor", + "bg.stacks.deployNamed": "{name} akıllı sözleşmesini oluştur", + "bg.stacks.notYourTx": "Bu işlem farklı bir Stacks hesabı tarafından imzalanıyor, bu yüzden Clip Wallet onu imzalayamaz.", + "bg.stacks.multisig": "Clip Wallet henüz ortak (çoklu imzalı) Stacks hesapları için imza atamıyor.", + "bg.stacks.memoTooLong": "Memo çok uzun. Bir Stacks memo'su en fazla 34 bayt alabilir.", + "bg.stacks.feeTooLow": "Ağ ücreti çok düşüktü, bu yüzden ağ işlemi kabul etmedi. Hiçbir şey gönderilmedi. Tekrar deneyin.", + "bg.stacks.nonceBusy": "Bu hesaptan başka bir işlem hâlâ bekliyor. Tamamlanmasını bekleyip tekrar deneyin.", + "bg.stacks.otherNetworkTx": "Bu işlem farklı bir Stacks ağı için, bu yüzden gönderilmedi.", + "bg.stacks.badContractCall": "Uygulamanın sözleşme çağrısı ağdaki sözleşmeyle eşleşmiyor. Hiçbir şey gönderilmedi.", + "bg.stacks.tooManyPending": "Bu hesapta bekleyen çok fazla işlem var. Bazılarının tamamlanmasını bekleyip tekrar deneyin.", + "bg.stacks.signatureRefused": "Ağ imzayı kabul etmedi. Hiçbir şey gönderilmedi.", + "bg.stacks.testAddressOnMainnet": "Bu bir Stacks test ağı adresi (ST…). Ana ağ adresi (SP…) kullanın.", + "bg.stacks.mainAddressOnTestnet": "Bu bir Stacks ana ağ adresi (SP…). Test ağı adresi (ST…) kullanın.", + "bg.stacks.notAnAddress": "Bu bir Stacks adresine benzemiyor.", + "bg.stacks.needStxForFee": "Ağ ücretini ödemek için biraz STX'e ihtiyacınız var.", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "Bundan sonra {symbol} bakiyenizden kalan her şey size değil, {to} adresine gider.", + "bg.fuel.leftoverLost": "{amount} bu işlemle hiçbir yere gönderilmiyor ve kaybolur.", + "bg.fuel.coinsSpent": "Bu işlemin kullandığı coinlerin bazıları zaten harcanmış. Uygulamadan tekrar denemesini isteyin.", + "bg.fuel.feeRose": "Bu hazırlandığından beri ağ ücreti yükseldi. Hiçbir şey gönderilmedi. Tekrar deneyin.", + "bg.fuel.failedOnChain": "Bu işlem Fuel ağında başarısız oldu. Yalnızca ağ ücreti ödendi.", + "bg.fuel.tooManyCoins": "Bunun için aynı anda çok fazla küçük coin gerekiyor. Önce daha küçük bir tutar gönderin.", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "Bu uygulama, Clip Wallet'tan anahtarını veya imzasını bir akıllı sözleşmenin verilerine eklemesini istiyor. Clip Wallet henüz bu tür bir işlemi imzalayamıyor.", + "bg.bch.spendsTokens": "Bu işlem cüzdanınızdan CashTokens gönderir: {tokens}.", + "bg.bch.highFee": "Ağ ücreti {fee}; bu alışılmadık derecede yüksek.", + "bg.bch.othersInputs": "Başka sahiplere veya sözleşmelere ait {count} coin", + "bg.bch.notCashAddr": "Bu bir Bitcoin Cash adresine benzemiyor. CashAddr biçimini (bitcoincash:q…) kullanın.", + "bg.bch.testAddressOnMainnet": "Bu bir Bitcoin Cash test ağı adresi (bchtest:…). Ana ağ adresi (bitcoincash:…) kullanın.", + "bg.bch.mainAddressOnTestnet": "Bu bir Bitcoin Cash ana ağ adresi (bitcoincash:…). Test ağı adresi (bchtest:…) kullanın.", + "bg.bch.tokenSendUnavailable": "CashTokens gönderimi henüz kullanılamıyor.", + "bg.bch.belowDust": "En az {amount} gönderin. Bitcoin Cash'te daha küçük tutarlar gönderilemez.", + "bg.bch.feeTooLow": "Ücret ağ için çok düşüktü. Hiçbir şey gönderilmedi. Tekrar deneyin.", + "bg.bch.coinsSpent": "Bu işlemdeki bazı coin'ler zaten harcanmış. Hiçbir şey gönderilmedi. Yenileyip tekrar deneyin.", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "Ağ kesintisi", + "bg.evm.flatFee": "İşlem başına {amount}, başarısız olsa bile", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "{symbol} bir Chainflip hesabından diğerine gönderilemez. Taşımak için onu bir Ethereum adresine geri al ya da diğer hesabı Ethereum'dan fonla.", + "bg.chainflip.redeem": "{amount} miktarını Ethereum'daki {to} adresine geri al", + "bg.chainflip.redeemAll": "Tüm {symbol} varlığını Ethereum'daki {to} adresine geri al", + "bg.chainflip.allRedeemable": "Bağlı olmayan tüm {symbol} varlığın", + "bg.chainflip.label.executor": "Kim tamamlayabilir", + "bg.chainflip.anyone": "Herkes", + "bg.chainflip.redeemSteps": "Bir bekleme süresinden sonra geri alım Ethereum'da tamamlanır (bunun için ETH gas ödenir). Süresi dolmadan kimse tamamlamazsa yeniden yapılması gerekir.", + "bg.chainflip.redeemWarn": "{symbol} varlığın Chainflip'ten Ethereum'daki {to} adresine gidiyor. Bu Ethereum adresini kontrol ettiğinden emin ol.", + "bg.chainflip.bindRedeem": "Geri alımlara kalıcı olarak yalnızca {to} adresine izin ver", + "bg.chainflip.bindRedeemWarn": "Bu işlem asla iptal edilemez. Bu hesap {symbol} varlığını yalnızca {to} adresine geri alabilecek. O Ethereum adresini kontrol etmiyorsan {symbol} varlığın sonsuza dek kilitli kalır.", + "bg.chainflip.bindExecutor": "Geri alımlarını kalıcı olarak yalnızca {to} tamamlayabilsin", + "bg.chainflip.bindExecutorWarn": "Bu işlem asla iptal edilemez. Bu hesabın Ethereum'daki {symbol} geri alımlarını yalnızca {to} tamamlayabilecek. O anahtar kaybolursa geri alınan {symbol} talep edilemez.", + "bg.chainflip.moveTo": "{amount} miktarını {to} Chainflip hesabına taşı", + "bg.chainflip.moveAllTo": "Tüm {symbol} varlığını {to} Chainflip hesabına taşı", + "bg.chainflip.otherAccount": "{to} senin hesaplarından biri değil. Oraya bir şey taşımadan önce kontrol et.", + "bg.chainflip.foreignRecipient": "Bu, {chain} ağındaki {to} adresine gidiyor. Adresi kontrol et: Clip Wallet bunu geri alamaz.", + "bg.chainflip.registerLp": "Chainflip likidite sağlayıcısı olarak kaydol", + "bg.chainflip.deregisterLp": "Chainflip likidite sağlayıcısı hesabını kapat", + "bg.chainflip.refundAddress": "{chain} iade adresini {to} olarak ayarla", + "bg.chainflip.depositAddress": "{asset} için bir yatırma adresi al", + "bg.chainflip.label.boostFee": "Boost ücreti", + "bg.chainflip.label.refundTo": "İadelerin gideceği yer", + "bg.chainflip.withdraw": "{amount} miktarını {chain} ağındaki {to} adresine çek", + "bg.chainflip.swap": "{amount} miktarını {asset} ile takas et", + "bg.chainflip.swapChannel": "{from} varlığını {to} ile takas etmek için bir kanal aç", + "bg.chainflip.addBoost": "Boost: {tier} bps havuzuna {amount} ekle", + "bg.chainflip.stopBoost": "{tier} bps havuzunda {asset} boost'unu durdur", + "bg.chainflip.lend": "{amount} borç ver", + "bg.chainflip.unlend": "Borç verdiğin {amount} miktarını geri çek", + "bg.chainflip.unlendAll": "Borç verdiğin tüm {asset} varlığını geri çek", + "bg.chainflip.borrow": "{amount} borç al", + "bg.chainflip.repay": "#{id} numaralı kredinin bir kısmını öde", + "bg.chainflip.repayAll": "#{id} numaralı krediyi tamamen öde", + "bg.chainflip.onSubAccount": "#{n} numaralı alt hesapta: {inner}", + "bg.chainflip.orderFee": "Emir vermek veya değiştirmek için bu hesapta en az 1 {symbol} gerekir. Çoğu geri gelir, ancak aynı havuzdaki emirleri bir blokta birçok kez değiştirmek her seferinde daha yüksek ücrete yol açar.", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts new file mode 100644 index 00000000..72c99ae9 --- /dev/null +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -0,0 +1,315 @@ +import type N87 from "../../en/n87.js"; + +/** Simplified Chinese: networks87 chain modules (same sections as ../../en/n87.ts). Follows ../zh-Hans.ts and its glossary. */ +const messages: { readonly [K in keyof typeof N87]: string } = { + // ---- chains-cosmos (cosmos, provenance, thorchain, initia) + "bg.cosmos.ibcChannel": "IBC 通道 {channel}", + "bg.cosmos.ibcLeaves": "这会通过 IBC({channel})把代币发送到另一条链。请确认该地址在那条链上是正确的。", + "bg.cosmos.unbonding": "在网络的解绑期结束前,你的 {symbol} 会一直锁定,也不会产生收益。", + "bg.cosmos.moveStake": "将 {amount} 的质押从 {from} 转到 {to}", + "bg.cosmos.claimRewardsFrom": "领取来自 {validator} 的质押奖励", + "bg.cosmos.grantAuthz": "允许 {grantee} 代表你的账户操作", + "bg.cosmos.authzTakeAll": "这会让 {grantee} 在你收回权限之前,无需再次征得你同意即可转移你的资金。对方可能拿走全部资产。", + "bg.cosmos.authzActs": "这会让 {grantee} 在你收回权限之前,无需再次征得你同意即可代表你的账户操作。只对你信任的应用授予此权限。", + "bg.cosmos.revokeAuthz": "不再允许 {grantee} 代表你的账户操作", + "bg.cosmos.grantFees": "允许 {grantee} 用你的 {symbol} 支付网络手续费", + "bg.cosmos.feegrantUpTo": "这会让 {grantee} 无需再次征得你同意,用你的资金支付最多 {amount} 的网络手续费。", + "bg.cosmos.feegrantAll": "这会让 {grantee} 无需再次征得你同意,不限额地用你的 {symbol} 支付网络手续费。", + "bg.cosmos.revokeFees": "不再允许 {grantee} 用你的资金支付网络手续费", + "bg.cosmos.block": "区块 {height}", + "bg.cosmos.appGetsSigned": "{host}(会收到已签名的交易)", + "bg.cosmos.wrongPrefix": "这个地址属于另一个网络。{network} 上的地址以 {prefix} 开头。", + "bg.cosmos.noAccount": "你的账户还没有出现在 {network} 上。请先接收一些 {symbol}。", + // ---- end chains-cosmos + // ---- chains-tron (tron) + "bg.tron.notControlled": "此 TRON 账户由另一个密钥控制,因此 Clip Wallet 无法为其签名。", + "bg.tron.notOpen": "你的 TRON 账户尚未开通。请先接收一些 TRX。", + "bg.tron.contractRecipient": "TRX 不能直接发送到智能合约。", + "bg.tron.newAccountFee": "该地址尚未在 TRON 上开通。向其发送还需额外支付 {amount} 来开通它。", + "bg.tron.getsSigned": "{host}(会收到已签名的交易)", + "bg.tron.energy": "能量", + "bg.tron.bandwidth": "带宽", + "bg.tron.tronPower": "投票权", + "bg.tron.stakeFor": "质押 {amount} 以获取{resource}", + "bg.tron.days": "{count} 天", + "bg.tron.cancelUnstaking": "取消待处理的解除质押,并重新质押这些 TRX", + "bg.tron.lend": "将你质押的 {amount} 所产生的{resource}借给 {to}", + "bg.tron.stopLending": "收回借给 {to} 的 {amount} 所产生的{resource}", + "bg.tron.lockedHours": "约 {hours} 小时。在此之前无法收回。", + "bg.tron.vote": "投票给 {count} 个 Super Representative", + "bg.tron.votesReplace": "这会替换你之前的所有投票。", + "bg.tron.claimVoteRewards": "领取你的投票奖励", + "bg.tron.changePermissions": "更改谁能控制你的 TRON 账户", + "bg.tron.keysThreshold": "{keys}(需要 {threshold})", + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + "bg.xrpl.payUpTo": "最多支付 {amount},让 {to} 收到 {get}", + "bg.xrpl.atMost": "最多 {amount}", + "bg.xrpl.anyAmount": "任意金额", + "bg.xrpl.partialPayment": "这是部分支付:接收方收到的可能远少于 {amount}。商店和交易所曾因此被骗。", + "bg.xrpl.xrpToSelf": "向你自己的账户发送 XRP 不会有任何效果,所以 XRP Ledger 会拒绝。", + "bg.xrpl.frees": "这会释放之前锁定的 {amount} 储备金。", + "bg.xrpl.locksReserve": "只要添加了此代币,你的 XRP 中就有 {amount} 保持锁定", + "bg.xrpl.freezesLine": "这也会为对方冻结该代币。", + "bg.xrpl.offer.fillOrKill": "立即全部成交,否则取消", + "bg.xrpl.offer.immediate": "立即成交能成交的部分,其余取消", + "bg.xrpl.offer.standing": "保持挂单,直到成交、过期或你取消", + "bg.xrpl.cancelOffer": "取消一个挂单", + "bg.xrpl.changeSettings": "更改你的账户设置", + "bg.xrpl.asf.requireDest": "要求付款给你时带上目标标签", + "bg.xrpl.asf.requireAuth": "逐个批准你的代币持有人", + "bg.xrpl.asf.disallowXrp": "请别人不要向你发送 XRP", + "bg.xrpl.asf.disableMaster": "关闭你账户自己的密钥", + "bg.xrpl.asf.accountTxnId": "记录你上一笔交易的 ID", + "bg.xrpl.asf.noFreeze": "永久放弃冻结你的代币", + "bg.xrpl.asf.globalFreeze": "冻结你发行的所有代币", + "bg.xrpl.asf.defaultRipple": "让你的代币在持有人之间流转(rippling)", + "bg.xrpl.asf.depositAuth": "只接受你批准的账户付款", + "bg.xrpl.asf.nftMinter": "允许另一个账户替你创建 NFT", + "bg.xrpl.asf.noNftOffers": "阻止收到的 NFT 报价", + "bg.xrpl.asf.noChecks": "阻止收到的支票", + "bg.xrpl.asf.noPayChannels": "阻止收到的支付通道", + "bg.xrpl.asf.noTrustLines": "阻止收到的信任线", + "bg.xrpl.asf.clawback": "允许你收回你发行的代币(clawback)", + "bg.xrpl.asf.lockTokens": "允许持有人把你的代币锁入托管", + "bg.xrpl.disableMasterWarning": "这会关闭你账户自己的密钥。之后只有另一个密钥或签名人列表能使用该账户;如果都没有,账户将被永久锁定。", + "bg.xrpl.issuerSettingWarning": "这会改变你发行的代币的行为。其中一些无法撤销。", + "bg.xrpl.nftMinterWarning": "这让 {who} 可以用你账户的名义创建 NFT。", + "bg.xrpl.cleared": "(已清除)", + "bg.xrpl.removeRegularKey": "从你的账户移除额外密钥", + "bg.xrpl.regularKeyWarning": "{who} 将获得一个可对你的账户做任何事(包括清空它)的密钥。", + "bg.xrpl.removeSigners": "移除可以替你的账户签名的账户", + "bg.xrpl.setSigners": "允许另外 {count} 个账户替你的账户签名", + "bg.xrpl.signersWarning": "这些账户联合起来可以对你的账户做任何事,包括清空它。", + "bg.xrpl.closeAccount": "关闭你的 XRP Ledger 账户并把所有资产发送给 {to}", + "bg.xrpl.closeWarning": "这会删除你的 XRP Ledger 账户,并把你所有的 XRP 发送给 {to}。{fee} 的手续费会被销毁。", + "bg.xrpl.mintNft": "创建一个 NFT", + "bg.xrpl.taxon": "分类号 {taxon}", + "bg.xrpl.nft.transferable": "可以出售或赠送", + "bg.xrpl.nft.notTransferable": "无法转给他人", + "bg.xrpl.nft.issuerCanBurn": "创建者可以销毁它", + "bg.xrpl.nft.mutable": "它的链接以后可以更改", + "bg.xrpl.burnNft": "销毁一个 NFT", + "bg.xrpl.giveNft": "把你的 NFT 免费提供给 {to}", + "bg.xrpl.freeForAnyone": "任何人都可以免费拿走这个 NFT。", + "bg.xrpl.sellNft": "以 {amount} 出售你的 NFT", + "bg.xrpl.buyNft": "出价 {amount} 购买一个 NFT", + "bg.xrpl.offerHolds": "在所有者接受之前不会扣款", + "bg.xrpl.acceptNftOffer": "接受一个 NFT 报价", + "bg.xrpl.brokered": "撮合一个买入报价和一个卖出报价(经纪成交)", + "bg.xrpl.offerUnknown": "Clip Wallet 无法查询这个报价,所以无法显示你要支付或收到什么。", + "bg.xrpl.acceptNftSell": "以 {amount} 购买一个 NFT", + "bg.xrpl.acceptNftBuy": "以 {amount} 出售你的 NFT", + "bg.xrpl.cancelNftOffers": "取消 {count} 个 NFT 报价", + "bg.xrpl.escrowCreate": "为 {to} 将 {amount} 锁入托管", + "bg.xrpl.escrowCondition": "释放需要一个密钥条件(crypto-condition)", + "bg.xrpl.escrowLocked": "在托管被释放或取消之前,这些资金都会保持锁定。", + "bg.xrpl.escrowFinish": "释放一个托管", + "bg.xrpl.escrowCancel": "取消一个托管", + "bg.xrpl.ammDeposit": "向流动性池添加资金", + "bg.xrpl.ammWithdraw": "从流动性池取出资金", + "bg.xrpl.yourShare": "你在池中的份额", + "bg.xrpl.poolAmounts": "确切金额由流动性池在执行时决定。", + "bg.xrpl.unknownTx": "批准一笔 XRP Ledger {type} 交易", + "bg.xrpl.recipientUnchecked": "Clip Wallet 无法检查接收方的账户。", + "bg.xrpl.newAccountTooSmall": "这个 XRP Ledger 账户还不存在。开户至少需要 {reserve},所以这会失败。", + "bg.xrpl.opensAccount": "这会开设一个新的 XRP Ledger 账户;其中 {reserve} 会作为储备金锁定在那里。", + "bg.xrpl.noRecipientAccount": "这个 XRP Ledger 账户不存在,因此无法接收代币。", + "bg.xrpl.destinationTagNeeded": "该接收方需要目标标签,但没有设置。没有它,资金可能会丢失。", + "bg.xrpl.disallowsXrp": "该账户表示不想接收 XRP。", + "bg.xrpl.depositAuth": "该账户只接受其已批准账户的付款,所以这可能会失败。", + "bg.xrpl.noTrustLine": "接收方还没有把 {symbol} 添加到其账户,所以这笔付款会失败。", + "bg.xrpl.notEnoughXrp": "你的 XRP 不够:{reserve} 必须作为储备金留在你的账户中。", + "bg.xrpl.highFee": "网络手续费异常高:{fee}。", + "bg.xrpl.appSends": "{host}(它会收到已签名的交易)", + "bg.xrpl.feeCharged": "网络手续费仍已扣除。", + "bg.xrpl.label.destinationTag": "目标标签", + "bg.xrpl.label.sourceTag": "来源标签", + "bg.xrpl.label.deliversAtLeast": "至少交付", + "bg.xrpl.label.issuer": "发行方", + "bg.xrpl.label.replacesOffer": "替换的挂单", + "bg.xrpl.label.turnsOn": "开启", + "bg.xrpl.label.turnsOff": "关闭", + "bg.xrpl.label.domain": "域名", + "bg.xrpl.label.transferFee": "转账手续费", + "bg.xrpl.label.nftMinter": "NFT 创建者", + "bg.xrpl.label.quorum": "所需签名数", + "bg.xrpl.label.link": "链接", + "bg.xrpl.label.royalty": "版税", + "bg.xrpl.label.releaseAfter": "可释放时间", + "bg.xrpl.label.cancelAfter": "可取消时间", + "bg.xrpl.err.wrongNetwork": "这个应用请求的 XRP Ledger 网络与它连接的不同。没有签署任何内容。", + "bg.xrpl.err.multisig": "Clip Wallet 暂不支持多重签名。", + "bg.xrpl.err.alreadySigned": "这笔交易已经签名,所以 Clip Wallet 不会再签一次。", + "bg.xrpl.err.cantRead": "Clip Wallet 无法读取这笔 XRP Ledger 交易的一部分({field}),因此无法签名。", + "bg.xrpl.err.notActivated": "你的 XRP Ledger 账户尚未激活。至少接收 {reserve} XRP 即可激活。", + "bg.xrpl.err.otherKey": "此账户自己的密钥已关闭,由另一个密钥控制,所以 Clip Wallet 无法替它签名。", + "bg.xrpl.err.xAddressNetwork": "这个 X 地址属于另一个 XRP Ledger 网络。", + "bg.xrpl.err.badAddress": "这看起来不像 XRP Ledger 地址。", + "bg.xrpl.err.notToken": "这不是 XRP Ledger 上的代币。", + "bg.xrpl.err.tagNeeded": "这个地址需要目标标签。请向接收方索取标签,或索取包含标签的 X 地址。", + "bg.xrpl.err.notEnoughXrp": "你的 XRP 不够。{reserve} XRP 必须留在你的账户中,手续费为 {fee} XRP。", + "bg.xrpl.err.newAccount": "这个 XRP Ledger 账户还不存在。至少发送 {reserve} XRP 来开户。", + "bg.xrpl.err.feeOnly": "你需要一点 XRP 来支付网络手续费。", + "bg.xrpl.err.noTrustLine": "对方还没有把 {symbol} 添加到其 XRP Ledger 账户。请先让对方添加。", + "bg.xrpl.err.reserveForToken": "添加代币会锁定 {amount} XRP,另加手续费。请先充值一些 XRP。", + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + "bg.antelope.callOn": "在 {contract} 上调用 {action}", + "bg.antelope.unknownToken": "{contract} 的 {symbol} 不是 Clip Wallet 认识的代币。发送前请检查合约。", + "bg.antelope.noRecipient": "这个网络上没有名为 {to} 的账户,所以这会失败。", + "bg.antelope.updateAuth": "更改谁控制 {account} 的 {permission} 权限", + "bg.antelope.deleteAuth": "删除 {account} 的 {permission} 权限", + "bg.antelope.linkAuth": "允许 {permission} 权限执行 {action}", + "bg.antelope.unlinkAuth": "禁止某个权限执行 {action}", + "bg.antelope.authWarning": "这会更改控制 {account} 的密钥或账户。如果它们不是你的,就可能拿走其中的一切。", + "bg.antelope.unlinkWarning": "这会更改哪个密钥可以执行你账户的某些操作。", + "bg.antelope.label.alsoSigns": "还需要", + "bg.antelope.delayed": "这会在发送 {seconds} 秒后才执行,在此之前可以取消。", + "bg.antelope.feeFree": "无(这个网络替你承担 CPU 和 NET)", + "bg.antelope.feeResources": "不收代币费用:使用你账户的 CPU 和 NET", + "bg.antelope.appSends": "{host}(它会收到已签名的交易)", + "bg.antelope.err.offline": "无法连接 {network}。请检查网络连接后重试。", + "bg.antelope.err.wrongNetwork": "这个请求针对的 Antelope 网络与你当前使用的不同。没有签署任何内容。", + "bg.antelope.err.contextFree": "这笔交易使用了 Clip Wallet 不签署的功能(无上下文操作或扩展)。", + "bg.antelope.err.noAbi": "Clip Wallet 无法读取合约 {contract},因此无法构建这个请求。", + "bg.antelope.err.permission": "这需要你账户中一个不由此密钥控制的权限。", + "bg.antelope.err.otherSigners": "这笔交易还需要其他账户签名,所以 Clip Wallet 无法单独发送。", + "bg.antelope.err.noAccountTestnet": "还没有使用此密钥的 {network} 账户。Antelope 网络上的账户是在链上创建的名称:请用该网络的测试网工具为你的公钥免费创建一个测试账户,然后再回来。", + "bg.antelope.err.noAccount": "还没有使用此密钥的 {network} 账户。Antelope 网络上的账户是别人在链上为你的公钥创建的名称:交易所、有账户的朋友或账户创建服务都可以创建。", + "bg.antelope.err.notToken": "这不是此网络上的代币。", + "bg.antelope.err.badAccount": "这不是有效的账户名。名称最多 12 个字符:a–z、1–5 和点。", + "bg.antelope.err.cpuPowerup": "你的账户在 {network} 上的 CPU 或 NET 时间已用完。请用 PowerUp 租用一些(24 小时只需少量 EOS),然后重试。", + "bg.antelope.err.cpuStake": "你的账户在 {network} 上的 CPU 或 NET 时间已用完。请质押一些 TLOS 换取 CPU 和 NET,或用 PowerUp 租用,然后重试。", + "bg.antelope.err.cpuFree": "{network} 暂时无法承担这笔操作的 CPU 或 NET。请稍等片刻后重试。", + "bg.antelope.err.ram": "你的账户在 {network} 上的 RAM 不足。请购买一些 RAM,然后重试。", + "bg.antelope.err.balance": "你的这种代币不够。", + "bg.antelope.err.expired": "这笔交易在发送前已过期。请重试。", + "bg.antelope.err.auth": "网络表示此密钥不能代表该账户签名。没有发送任何内容。", + "bg.antelope.err.noRecipient": "接收方账户在这个网络上不存在。没有发送任何内容。", + "bg.antelope.err.duplicate": "完全相同的交易已经发送过。请查看动态。", + "bg.antelope.err.rejected": "{network} 拒绝了这笔交易。没有发送任何内容。", + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + "bg.multiversx.claimRewardsFrom": "从 {validator} 领取你的质押奖励", + "bg.multiversx.withdrawFrom": "从 {validator} 提取你已解除质押的 EGLD", + "bg.multiversx.restakeRewardsWith": "将你的奖励重新质押给 {validator}", + "bg.multiversx.labelGuardian": "守护者", + "bg.multiversx.setGuardianTitle": "将 {guardian} 设为你账户的守护者", + "bg.multiversx.setGuardianWarn": "守护者生效后,此账户的每笔交易都需要它的联合签名。如果 {guardian} 不是你自己选的,别人可能会把你锁在自己的账户之外。", + "bg.multiversx.guardAccountTitle": "开启你账户的守护者", + "bg.multiversx.guardAccountWarn": "从现在起,此账户的每笔交易都需要守护者的联合签名。Clip Wallet 无法提供,因此你将无法再从 Clip Wallet 发送。", + "bg.multiversx.unguardAccountTitle": "关闭你账户的守护者", + "bg.multiversx.changeOwnerTitle": "将合约 {contract} 交给 {owner}", + "bg.multiversx.changeOwnerWarn": "这会让 {owner} 成为合约 {contract} 的所有者。新所有者可以修改它的代码并拿走其中的资产。只有在你确实想转让它时才这样做。", + "bg.multiversx.guardedAccount": "此账户设有守护者,Clip Wallet 无法获得守护者的联合签名。没有发送任何内容。", + // ---- end chains-multiversx + // ---- chains-icp (icp) + "bg.icp.toAccountId": "这会发送到一个账户 ID,也就是交易所给出的那种充值地址。请确认它与交易所显示的完全一致。", + "bg.icp.expired": "此转账在你批准之前已过期。没有发送任何内容。请重试。", + // ---- end chains-icp + // ---- chains-stacks (stacks) + "bg.stacks.postCondition": "后置条件", + "bg.stacks.pcYouSendExactly": "你恰好发送 {amount}", + "bg.stacks.pcYouSendAtMost": "你最多发送 {amount}", + "bg.stacks.pcYouSendAtLeast": "你至少发送 {amount}", + "bg.stacks.pcYouSendMoreThan": "你发送超过 {amount}", + "bg.stacks.pcYouSendLessThan": "你发送少于 {amount}", + "bg.stacks.pcSendsExactly": "{who} 恰好发送 {amount}", + "bg.stacks.pcSendsAtMost": "{who} 最多发送 {amount}", + "bg.stacks.pcSendsAtLeast": "{who} 至少发送 {amount}", + "bg.stacks.pcSendsMoreThan": "{who} 发送超过 {amount}", + "bg.stacks.pcSendsLessThan": "{who} 发送少于 {amount}", + "bg.stacks.pcYouSendNft": "你发送 {item}", + "bg.stacks.pcYouKeepNft": "你保留 {item}", + "bg.stacks.pcYouMaySendNft": "你可能发送 {item}", + "bg.stacks.pcSendsNft": "{who} 发送 {item}", + "bg.stacks.pcKeepsNft": "{who} 保留 {item}", + "bg.stacks.pcMaySendNft": "{who} 可能发送 {item}", + "bg.stacks.pcStakingRule": "{who} 的质押规则", + "bg.stacks.allowMode": "这会让合约可以转移你的任何资产,而不仅是列出的资产。只有在你完全信任 {host} 时才批准。", + "bg.stacks.originatorMode": "只有列出的转账可以离开你的账户。合约仍可能转移其他账户的资产。", + "bg.stacks.nothingLeaves": "在此交易中,除网络手续费外,你的任何资产都不会离开你的账户。", + "bg.stacks.highFee": "网络手续费为 {fee},异常高。", + "bg.stacks.sponsorPays": "由 {host} 选择的赞助方", + "bg.stacks.deployNamed": "创建智能合约 {name}", + "bg.stacks.notYourTx": "此交易由另一个 Stacks 账户签名,因此 Clip Wallet 无法签名。", + "bg.stacks.multisig": "Clip Wallet 暂不支持为共享(多重签名)Stacks 账户签名。", + "bg.stacks.memoTooLong": "备忘过长。Stacks 备忘最多可容纳 34 字节。", + "bg.stacks.feeTooLow": "网络手续费过低,因此网络未接受此交易。没有发送任何内容。请重试。", + "bg.stacks.nonceBusy": "此账户的另一笔交易仍在等待中。请等它完成后重试。", + "bg.stacks.otherNetworkTx": "此交易属于另一个 Stacks 网络,因此未发送。", + "bg.stacks.badContractCall": "应用的合约调用与网络上的合约不匹配。没有发送任何内容。", + "bg.stacks.tooManyPending": "此账户有太多交易在等待中。请等其中一些完成后重试。", + "bg.stacks.signatureRefused": "网络未接受该签名。没有发送任何内容。", + "bg.stacks.testAddressOnMainnet": "这是 Stacks 测试网络地址(ST…)。请使用主网地址(SP…)。", + "bg.stacks.mainAddressOnTestnet": "这是 Stacks 主网地址(SP…)。请使用测试网络地址(ST…)。", + "bg.stacks.notAnAddress": "这看起来不像 Stacks 地址。", + "bg.stacks.needStxForFee": "你需要少量 STX 来支付网络手续费。", + // ---- end chains-stacks + // ---- chains-fuel (fuel) + "bg.fuel.changeToOther": "此后你剩余的所有 {symbol} 都会转给 {to},而不会退回给你。", + "bg.fuel.leftoverLost": "{amount} 在此交易中没有发送到任何地方,将会丢失。", + "bg.fuel.coinsSpent": "此交易使用的部分币已经被花掉。请让应用重试。", + "bg.fuel.feeRose": "准备之后网络手续费上涨了。没有发送任何内容。请重试。", + "bg.fuel.failedOnChain": "此交易在 Fuel 网络上失败。只花费了网络手续费。", + "bg.fuel.tooManyCoins": "这需要一次使用太多小额币。请先发送较小的金额。", + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + "bg.bch.contractPlaceholders": "此应用要求 Clip Wallet 将其密钥或签名放入智能合约的数据中。Clip Wallet 暂不支持签署此类交易。", + "bg.bch.spendsTokens": "这会从你的钱包发送 CashTokens:{tokens}。", + "bg.bch.highFee": "网络手续费为 {fee},异常高。", + "bg.bch.othersInputs": "{count} 个来自其他所有者或合约的币", + "bg.bch.notCashAddr": "这看起来不像 Bitcoin Cash 地址。请使用其 CashAddr 格式(bitcoincash:q…)。", + "bg.bch.testAddressOnMainnet": "这是 Bitcoin Cash 测试网络地址(bchtest:…)。请使用主网地址(bitcoincash:…)。", + "bg.bch.mainAddressOnTestnet": "这是 Bitcoin Cash 主网地址(bitcoincash:…)。请使用测试网络地址(bchtest:…)。", + "bg.bch.tokenSendUnavailable": "暂不支持发送 CashTokens。", + "bg.bch.belowDust": "请至少发送 {amount}。在 Bitcoin Cash 上无法发送更小的金额。", + "bg.bch.feeTooLow": "手续费低于网络要求。没有发送任何内容。请重试。", + "bg.bch.coinsSpent": "此交易中的部分币已被花费。没有发送任何内容。请刷新后重试。", + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "网络定额收费", + "bg.evm.flatFee": "每笔交易 {amount},失败也收取", + // ---- end chains-evm + // ---- chains-substrate (Chainflip) + "bg.chainflip.noTransfer": "{symbol} 不能在 Chainflip 账户之间转账。要转移它,请将其赎回到Ethereum 地址,或从 Ethereum为另一个账户充值。", + "bg.chainflip.redeem": "将 {amount} 赎回到Ethereum 上的 {to}", + "bg.chainflip.redeemAll": "将你所有的 {symbol} 赎回到Ethereum 上的 {to}", + "bg.chainflip.allRedeemable": "你所有未绑定的 {symbol}", + "bg.chainflip.label.executor": "谁可以完成", + "bg.chainflip.anyone": "任何人", + "bg.chainflip.redeemSteps": "等待一段时间后,赎回需在 Ethereum 上完成(需要支付 ETH gas)。如果到期前无人完成,就需要重新发起。", + "bg.chainflip.redeemWarn": "你的 {symbol} 将离开 Chainflip,转到Ethereum 上的 {to}。请确认你控制这个Ethereum 地址。", + "bg.chainflip.bindRedeem": "永久只允许赎回到 {to}", + "bg.chainflip.bindRedeemWarn": "此操作永远无法撤销。此账户今后只能将 {symbol} 赎回到 {to}。如果你不控制该Ethereum 地址,你的 {symbol} 将被永久锁定。", + "bg.chainflip.bindExecutor": "永久只允许 {to} 完成你的赎回", + "bg.chainflip.bindExecutorWarn": "此操作永远无法撤销。只有 {to} 能在 Ethereum 上完成此账户的 {symbol} 赎回。如果该密钥丢失,已赎回的 {symbol} 将无法领取。", + "bg.chainflip.moveTo": "将 {amount} 转移到 Chainflip 账户 {to}", + "bg.chainflip.moveAllTo": "将你所有的 {symbol} 转移到 Chainflip 账户 {to}", + "bg.chainflip.otherAccount": "{to} 不是你的账户。转移任何资产前请先核对。", + "bg.chainflip.foreignRecipient": "这将发送到 {chain} 上的 {to}。请核对地址:Clip Wallet 无法撤销。", + "bg.chainflip.registerLp": "注册为 Chainflip 流动性提供者", + "bg.chainflip.deregisterLp": "关闭你的 Chainflip 流动性提供者账户", + "bg.chainflip.refundAddress": "将你的 {chain} 退款地址设为 {to}", + "bg.chainflip.depositAddress": "获取 {asset} 的充值地址", + "bg.chainflip.label.boostFee": "Boost 手续费", + "bg.chainflip.label.refundTo": "退款发送到", + "bg.chainflip.withdraw": "将 {amount} 提取到 {chain} 上的 {to}", + "bg.chainflip.swap": "将 {amount} 兑换为 {asset}", + "bg.chainflip.swapChannel": "开启通道,将 {from} 兑换为 {to}", + "bg.chainflip.addBoost": "Boost:向 {tier} bps 池添加 {amount}", + "bg.chainflip.stopBoost": "停止在 {tier} bps 池中为 {asset} 提供 Boost", + "bg.chainflip.lend": "借出 {amount}", + "bg.chainflip.unlend": "取回你借出的 {amount}", + "bg.chainflip.unlendAll": "取回你借出的全部 {asset}", + "bg.chainflip.borrow": "借入 {amount}", + "bg.chainflip.repay": "偿还贷款 #{id} 的一部分", + "bg.chainflip.repayAll": "全额偿还贷款 #{id}", + "bg.chainflip.onSubAccount": "在子账户 #{n} 上:{inner}", + "bg.chainflip.orderFee": "下单或修改订单需要此账户中至少有 1 {symbol}。大部分会退回,但在同一个区块内多次修改同一池子的订单,每次的手续费都会更高。", + // ---- end chains-substrate (Chainflip) +}; +export default messages; diff --git a/packages/core/src/messages/locales/pt-BR.ts b/packages/core/src/messages/locales/pt-BR.ts index 84e72487..cd4ecc49 100644 --- a/packages/core/src/messages/locales/pt-BR.ts +++ b/packages/core/src/messages/locales/pt-BR.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/pt-BR.js"; /** Brazilian Portuguese: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/pt-BR/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{count} etapas em {app}", "bg.act.batchStopped": "{count} etapas em {app}, interrompidas na etapa {n}", "bg.err.batchHardware": "Várias etapas em uma só aprovação ainda não estão disponíveis para carteiras de hardware. Peça ao app para enviá-las uma de cada vez.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/tr.ts b/packages/core/src/messages/locales/tr.ts index c929745b..4eb39b31 100644 --- a/packages/core/src/messages/locales/tr.ts +++ b/packages/core/src/messages/locales/tr.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/tr.js"; /** Turkish: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/tr/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "{app} üzerinde {count} adım", "bg.act.batchStopped": "{app} üzerinde {count} adım, {n}. adımda durdu", "bg.err.batchHardware": "Tek onayda birden fazla adım donanım cüzdanlarında henüz kullanılamıyor. Uygulamadan adımları tek tek göndermesini isteyin.", + ...n87, }; export default messages; diff --git a/packages/core/src/messages/locales/zh-Hans.ts b/packages/core/src/messages/locales/zh-Hans.ts index cd853d76..f4cda763 100644 --- a/packages/core/src/messages/locales/zh-Hans.ts +++ b/packages/core/src/messages/locales/zh-Hans.ts @@ -1,4 +1,5 @@ import type { BgTranslation } from "../en/index.js"; +import n87 from "./n87/zh-Hans.js"; /** Simplified Chinese: background messages (approval titles, labels, warnings, errors, activity). Follows packages/ui/src/i18n/zh-Hans/index.ts. */ const messages: BgTranslation = { @@ -586,5 +587,6 @@ const messages: BgTranslation = { "bg.act.batch": "在 {app} 上的 {count} 个步骤", "bg.act.batchStopped": "在 {app} 上的 {count} 个步骤,在第 {n} 步停止", "bg.err.batchHardware": "硬件钱包暂不支持在一次批准中完成多个步骤。请让应用逐个发送。", + ...n87, }; export default messages; diff --git a/packages/create-clip-wallet/README.md b/packages/create-clip-wallet/README.md index 10cf26ba..b421be21 100644 --- a/packages/create-clip-wallet/README.md +++ b/packages/create-clip-wallet/README.md @@ -2,7 +2,7 @@ Start your own non-custodial wallet on the [Clip Wallet](https://github.com/ColdAI-org/clip-wallet) kit, on every platform, from one `clip.config.ts`: a browser extension, a desktop app for macOS, Windows and Linux, and a phone app for -iOS and Android, for 14 network families, plus (if you want it) a Scaffold-HBAR dapp that connects to it on Hedera +iOS and Android, for 26 network families, plus (if you want it) a Scaffold-HBAR dapp that connects to it on Hedera testnet. > Pre-release: Clip Wallet runs on test networks only and has had no external audit. diff --git a/packages/engine/package.json b/packages/engine/package.json index 7bb5eae7..e8e080b0 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -168,19 +168,28 @@ "@clip-wallet/1mask": "workspace:*", "@clip-wallet/backup-client": "workspace:*", "@clip-wallet/chains-algorand": "workspace:*", + "@clip-wallet/chains-antelope": "workspace:*", "@clip-wallet/chains-aptos": "workspace:*", "@clip-wallet/chains-bitcoin": "workspace:*", + "@clip-wallet/chains-bitcoincash": "workspace:*", "@clip-wallet/chains-cardano": "workspace:*", + "@clip-wallet/chains-cosmos": "workspace:*", "@clip-wallet/chains-evm": "workspace:*", + "@clip-wallet/chains-fuel": "workspace:*", "@clip-wallet/chains-hedera": "workspace:*", + "@clip-wallet/chains-icp": "workspace:*", + "@clip-wallet/chains-multiversx": "workspace:*", "@clip-wallet/chains-near": "workspace:*", "@clip-wallet/chains-solana": "workspace:*", + "@clip-wallet/chains-stacks": "workspace:*", "@clip-wallet/chains-starknet": "workspace:*", "@clip-wallet/chains-stellar": "workspace:*", "@clip-wallet/chains-substrate": "workspace:*", "@clip-wallet/chains-sui": "workspace:*", "@clip-wallet/chains-tezos": "workspace:*", "@clip-wallet/chains-ton": "workspace:*", + "@clip-wallet/chains-tron": "workspace:*", + "@clip-wallet/chains-xrpl": "workspace:*", "@clip-wallet/config": "workspace:*", "@clip-wallet/core": "workspace:*", "@clip-wallet/features": "workspace:*", diff --git a/packages/engine/src/adapters.ts b/packages/engine/src/adapters.ts index 41f0e990..95b0a084 100644 --- a/packages/engine/src/adapters.ts +++ b/packages/engine/src/adapters.ts @@ -6,7 +6,7 @@ import type { Family, Network } from "@clip-wallet/core"; import { ClipError, knownMsg, recallMsg, msg } from "@clip-wallet/core"; import type { ApprovalPlan, PlanStep, SessionView } from "@clip-wallet/ui"; import type { ClipConfig } from "@clip-wallet/config"; -import { CARDANO_METHODS_ALLOWED, createOneMaskRouter, EVM_METHODS, type OneMaskRouter, type RouterPort } from "@clip-wallet/1mask/background"; +import { CARDANO_METHODS_ALLOWED, createOneMaskRouter, EVM_METHODS, N87_CHAIN_READ, N87_CONNECT_METHODS, type OneMaskRouter, type RouterPort } from "@clip-wallet/1mask/background"; import { P2_CONNECT_METHODS } from "@clip-wallet/1mask/background/p2"; import type { createStarknetModule } from "@clip-wallet/chains-starknet"; import type { createTonModule } from "@clip-wallet/chains-ton"; @@ -26,10 +26,11 @@ const CONNECT_METHODS = new Set([ "substrate_enable", "wallet_requestAccounts", // Starknet (get-starknet) "tonconnect:connect", + ...N87_CONNECT_METHODS, ]); const READ_ONLY = new Set(EVM_METHODS.readOnly); /** Read-only chain calls a module answers without an approval (CIP-30 getUtxos, getBalance, …). */ -const CHAIN_READ = new Set(CARDANO_METHODS_ALLOWED.readOnly); +const CHAIN_READ = new Set([...CARDANO_METHODS_ALLOWED.readOnly, ...N87_CHAIN_READ]); /* ------------------------------------------------------------------ 1Mask */ @@ -215,6 +216,19 @@ const PLAIN_ETA: Record = { stellar: 6, tezos: 10, algorand: 4, + // networks87 + cosmos: 6, + provenance: 6, + thorchain: 6, + initia: 2, + tron: 3, + xrpl: 4, + antelope: 2, + multiversx: 6, + icp: 2, + stacks: 10, + fuel: 2, + bitcoincash: 600, }; /** CLPRouter funding through @clip-wallet/route. */ diff --git a/packages/engine/src/catalog.ts b/packages/engine/src/catalog.ts index 94205887..ca65a00d 100644 --- a/packages/engine/src/catalog.ts +++ b/packages/engine/src/catalog.ts @@ -23,6 +23,16 @@ import { NEAR_NETWORKS, USDC_CONTRACTS, tokenAssetKey as nep141Key } from "@clip import { STELLAR_NETWORKS, USDC_ISSUERS, classicAsset } from "@clip-wallet/chains-stellar/networks"; import { TEZOS_NETWORKS, KNOWN_TOKENS as TEZOS_TOKENS } from "@clip-wallet/chains-tezos/networks"; import { ALGORAND_NETWORKS, ALGORAND_NETS, asaAssetKey } from "@clip-wallet/chains-algorand/networks"; +// networks87 +import { COSMOS_NETWORKS, assetOf as cosmosAssetOf, specOf as cosmosSpecOf } from "@clip-wallet/chains-cosmos/networks"; +import { TRON_NETWORKS, usdtAsset as tronUsdt } from "@clip-wallet/chains-tron/networks"; +import { MULTIVERSX_NETWORKS } from "@clip-wallet/chains-multiversx/networks"; +import { ICP_IDS, ICP_NETWORKS, LEDGERS as ICP_LEDGERS, ledgerAsset as icpLedgerAsset } from "@clip-wallet/chains-icp/networks"; +import { STACKS_NETWORKS, specFor as stacksSpecFor, tokenAsset as stacksTokenAsset } from "@clip-wallet/chains-stacks/networks"; +import { BCH_NETWORKS } from "@clip-wallet/chains-bitcoincash/networks"; +import { XRPL_NETWORKS } from "@clip-wallet/chains-xrpl/networks"; +import { ANTELOPE_NETWORKS } from "@clip-wallet/chains-antelope/networks"; +import { FUEL_NETWORKS, assetFor as fuelAssetFor, specFor as fuelSpecFor } from "@clip-wallet/chains-fuel/networks"; /** Same mapping as chains-evm's (unexported) curatedAsset(). */ function curatedAsset(t: CuratedToken): AssetRef { @@ -55,6 +65,15 @@ export function walletNetworks(config: Pick) ...STELLAR_NETWORKS, ...TEZOS_NETWORKS, ...ALGORAND_NETWORKS, + ...COSMOS_NETWORKS, + ...TRON_NETWORKS, + ...MULTIVERSX_NETWORKS, + ...ICP_NETWORKS, + ...STACKS_NETWORKS, + ...FUEL_NETWORKS, + ...BCH_NETWORKS, + ...XRPL_NETWORKS, + ...ANTELOPE_NETWORKS, ]; return all.filter((n) => (families as readonly string[]).includes(n.family) && (mainnet || n.testnet)); } @@ -121,6 +140,19 @@ export function walletAssets(networks: Network[]): AssetRef[] { const id = ALGORAND_NETS[n.testnet ? "testnet" : "mainnet"].usdc; out.push({ key: asaAssetKey(n.id, id), symbol: "USDC", name: "USD Coin", decimals: 6, networkId: n.id, address: id }); } + // networks87: the curated tokens each module lists even at zero. + const cs = cosmosSpecOf(n.id); + if (cs) for (const t of cs.tokens ?? []) if (t.alwaysShow || t.key === "usdc") out.push(cosmosAssetOf(cs, t.denom)); + if (n.family === "tron") { + const u = tronUsdt(n.id); + if (u) out.push(u); + } + if (n.family === "icp") { + const net = n.id === ICP_IDS.mainnet ? "mainnet" : "test"; + for (const l of ICP_LEDGERS[net].slice(1)) out.push(icpLedgerAsset(n.id, l, false)); + } + if (n.family === "stacks") for (const t of stacksSpecFor(n.id)?.tokens ?? []) out.push(stacksTokenAsset(n.id, t.assetId, t)); + if (n.family === "fuel") for (const t of fuelSpecFor(n.id)?.tokens ?? []) out.push(fuelAssetFor(n.id, t.assetId)); if (n.family === "tezos") { for (const t of TEZOS_TOKENS.filter((t) => t.networkId === n.id && t.key === "usdt")) { out.push({ key: t.key, symbol: t.symbol, name: t.name, decimals: t.decimals, networkId: n.id, address: `${t.contract}:${t.tokenId}` }); diff --git a/packages/engine/src/engine.ts b/packages/engine/src/engine.ts index c6c95447..faabcbd2 100644 --- a/packages/engine/src/engine.ts +++ b/packages/engine/src/engine.ts @@ -26,7 +26,7 @@ import type { SessionView, WalletState, } from "@clip-wallet/ui"; -import { CARDANO_METHODS_ALLOWED, type RouterPort } from "@clip-wallet/1mask/background"; +import { CARDANO_METHODS_ALLOWED, COSMOS_FAMILIES, N87_CHAIN_READ, type RouterPort } from "@clip-wallet/1mask/background"; import type { CardanoModule, CardanoReadMethod } from "@clip-wallet/chains-cardano"; import { isFeatureRequest, type FeatureRequest } from "@clip-wallet/features/messages"; import type { FeaturesService } from "@clip-wallet/features"; @@ -704,9 +704,13 @@ export class WalletEngine implements DappHost { for (const asset of this.deps.assets.filter((a) => a.key === assetKey && !a.bridged)) { const network = this.network(asset.networkId); const acct = await this.account(network.family); - const t = byAddress.get(acct.address) ?? { asset, address: acct.address, displayAddress: acct.hederaAccountId, networks: [] }; + // networks87: families whose address is spelled per network (Cosmos prefixes, Stacks SP/ST, CashAddr) or lives + // on chain (Antelope account names) say which one receives here; a plain-words ClipError when none can yet. + const mod = this.deps.chains[network.family]; + const address = mod?.receiveAddress ? await mod.receiveAddress(await this.ctx(network.id)) : acct.address; + const t = byAddress.get(address) ?? { asset, address, displayAddress: acct.hederaAccountId, networks: [] }; t.networks.push(this.networkView(network)); - byAddress.set(acct.address, t); + byAddress.set(address, t); } return [...byAddress.values()]; } @@ -1155,6 +1159,12 @@ export class WalletEngine implements DappHost { } async chainRead(req: DappRequest): Promise { + // networks87: Keplr sendTx / verifyArbitrary on the Cosmos SDK families (chains-cosmos read()). + if ((COSMOS_FAMILIES as readonly string[]).includes(req.family) && N87_CHAIN_READ.includes(req.method)) { + const c = this.deps.chains[req.family] as (ChainModule & { read?(method: string, params: unknown, ctx: ChainContext): Promise }) | undefined; + if (!c || typeof c.read !== "function") throw new ClipError("This request isn't available.", "chain-read/unsupported"); + return c.read(req.method, req.params, await this.ctx(req.networkId, req.origin)); + } const m = this.deps.chains.cardano as CardanoModule | undefined; if (req.family !== "cardano" || !m || typeof m.read !== "function" || !(CARDANO_METHODS_ALLOWED.readOnly as readonly string[]).includes(req.method)) { throw new ClipError("This request isn't available.", "chain-read/unsupported"); diff --git a/packages/engine/src/wiring.ts b/packages/engine/src/wiring.ts index 2f3adebb..b89ed2f4 100644 --- a/packages/engine/src/wiring.ts +++ b/packages/engine/src/wiring.ts @@ -16,6 +16,15 @@ import { createSuiModule } from "@clip-wallet/chains-sui"; import { createAptosModule } from "@clip-wallet/chains-aptos"; import { createCardanoModule } from "@clip-wallet/chains-cardano"; import { createSubstrateModule } from "@clip-wallet/chains-substrate"; +import { createCosmosModule } from "@clip-wallet/chains-cosmos"; +import { createTronModule } from "@clip-wallet/chains-tron"; +import { createMultiversXModule } from "@clip-wallet/chains-multiversx"; +import { createIcpModule } from "@clip-wallet/chains-icp"; +import { createStacksModule } from "@clip-wallet/chains-stacks"; +import { createFuelModule } from "@clip-wallet/chains-fuel"; +import { createBitcoinCashModule } from "@clip-wallet/chains-bitcoincash"; +import { createXrplModule } from "@clip-wallet/chains-xrpl"; +import { createAntelopeModule } from "@clip-wallet/chains-antelope"; import { createStarknetModule } from "@clip-wallet/chains-starknet"; import { createTonModule } from "@clip-wallet/chains-ton"; import { createNearModule } from "@clip-wallet/chains-near"; @@ -56,7 +65,7 @@ export function createEngineDependencies(o: EngineWiringOptions): Dependencies & // Defaults match the vault's: Starknet OpenZeppelin account, TON wallet v5r1, Algorand ARC-52. const starknet = createStarknetModule(); const ton = createTonModule(); - const all: Record ChainModule> = { + const all: Partial ChainModule>> = { evm: createEvmModule, hedera: () => hedera, solana: createSolanaModule, @@ -71,10 +80,25 @@ export function createEngineDependencies(o: EngineWiringOptions): Dependencies & stellar: createStellarModule, tezos: createTezosModule, algorand: createAlgorandModule, + cosmos: () => createCosmosModule({ family: "cosmos" }), + provenance: () => createCosmosModule({ family: "provenance" }), + thorchain: () => createCosmosModule({ family: "thorchain" }), + initia: () => createCosmosModule({ family: "initia" }), + tron: createTronModule, + multiversx: createMultiversXModule, + icp: createIcpModule, + stacks: createStacksModule, + fuel: createFuelModule, + bitcoincash: createBitcoinCashModule, + xrpl: createXrplModule, + antelope: createAntelopeModule, }; // One instance per enabled family for the engine's lifetime (modules keep prepare→finalize state). const chains: Partial> = {}; - for (const f of families) chains[f] = all[f](); + for (const f of families) { + const make = all[f]; + if (make) chains[f] = make(); + } const prices = o.kv ? createPriceFeed(o.kv, o.coingeckoDemoKey) : new ReferencePriceFeed(); const backupUrl = o.config.services.backupUrl; // Phase 3 "settle on Hedera": only with route.settleOnHedera and a known deployment (testnet only). diff --git a/packages/engine/test/engine.test.ts b/packages/engine/test/engine.test.ts index 34a8a74a..13d43308 100644 --- a/packages/engine/test/engine.test.ts +++ b/packages/engine/test/engine.test.ts @@ -132,6 +132,19 @@ describe("WalletEngine: portfolio, send, receive", () => { expect(t).toHaveLength(1); expect(t[0]!.networks).toHaveLength(2); }); + + it("asks the module for the receive address when it spells it per network (networks87 receiveAddress)", async () => { + const vault = new FakeVault(); + const deps = makeDeps(vault); + const evm = deps.chains.evm!; + deps.chains.evm = { ...evm, receiveAddress: async (ctx) => `${ctx.account.address}@${ctx.network.id}` }; + const engine = new WalletEngine(deps, new MemoryKV(), makeEnv()); + engine.start(); + await engine.handle({ type: "createWallet", password: "a long test password" }); + const t = await engine.handle({ type: "getReceiveTargets", assetKey: "eth" }); + expect(t.map((x) => x.address).sort()).toEqual([`${EVM_ADDRESS}@${BASE_SEPOLIA.id}`, `${EVM_ADDRESS}@${SEPOLIA.id}`].sort()); + expect(t.every((x) => x.networks.length === 1)).toBe(true); + }); }); describe("wallet-built origin", () => { diff --git a/packages/engine/test/networks87.test.ts b/packages/engine/test/networks87.test.ts new file mode 100644 index 00000000..cb2f6b47 --- /dev/null +++ b/packages/engine/test/networks87.test.ts @@ -0,0 +1,150 @@ +/** + * networks87: which of the 87 CLPR networks Clip Wallet supports, computed from the code (the catalogue every host + * uses), and the supported-mainnets count by the landing page's rule. docs/r1/networks87.md is the human version. + * + * Source of the 87: coldai-clpr-landing artifacts/coldai-web/src/data/clpr-data.ts NETWORKS (2026-10-06), copied + * here as slugs and names so this test runs inside the wallet repo. + * + * pnpm --filter @clip-wallet/engine exec vitest run test/networks87.test.ts (prints the table) + */ +import { describe, expect, it } from "vitest"; +import type { Network } from "@clip-wallet/core"; +import { NETWORK_FAMILIES } from "@clip-wallet/config"; +import { walletNetworks } from "../src/catalog.js"; + +/** Every family, mainnet on: the full catalogue. */ +const ALL: Network[] = walletNetworks({ + networks: ["evm:*", ...NETWORK_FAMILIES.filter((f) => f !== "evm")], + mainnet: { enabled: true } as never, +}); +const MAINNETS = ALL.filter((n) => !n.testnet); + +type Match = (n: Network) => boolean; +const evm = (chainId: number): Match => (n) => n.family === "evm" && n.chainId === chainId; +const fam = (family: string, name?: RegExp): Match => (n) => n.family === family && (!name || name.test(n.name)); + +/** Not supportable with a self-custodial key wallet: why, in one line (details in docs/r1/networks87.md). */ +const UNSUPPORTED: Record = { + canton: + "Canton parties are hosted by a participant (validator) node: every read and every submission goes through that node's authenticated Ledger API, and there is no public endpoint a wallet can use on its own.", + mixin: + "The Mixin kernel's public RPC has no address index (no balance or UTXO lookup by key), and user accounts live behind Mixin's registered-account API, so a phrase-derived key alone can neither see nor spend funds.", +}; + +/** CLPR network (slug, name) → how Clip covers it. */ +const CLPR: [slug: string, name: string, match: Match | null][] = [ + ["abstract", "Abstract", evm(2741)], + ["algorand", "Algorand", fam("algorand")], + ["anubis", "Anubis", evm(6714)], + ["arbitrum-nova", "Arbitrum Nova", evm(42170)], + ["arbitrum-one", "Arbitrum One", evm(42161)], + ["arc", "Arc", evm(5042)], + ["aurora", "Aurora", evm(1313161554)], + ["avalanche-c-chain", "Avalanche C-Chain", evm(43114)], + ["bnb-smart-chain", "BNB Smart Chain", evm(56)], + ["bob", "BOB", evm(60808)], + ["bot-chain", "BOT Chain", evm(677)], + ["base", "Base", evm(8453)], + ["bifrost-network", "Bifrost Network", evm(3068)], + ["bitcoin", "Bitcoin", fam("bitcoin")], + ["bitcoin-cash", "Bitcoin Cash", fam("bitcoincash")], + ["bittensor", "Bittensor", evm(964)], + ["blast", "Blast", evm(81457)], + ["canton", "Canton", null], + ["cardano", "Cardano", fam("cardano")], + ["celo", "Celo", evm(42220)], + ["chainflip", "Chainflip", fam("substrate", /^Chainflip$/)], + ["conflux", "Conflux", evm(1030)], + ["core", "Core", evm(1116)], + ["cronos", "Cronos", evm(25)], + ["ethereum", "Ethereum", evm(1)], + ["flare", "Flare", evm(14)], + ["fraxtal", "Fraxtal", evm(252)], + ["grx-chain", "GRX Chain", evm(1110)], + ["gnosis-chain", "Gnosis Chain", evm(100)], + ["hydration", "Hydration", evm(222222)], + ["hyperliquid", "Hyperliquid", evm(999)], + ["immutable-zkevm", "Immutable zkEVM", evm(13371)], + ["injective", "Injective", evm(1776)], + ["ink", "Ink", evm(57073)], + ["kub-chain", "KUB Chain", evm(96)], + ["kaia", "Kaia", evm(8217)], + ["katana", "Katana", evm(747474)], + ["kava", "Kava", evm(2222)], + ["mantra", "MANTRA", evm(5888)], + ["mantle", "Mantle", evm(5000)], + ["megaeth", "MegaETH", evm(4326)], + ["mezo", "Mezo", evm(31612)], + ["mixin", "Mixin", null], + ["monad", "Monad", evm(143)], + ["near", "NEAR", fam("near")], + ["op-mainnet", "OP Mainnet", evm(10)], + ["osmosis", "Osmosis", fam("cosmos", /^Osmosis$/)], + ["plasma", "Plasma", evm(9745)], + ["plume", "Plume", evm(98866)], + ["polygon-pos", "Polygon PoS", evm(137)], + ["provenance", "Provenance", fam("provenance")], + ["pulsechain", "PulseChain", evm(369)], + ["rise", "RISE", evm(4153)], + ["reya", "Reya", evm(1729)], + ["robinhood-chain", "Robinhood Chain", evm(4663)], + ["ronin", "Ronin", evm(2020)], + ["rootstock", "Rootstock", evm(30)], + ["strato", "STRATO", evm(123354377739506)], + ["sei", "Sei", evm(1329)], + ["solana", "Solana", fam("solana")], + ["soneium", "Soneium", evm(1868)], + ["stable", "Stable", evm(988)], + ["stacks", "Stacks", fam("stacks")], + ["starknet", "Starknet", fam("starknet")], + ["stellar", "Stellar", fam("stellar")], + ["thorchain", "THORChain", fam("thorchain")], + ["ton", "TON", fam("ton")], + ["tron", "TRON", fam("tron")], + ["telos", "Telos", evm(40)], + ["unichain", "Unichain", evm(130)], + ["vaulta", "Vaulta", fam("antelope", /Vaulta/)], + ["world-chain", "World Chain", evm(480)], + ["x-layer", "X Layer", evm(196)], + ["xpr-network", "XPR Network", fam("antelope", /XPR/)], + ["xrp-ledger", "XRP Ledger", fam("xrpl")], + ["zigchain", "ZIGChain", fam("cosmos", /^ZIGChain$/)], + ["zksync-era", "ZKsync Era", evm(324)], + ["dydx", "dYdX", fam("cosmos", /^dYdX$/)], + ["linea", "Linea", evm(59144)], + ["scroll", "Scroll", evm(534352)], + ["morph", "Morph", evm(2818)], + ["icp", "ICP", fam("icp")], + ["multiversx", "MultiversX", fam("multiversx")], + ["initia", "Initia", fam("initia")], + ["fuel", "Fuel", fam("fuel")], + ["tezos", "Tezos", fam("tezos")], + ["etherlink", "Etherlink", evm(42793)], +]; + +const covered = CLPR.map(([slug, name, match]) => ({ slug, name, nets: match ? MAINNETS.filter(match) : [] })); + +describe("networks87: CLPR coverage from code", () => { + it("lists the 87 CLPR networks once each", () => { + expect(CLPR).toHaveLength(87); + expect(new Set(CLPR.map(([slug]) => slug)).size).toBe(87); + }); + + it("supports every CLPR network on mainnet except the ones with no self-custodial key model", () => { + const table = covered.map((c) => `${c.slug.padEnd(18)} ${c.nets.length ? c.nets.map((n) => `${n.id} (${n.name})`).join(", ") : `UNSUPPORTED: ${UNSUPPORTED[c.slug] ?? "?"}`}`); + console.log(`\nCLPR coverage (mainnets in the catalogue):\n${table.join("\n")}`); + const missing = covered.filter((c) => c.nets.length === 0).map((c) => c.slug); + expect(missing.sort()).toEqual(Object.keys(UNSUPPORTED).sort()); + expect(covered.filter((c) => c.nets.length > 0)).toHaveLength(85); + }); + + it("supported mainnets by the landing page's rule (EVM mainnets without Hedera's EVM, plus every other family's mainnets)", () => { + const evmMainnets = MAINNETS.filter((n) => n.family === "evm" && n.chainId !== 295); + const others = MAINNETS.filter((n) => n.family !== "evm"); + const byFamily = Object.fromEntries(NETWORK_FAMILIES.map((f) => [f, MAINNETS.filter((n) => n.family === f).length])); + console.log(`\nSupported mainnets: ${evmMainnets.length} EVM + ${others.length} others = ${evmMainnets.length + others.length}`, byFamily); + expect(MAINNETS.some((n) => n.chainId === 295)).toBe(false); // Hedera's EVM is request-only, never listed + expect(evmMainnets).toHaveLength(60); + expect(evmMainnets.length + others.length).toBe(93); + }); +}); diff --git a/packages/extension-kit/README.md b/packages/extension-kit/README.md index 4fe8c1ec..4ade23c8 100644 --- a/packages/extension-kit/README.md +++ b/packages/extension-kit/README.md @@ -2,7 +2,7 @@ The Clip Wallet browser extension as a library, for [WXT](https://wxt.dev). A wallet project keeps its identity (`clip.config.ts`, icon), `wxt.config.ts` and one-line entrypoints; the background (vault, approvals, security -checks), the pages, 1Mask for all 14 network families and the build wiring come from here, versioned and signed. +checks), the pages, 1Mask for all 26 network families and the build wiring come from here, versioned and signed. > Pre-release: Clip Wallet runs on test networks only and has had no external audit. diff --git a/packages/extension-kit/package.json b/packages/extension-kit/package.json index e7e8062e..772998c9 100644 --- a/packages/extension-kit/package.json +++ b/packages/extension-kit/package.json @@ -135,19 +135,28 @@ "@clip-wallet/1mask": "workspace:*", "@clip-wallet/backup-client": "workspace:*", "@clip-wallet/chains-algorand": "workspace:*", + "@clip-wallet/chains-antelope": "workspace:*", "@clip-wallet/chains-aptos": "workspace:*", "@clip-wallet/chains-bitcoin": "workspace:*", + "@clip-wallet/chains-bitcoincash": "workspace:*", "@clip-wallet/chains-cardano": "workspace:*", + "@clip-wallet/chains-cosmos": "workspace:*", "@clip-wallet/chains-evm": "workspace:*", + "@clip-wallet/chains-fuel": "workspace:*", "@clip-wallet/chains-hedera": "workspace:*", + "@clip-wallet/chains-icp": "workspace:*", + "@clip-wallet/chains-multiversx": "workspace:*", "@clip-wallet/chains-near": "workspace:*", "@clip-wallet/chains-solana": "workspace:*", + "@clip-wallet/chains-stacks": "workspace:*", "@clip-wallet/chains-starknet": "workspace:*", "@clip-wallet/chains-stellar": "workspace:*", "@clip-wallet/chains-substrate": "workspace:*", "@clip-wallet/chains-sui": "workspace:*", "@clip-wallet/chains-tezos": "workspace:*", "@clip-wallet/chains-ton": "workspace:*", + "@clip-wallet/chains-tron": "workspace:*", + "@clip-wallet/chains-xrpl": "workspace:*", "@clip-wallet/config": "workspace:*", "@clip-wallet/core": "workspace:*", "@clip-wallet/engine": "workspace:*", diff --git a/packages/extension-kit/src/background/mocks/mock-chains.ts b/packages/extension-kit/src/background/mocks/mock-chains.ts index 80b7fa15..562a650f 100644 --- a/packages/extension-kit/src/background/mocks/mock-chains.ts +++ b/packages/extension-kit/src/background/mocks/mock-chains.ts @@ -78,6 +78,19 @@ const ADDRESS_RE: Partial> = { stellar: /^G[A-Z2-7]{55}$/, tezos: /^tz[1-4][1-9A-HJ-NP-Za-km-z]{33}$/, algorand: /^[A-Z2-7]{58}$/, + // networks87 + cosmos: /^(osmo|dydx|zig|cosmos)1[02-9ac-hj-np-z]{38}$/, + provenance: /^(pb|tp)1[02-9ac-hj-np-z]{38}$/, + thorchain: /^s?thor1[02-9ac-hj-np-z]{38}$/, + initia: /^init1[02-9ac-hj-np-z]{38}$/, + tron: /^T[1-9A-HJ-NP-Za-km-z]{33}$/, + xrpl: /^r[1-9A-HJ-NP-Za-km-z]{24,34}$/, + antelope: /^([a-z1-5.]{1,12}|PUB_K1_[1-9A-HJ-NP-Za-km-z]{50})$/, + multiversx: /^erd1[02-9ac-hj-np-z]{58}$/, + icp: /^([a-z2-7]{5}-){10}[a-z2-7]{3}$|^[0-9a-f]{64}$/, + stacks: /^S[PT][0-9A-HJKMNP-TV-Z]{38,39}$/, + fuel: /^0x[0-9a-fA-F]{64}$/, + bitcoincash: /^(bitcoincash|bchtest):[qp][02-9ac-hj-np-z]{41}$/, }; const SCHEME: Partial> = { @@ -95,6 +108,18 @@ const SCHEME: Partial> = { stellar: "ed25519", tezos: "ed25519", algorand: "ed25519", + cosmos: "ecdsa-secp256k1", + provenance: "ecdsa-secp256k1", + thorchain: "ecdsa-secp256k1", + initia: "ecdsa-secp256k1", + tron: "ecdsa-secp256k1", + xrpl: "ecdsa-secp256k1", + antelope: "ecdsa-secp256k1", + multiversx: "ed25519", + icp: "ecdsa-secp256k1", + stacks: "ecdsa-secp256k1", + fuel: "ecdsa-secp256k1", + bitcoincash: "ecdsa-secp256k1", }; const PATHS: Partial string>> = { @@ -112,6 +137,18 @@ const PATHS: Partial string>> = { stellar: (i) => `m/44'/148'/${i}'`, tezos: (i) => `m/44'/1729'/${i}'/0'`, algorand: (i) => `m/44'/283'/${i}'/0/0`, + cosmos: (i) => `m/44'/118'/0'/0/${i}`, + provenance: (i) => `m/44'/505'/0'/0/${i}`, + thorchain: (i) => `m/44'/931'/0'/0/${i}`, + initia: (i) => `m/44'/60'/0'/0/${i}`, + tron: (i) => `m/44'/195'/0'/0/${i}`, + xrpl: (i) => `m/44'/144'/${i}'/0/0`, + antelope: (i) => `m/44'/194'/0'/0/${i}`, + multiversx: (i) => `m/44'/508'/0'/0'/${i}'`, + icp: (i) => `m/44'/223'/0'/0/${i}`, + stacks: (i) => `m/44'/5757'/0'/0/${i}`, + fuel: (i) => `m/44'/1179993420'/${i}'/0/0`, + bitcoincash: (i) => `m/44'/145'/0'/0/${i}`, }; const CURVE: Record = { @@ -129,6 +166,18 @@ const CURVE: Record = { stellar: "ed25519", tezos: "ed25519", algorand: "bip32-ed25519", + cosmos: "secp256k1", + provenance: "secp256k1", + thorchain: "secp256k1", + initia: "secp256k1", + tron: "secp256k1", + xrpl: "secp256k1", + antelope: "secp256k1", + multiversx: "ed25519", + icp: "secp256k1", + stacks: "secp256k1", + fuel: "secp256k1", + bitcoincash: "secp256k1", }; /** Mock-only Hedera account ids for EVM aliases (real module asks the mirror node). */ diff --git a/packages/extension-kit/src/background/real.ts b/packages/extension-kit/src/background/real.ts index 036f30a5..80092fde 100644 --- a/packages/extension-kit/src/background/real.ts +++ b/packages/extension-kit/src/background/real.ts @@ -5,7 +5,7 @@ import type { Family, Network, Warning } from "@clip-wallet/core"; import { ClipError, knownMsg, recallMsg, msg } from "@clip-wallet/core"; import type { ApprovalPlan, PlanStep, SessionView } from "@clip-wallet/ui"; import { rdnsDomain, type ClipConfig } from "@clip-wallet/config"; -import { CARDANO_METHODS_ALLOWED, createOneMaskRouter, EVM_METHODS, type OneMaskRouter, type RouterPort } from "@clip-wallet/1mask/background"; +import { CARDANO_METHODS_ALLOWED, createOneMaskRouter, EVM_METHODS, N87_CHAIN_READ, N87_CONNECT_METHODS, type OneMaskRouter, type RouterPort } from "@clip-wallet/1mask/background"; import type { createStarknetModule } from "@clip-wallet/chains-starknet"; import type { createTonModule } from "@clip-wallet/chains-ton"; import { P2_CONNECT_METHODS, type BeaconRelay } from "@clip-wallet/1mask/background/p2"; @@ -25,9 +25,10 @@ const CONNECT_METHODS = new Set([ "substrate_enable", "wallet_requestAccounts", // Starknet (get-starknet) "tonconnect:connect", + ...N87_CONNECT_METHODS, ]); /** Read-only chain calls a module answers without an approval (CIP-30 getUtxos, getBalance, …). */ -const CHAIN_READ = new Set(CARDANO_METHODS_ALLOWED.readOnly); +const CHAIN_READ = new Set([...CARDANO_METHODS_ALLOWED.readOnly, ...N87_CHAIN_READ]); const READ_ONLY = new Set(EVM_METHODS.readOnly); /* ------------------------------------------------------------------ 1Mask */ @@ -233,6 +234,19 @@ const PLAIN_ETA: Record = { stellar: 6, tezos: 10, algorand: 4, + // networks87 + cosmos: 6, + provenance: 6, + thorchain: 6, + initia: 2, + tron: 3, + xrpl: 4, + antelope: 2, + multiversx: 6, + icp: 2, + stacks: 10, + fuel: 2, + bitcoincash: 600, }; /** CLPRouter funding through @clip-wallet/route. Phase 1 routes pay on Hedera from EVM networks. */ diff --git a/packages/extension-kit/src/background/service.ts b/packages/extension-kit/src/background/service.ts index 31ea12b1..d064b521 100644 --- a/packages/extension-kit/src/background/service.ts +++ b/packages/extension-kit/src/background/service.ts @@ -24,7 +24,7 @@ import type { Request, ResponseMap } from "../shared/messages"; import type { KV } from "../shared/storage"; import type { DappHost, Dependencies, PermissionStoreLike } from "./wiring"; import type { CardanoModule, CardanoReadMethod } from "@clip-wallet/chains-cardano"; -import { CARDANO_METHODS_ALLOWED } from "@clip-wallet/1mask/background"; +import { CARDANO_METHODS_ALLOWED, COSMOS_FAMILIES, N87_CHAIN_READ } from "@clip-wallet/1mask/background"; import type { LazyChainModule } from "./wiring"; import { PasskeyCeremonies, type CeremonyMeta } from "./passkey-proxy"; import { PlatformService, type PlatformRequest } from "./platform"; @@ -741,9 +741,13 @@ export class WalletService implements DappHost { for (const asset of this.deps.assets.filter((a) => a.key === assetKey && !a.bridged)) { const network = this.network(asset.networkId); const acct = await this.account(network.family); - const t = byAddress.get(acct.address) ?? { asset, address: acct.address, displayAddress: acct.hederaAccountId, networks: [] }; + // networks87: families whose address is spelled per network (Cosmos prefixes, Stacks SP/ST, CashAddr) or lives + // on chain (Antelope account names) say which one receives here; a plain-words ClipError when none can yet. + const mod = this.deps.chains[network.family]; + const address = mod?.receiveAddress ? await mod.receiveAddress(await this.ctx(network.id)) : acct.address; + const t = byAddress.get(address) ?? { asset, address, displayAddress: acct.hederaAccountId, networks: [] }; t.networks.push(this.networkView(network)); - byAddress.set(acct.address, t); + byAddress.set(address, t); } return [...byAddress.values()]; } @@ -1282,6 +1286,14 @@ export class WalletService implements DappHost { } async chainRead(req: DappRequest): Promise { + // networks87: Keplr sendTx / verifyArbitrary on the Cosmos SDK families (chains-cosmos read()). + if ((COSMOS_FAMILIES as readonly string[]).includes(req.family) && N87_CHAIN_READ.includes(req.method)) { + type Readable = ChainModule & { read?(method: string, params: unknown, ctx: ChainContext): Promise }; + const e = this.deps.chains[req.family] as (Readable | LazyChainModule) | undefined; + const c = e && "load" in e ? await e.load() : e; + if (!c || typeof c.read !== "function") throw new ClipError("This request isn't available.", "chain-read/unsupported"); + return c.read(req.method, req.params, await this.ctx(req.networkId, req.origin)); + } const entry = this.deps.chains.cardano as (CardanoModule | LazyChainModule) | undefined; if (req.family !== "cardano" || !entry || !(CARDANO_METHODS_ALLOWED.readOnly as readonly string[]).includes(req.method)) { throw new ClipError("This request isn't available.", "chain-read/unsupported"); diff --git a/packages/extension-kit/src/background/wiring.ts b/packages/extension-kit/src/background/wiring.ts index d36548ee..30fef1d7 100644 --- a/packages/extension-kit/src/background/wiring.ts +++ b/packages/extension-kit/src/background/wiring.ts @@ -4,7 +4,7 @@ * dependency real (default build) fixture mode (CLIP_MOCKS=1) * ----------------- ---------------------------------------------------- --------------------------- * vault @clip-wallet/vault ClipVault same (always real) - * chain modules @clip-wallet/chains-* (all 14 families) mocks/mock-chains.ts + * chain modules @clip-wallet/chains-* (all 26 families) mocks/mock-chains.ts * networks/assets chain packages via shared/catalog.ts + clip.config mocks/networks.ts * 1Mask (injected) @clip-wallet/1mask/background router (real.ts) same router over the fixture networks * WalletConnect @clip-wallet/1mask/walletconnect (real.ts) mocks/mock-dapps.ts @@ -264,6 +264,10 @@ export function lazyChain(family: Family, curve: ChainMod prepare: async (r, ctx, id) => (await load()).prepare(r, ctx, id), finalize: async (r, s, ctx) => (await load()).finalize(r, s, ctx), buildTransfer: async (x, ctx) => (await load()).buildTransfer(x, ctx), + receiveAddress: async (ctx) => { + const m = await load(); + return m.receiveAddress ? m.receiveAddress(ctx) : ctx.account.address; + }, }; } @@ -340,6 +344,19 @@ export function createDependencies(opts: WiringOptions): Dependencies { substrate: lazyChain("substrate", "sr25519", () => import("@clip-wallet/chains-substrate").then((m) => m.createSubstrateModule())), starknet, ton, + // networks87: each loads on first use like the Phase 2 families. + cosmos: lazyChain("cosmos", "secp256k1", () => import("@clip-wallet/chains-cosmos").then((m) => m.createCosmosModule({ family: "cosmos" }))), + provenance: lazyChain("provenance", "secp256k1", () => import("@clip-wallet/chains-cosmos").then((m) => m.createCosmosModule({ family: "provenance" }))), + thorchain: lazyChain("thorchain", "secp256k1", () => import("@clip-wallet/chains-cosmos").then((m) => m.createCosmosModule({ family: "thorchain" }))), + initia: lazyChain("initia", "secp256k1", () => import("@clip-wallet/chains-cosmos").then((m) => m.createCosmosModule({ family: "initia" }))), + tron: lazyChain("tron", "secp256k1", () => import("@clip-wallet/chains-tron").then((m) => m.createTronModule())), + multiversx: lazyChain("multiversx", "ed25519", () => import("@clip-wallet/chains-multiversx").then((m) => m.createMultiversXModule())), + icp: lazyChain("icp", "secp256k1", () => import("@clip-wallet/chains-icp").then((m) => m.createIcpModule())), + stacks: lazyChain("stacks", "secp256k1", () => import("@clip-wallet/chains-stacks").then((m) => m.createStacksModule())), + fuel: lazyChain("fuel", "secp256k1", () => import("@clip-wallet/chains-fuel").then((m) => m.createFuelModule())), + xrpl: lazyChain("xrpl", "secp256k1", () => import("@clip-wallet/chains-xrpl").then((m) => m.createXrplModule())), + antelope: lazyChain("antelope", "secp256k1", () => import("@clip-wallet/chains-antelope").then((m) => m.createAntelopeModule())), + bitcoincash: lazyChain("bitcoincash", "secp256k1", () => import("@clip-wallet/chains-bitcoincash").then((m) => m.createBitcoinCashModule())), }; const prices = createPriceFeed(opts.kv, opts.features?.coingeckoDemoKey); const eager = { evm: createEvmModule(), hedera, solana: createSolanaModule(), bitcoin: createBitcoinModule() }; diff --git a/packages/extension-kit/test/catalog.test.ts b/packages/extension-kit/test/catalog.test.ts index 8e73263f..eb468af7 100644 --- a/packages/extension-kit/test/catalog.test.ts +++ b/packages/extension-kit/test/catalog.test.ts @@ -9,7 +9,8 @@ describe("catalog (real chain packages)", () => { const nets = walletNetworks(config); expect(nets.length).toBeGreaterThan(4); expect(nets.every((n) => n.testnet)).toBe(true); - expect(new Set(nets.map((n) => n.family))).toEqual(new Set(enabledFamilies(config))); + // THORChain has no public test network (its stagenet runs on real funds), so it only appears with mainnet on. + expect(new Set(nets.map((n) => n.family))).toEqual(new Set(enabledFamilies(config).filter((f) => f !== "thorchain"))); const onlyBase = walletNetworks(defineConfig({ name: "X", rdns: "com.x.wallet", networks: ["evm:84532", "hedera"] })); expect(onlyBase.map((n) => n.id)).toEqual(["eip155:84532", "hedera:testnet"]); }); diff --git a/packages/extension-kit/test/clip.config.ts b/packages/extension-kit/test/clip.config.ts index ea579f7b..6af478f2 100644 --- a/packages/extension-kit/test/clip.config.ts +++ b/packages/extension-kit/test/clip.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ // ColdAI orange; white text on orange buttons is the owner's preference (contrast 3.6:1 ≥ the schema's 3:1). theme: { accent: "#FF3C00", accentText: "#FFFFFF", font: "Inter", radius: 14 }, // All 14 families (testnets; mainnet stays gated below). - networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand"], + networks: ["evm:*", "hedera", "solana", "bitcoin", "sui", "aptos", "cardano", "substrate", "starknet", "ton", "near", "stellar", "tezos", "algorand", "cosmos", "provenance", "thorchain", "initia", "tron", "xrpl", "antelope", "multiversx", "icp", "stacks", "fuel", "bitcoincash"], // rpOrigin unset: the extension's own origin is the WebAuthn RP (Chrome 122+). Set an https origin you // own (and add it to host_permissions) to keep passkeys stable across extension ids and browsers. passkeys: { enabled: true }, diff --git a/packages/extension-kit/test/derivation-paths.test.ts b/packages/extension-kit/test/derivation-paths.test.ts index 04175ad4..b36459cf 100644 --- a/packages/extension-kit/test/derivation-paths.test.ts +++ b/packages/extension-kit/test/derivation-paths.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { Family } from "@clip-wallet/core"; +import { FAMILIES, type Family } from "@clip-wallet/core"; import config from "./clip.config"; import { FAST_ARGON2 } from "./helpers"; import { MemoryKV } from "../src/shared/storage"; @@ -17,7 +17,7 @@ describe("chain module derivation paths match the vault's accounts", () => { await deps.loadChains(); await deps.vault.create("a long test password"); const families = Object.keys(deps.chains).sort() as Family[]; - expect(families).toHaveLength(14); + expect(families).toHaveLength(FAMILIES.length); for (const family of families) { const mod = deps.chains[family]!; for (const i of [0, 1]) { diff --git a/packages/extension-kit/test/lazy-chains.test.ts b/packages/extension-kit/test/lazy-chains.test.ts index 540b50bd..23f4d6e3 100644 --- a/packages/extension-kit/test/lazy-chains.test.ts +++ b/packages/extension-kit/test/lazy-chains.test.ts @@ -4,13 +4,16 @@ import { MemoryKV } from "../src/shared/storage"; import { createDependencies, type LazyChainModule } from "../src/background/wiring"; describe("lazily loaded chain modules", () => { - it("cover all 14 families; the Phase 2 ones match their real module once loaded", async () => { + it("cover all 26 families; the lazy ones match their real module once loaded", async () => { const deps = createDependencies({ kv: new MemoryKV(), mocks: false, config, iconUrl: "x", currency: async () => "USD" }); expect(Object.keys(deps.chains).sort()).toEqual( - ["algorand", "aptos", "bitcoin", "cardano", "evm", "hedera", "near", "solana", "starknet", "stellar", "substrate", "sui", "tezos", "ton"], + [ + "algorand", "antelope", "aptos", "bitcoin", "bitcoincash", "cardano", "cosmos", "evm", "fuel", "hedera", "icp", "initia", "multiversx", + "near", "provenance", "solana", "stacks", "starknet", "stellar", "substrate", "sui", "tezos", "thorchain", "ton", "tron", "xrpl", + ], ); const lazy = Object.values(deps.chains).filter((m): m is LazyChainModule => "load" in m!); - expect(lazy).toHaveLength(10); + expect(lazy).toHaveLength(22); // Synchronous members refuse plainly until the module is loaded. expect(() => deps.chains.sui!.isAddress("0x1")).toThrow(/still loading/); await deps.loadChains(); diff --git a/packages/features/src/prices/coingecko.ts b/packages/features/src/prices/coingecko.ts index 3cddaab4..ec6f9961 100644 --- a/packages/features/src/prices/coingecko.ts +++ b/packages/features/src/prices/coingecko.ts @@ -1,5 +1,12 @@ import { COINGECKO_IDS, PEGGED_USD, TESTNET_ALIASES } from "./ids.js"; +/** `url` without trailing slashes (a scan, not a regex: linear on any input). */ +function withoutTrailingSlashes(url: string): string { + let end = url.length; + while (end > 0 && url[end - 1] === "/") end--; + return url.slice(0, end); +} + /** The background's PriceFeed seam (packages/extension-kit/src/background/wiring.ts): synchronous reads. */ export interface SyncPriceFeed { usd(assetKey: string): number | undefined; @@ -109,7 +116,7 @@ export class CoinGeckoPriceFeed implements SyncPriceFeed { private async fetchAll(): Promise { const ids = [...new Set(Object.values(this.ids))].sort(); - const base = (this.opts.baseUrl ?? "https://api.coingecko.com/api/v3").replace(/\/+$/, ""); + const base = withoutTrailingSlashes(this.opts.baseUrl ?? "https://api.coingecko.com/api/v3"); const url = `${base}/simple/price?ids=${encodeURIComponent(ids.join(","))}&vs_currencies=${FIAT.join(",")}`; const headers: Record = { accept: "application/json" }; if (this.opts.demoApiKey) headers["x-cg-demo-api-key"] = this.opts.demoApiKey; diff --git a/packages/route/src/client.ts b/packages/route/src/client.ts index f0987f35..752cce41 100644 --- a/packages/route/src/client.ts +++ b/packages/route/src/client.ts @@ -51,7 +51,11 @@ interface QuoteInternals { const internals = new WeakMap(); function shortName(l: Ledger | undefined, fallback: string): string { - return l ? l.name.replace(/\s*\(.*\)\s*$/, "") : fallback; + if (!l) return fallback; + // "Name (detail)" -> "Name": the first "(" when the name ends in ")" (a scan, not a regex: linear on any input). + const t = l.name.trimEnd(); + const open = t.endsWith(")") ? t.indexOf("(") : -1; + return open >= 0 ? t.slice(0, open).trimEnd() : l.name; } /** 0.0.N (shard 0, realm 0) -> long-zero EVM address; EVM addresses pass through checksummed. */ diff --git a/packages/route/src/settle-reader.ts b/packages/route/src/settle-reader.ts index 685c596c..ea340144 100644 --- a/packages/route/src/settle-reader.ts +++ b/packages/route/src/settle-reader.ts @@ -1,6 +1,7 @@ import { ClipError } from "@clip-wallet/core"; import { hexToNumber, isHex } from "viem"; import type { Hex } from "viem"; +import { withoutTrailingSlashes } from "./url.js"; /** * Reads from Hedera for the settle client: contract calls, Hedera's clock and order-book logs. @@ -56,7 +57,8 @@ export class MirrorNodeReader implements HederaReader { private readonly f: typeof fetch; constructor(baseUrl: string, fetchImpl?: typeof fetch) { - this.base = baseUrl.replace(/\/+$/, "").replace(/\/api\/v1$/, ""); + const trimmed = withoutTrailingSlashes(baseUrl); + this.base = trimmed.endsWith("/api/v1") ? trimmed.slice(0, -"/api/v1".length) : trimmed; this.f = fetchImpl ?? globalThis.fetch.bind(globalThis); } diff --git a/packages/route/src/settle.ts b/packages/route/src/settle.ts index 11efcdf0..40c2822d 100644 --- a/packages/route/src/settle.ts +++ b/packages/route/src/settle.ts @@ -30,6 +30,8 @@ import { type SettleQuote, } from "./settle-quote.js"; import { JsonRpcReader, MIRROR_LOG_WINDOW_S, MirrorNodeReader, type HederaReader } from "./settle-reader.js"; +import { withoutTrailingSlashes } from "./url.js"; + /** * "Settle on Hedera" client (Phase 3). Reads the order book on Hedera, asks Connectors for signed quotes and @@ -276,7 +278,7 @@ export class SettleClient implements SettleOnHederaClient { }); try { const res = await Promise.race([ - this.fetchImpl(`${c.url.replace(/\/+$/, "")}/quote`, { + this.fetchImpl(`${withoutTrailingSlashes(c.url)}/quote`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify(body), diff --git a/packages/route/src/track.ts b/packages/route/src/track.ts index 3b5c95c1..76bcd6e2 100644 --- a/packages/route/src/track.ts +++ b/packages/route/src/track.ts @@ -1,4 +1,5 @@ import { ClipError } from "@clip-wallet/core"; +import { withoutTrailingSlashes } from "./url.js"; /** * Client for the CLPRouter route status API (clprouter services/, `GET /routes/{routeId}`, schema RouteStatus in @@ -27,7 +28,7 @@ export class RouteStatusClient { private readonly fetchImpl: typeof fetch; constructor(opts: { baseUrl: string; fetch?: typeof fetch }) { - this.baseUrl = opts.baseUrl.replace(/\/+$/, ""); + this.baseUrl = withoutTrailingSlashes(opts.baseUrl); this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis); } diff --git a/packages/route/src/url.ts b/packages/route/src/url.ts new file mode 100644 index 00000000..d5dc3648 --- /dev/null +++ b/packages/route/src/url.ts @@ -0,0 +1,6 @@ +/** `url` without trailing slashes (a scan, not a regex: linear on any input). */ +export function withoutTrailingSlashes(url: string): string { + let end = url.length; + while (end > 0 && url[end - 1] === "/") end--; + return url.slice(0, end); +} diff --git a/packages/ui/src/platform/client.ts b/packages/ui/src/platform/client.ts index 676b1b76..201f37f2 100644 --- a/packages/ui/src/platform/client.ts +++ b/packages/ui/src/platform/client.ts @@ -124,6 +124,19 @@ export const FAMILY_LABEL: Partial> = { stellar: "Stellar", tezos: "Tezos", algorand: "Algorand", + // networks87: bare network names (never translated), one key per family. + cosmos: "Osmosis, dYdX, ZIGChain", + provenance: "Provenance", + thorchain: "THORChain", + initia: "Initia", + tron: "TRON", + xrpl: "XRP Ledger", + antelope: "Vaulta, Telos, XPR Network", + multiversx: "MultiversX", + icp: "Internet Computer", + stacks: "Stacks", + fuel: "Fuel", + bitcoincash: "Bitcoin Cash", }; /** Catalog ids for the FAMILY_LABEL entries that carry words or a ticker list; the others are bare network names. */ diff --git a/packages/vault/README.md b/packages/vault/README.md index ee4ae883..b4aae63b 100644 --- a/packages/vault/README.md +++ b/packages/vault/README.md @@ -79,11 +79,29 @@ sr25519 is `@scure/sr25519`; the Stark curve is `@scure/starknet`. `i` is the ac | cardano | BIP32-Ed25519, CIP-3 Icarus | payment `m/1852'/1815'/i'/0/0`, stake `…/i'/2/0` | CIP-19 base address `addr_test…` / `addr…` | Eternl, Lace, Yoroi, cardano-serialization-lib | | substrate | sr25519 | account 0 = root; `i ≥ 1` = `//(i-1)` | SS58 generic prefix 42 | polkadot.js, Talisman, SubWallet (root) | | starknet | Stark (grindKey) | `argent-x:m/44'/9004'/0'/0/i` | OpenZeppelin counterfactual address (see below) | Argent X; `starknetScheme: "braavos"` / `"ledger"` | +| cosmos | secp256k1 | `m/44'/118'/0'/0/i` | bech32 of RIPEMD-160(SHA-256(pk)); vault writes `cosmos1…`, modules re-prefix (osmo, dydx, zig) | Keplr, Leap, Cosmostation (chain-registry slip44 118) | +| provenance | secp256k1 | `m/44'/505'/0'/0/i` | bech32 `pb1…` (`tp1…` testnet) | Keplr, Leap (slip44 505) | +| thorchain | secp256k1 | `m/44'/931'/0'/0/i` | bech32 `thor1…` | Keplr, Ctrl (XDEFI), Vultisig (slip44 931) | +| initia | secp256k1 (ethsecp256k1) | `m/44'/60'/0'/0/i` (the EVM key) | bech32 `init1…` of keccak256(pk)[12..] | Initia Wallet, Keplr (slip44 60) | +| tron | secp256k1 | `m/44'/195'/0'/0/i` | base58check `T…` of 0x41 ‖ keccak256(pk)[12..] | TronLink, TronWeb `fromMnemonic` | +| xrpl | secp256k1 | `m/44'/144'/i'/0/0` | classic `r…` (Ripple base58check of RIPEMD-160(SHA-256(pk))) | xrpl.js `Wallet.fromMnemonic` (i = 0), Ledger Live | +| antelope | secp256k1 (K1) | `m/44'/194'/0'/0/i` | the key, `PUB_K1_…` (accounts are names on chain) | TokenPocket; one key for Vaulta, Telos and XPR Network | +| multiversx | ed25519 (SLIP-10) | `m/44'/508'/0'/0'/i'` | bech32 `erd1…` of pk | xPortal, DeFi Wallet, sdk-core `Mnemonic.deriveKey(i)` | +| icp | secp256k1 | `m/44'/223'/0'/0/i` | self-authenticating principal (SHA-224(DER pk) ‖ 0x02) | Plug, dfx `identity import`, `@dfinity/identity-secp256k1` | +| stacks | secp256k1 | `m/44'/5757'/0'/0/i` | c32check `SP…` (`ST…` testnet) | Leather, Xverse, `@stacks/wallet-sdk` | +| fuel | secp256k1 | `m/44'/1179993420'/i'/0/0` | `0x` SHA-256(uncompressed pk) with the fuels-ts checksum | Fuel Wallet, fuels-ts | +| bitcoincash | secp256k1 | `m/44'/145'/0'/0/i` | CashAddr P2PKH `bitcoincash:q…` (`bchtest:q…`) | Electron Cash, Paytaca, Bitcoin.com Wallet | Network-dependent encodings default to **testnet** (AGENTS.md rule 6): `bitcoinNetwork`, `cardanoNetwork`, `tonNetwork`. Other families' addresses are network-independent; chain modules re-encode where a network needs it (e.g. SS58 prefix 0 for Polkadot). +The networks87 rows (SLIP-44 registry plus the wallets named) are cross-checked for "abandon … about" against each +ecosystem's SDK in `test/families87.test.ts`. Antelope's K1 signatures must be canonical (r and s without a needless +high bit or leading zero, Spring/Leap `is_canonical`): `signEcdsaCanonical` retries with an RFC 6979 extra-entropy +counter, as WharfKit bumps its personalisation, so it stays deterministic. Antelope uses SLIP-44 194 on every Antelope +chain (Telos also has 977 registered, but its wallets don't derive from BIP-39); one key can control accounts on all three. + Sources for each row (checked October 2026): - **Sui:** `DEFAULT_ED25519_DERIVATION_PATH` and the all-hardened path regex in diff --git a/packages/vault/package.json b/packages/vault/package.json index 6406f5e0..0793d444 100644 --- a/packages/vault/package.json +++ b/packages/vault/package.json @@ -69,25 +69,49 @@ "devDependencies": { "@algorandfoundation/xhd-wallet-api": "^1.0.3", "@aptos-labs/ts-sdk": "^7.3.0", + "@bitauth/libauth": "^3.0.0", + "@clip-wallet/chains-antelope": "workspace:*", "@clip-wallet/chains-bitcoin": "workspace:*", + "@clip-wallet/chains-bitcoincash": "workspace:*", + "@clip-wallet/chains-cosmos": "workspace:*", + "@clip-wallet/chains-fuel": "workspace:*", + "@clip-wallet/chains-icp": "workspace:*", + "@clip-wallet/chains-multiversx": "workspace:*", + "@clip-wallet/chains-stacks": "workspace:*", + "@clip-wallet/chains-substrate": "workspace:*", + "@clip-wallet/chains-tron": "workspace:*", + "@clip-wallet/chains-xrpl": "workspace:*", "@clip-wallet/link": "workspace:*", + "@cosmjs/amino": "^0.39.0", + "@cosmjs/crypto": "^0.39.0", + "@dfinity/agent": "^2.4.1", + "@dfinity/candid": "^2.4.1", + "@dfinity/identity-secp256k1": "^2.4.1", + "@dfinity/principal": "^2.4.1", "@emurgo/cardano-serialization-lib-nodejs": "^17.0.0", + "@multiversx/sdk-core": "^16.0.0", "@mysten/sui": "^2.33.2", "@polkadot/keyring": "^14.0.3", "@polkadot/util": "^14.0.3", "@polkadot/util-crypto": "14.0.3", "@scure/btc-signer": "2.4.1", + "@stacks/transactions": "^7.6.0", + "@stacks/wallet-sdk": "^7.6.0", "@stellar/stellar-base": "^15.0.0", "@taquito/signer": "^25.0.0", "@taquito/utils": "^25.0.0", "@ton/core": "^0.63.1", "@ton/crypto": "3.3.0", "@ton/ton": "^16.3.0", + "@wharfkit/antelope": "^4.0.2", "algosdk": "^3.8.0", "ed25519-hd-key": "2.0.0", "ethers": "^6.16.0", + "fuels": "^0.103.0", "micro-key-producer": "^0.10.2", "near-seed-phrase": "^0.2.1", - "starknet": "^10.8.0" + "starknet": "^10.8.0", + "tronweb": "^6.5.1", + "xrpl": "^5.3.0" } } diff --git a/packages/vault/src/address.ts b/packages/vault/src/address.ts index 779857fd..d3e9cf09 100644 --- a/packages/vault/src/address.ts +++ b/packages/vault/src/address.ts @@ -23,6 +23,18 @@ import { type Network2, type TonWalletVersion, } from "./encodings.js"; +import { + antelopePublicKey, + bitcoincashAddress, + cosmosAddress, + fuelAddress, + icpPrincipal, + initiaAddress, + multiversxAddress, + stacksAddress, + tronAddress, + xrplAddress, +} from "./encodings87.js"; export type BitcoinNetwork = "mainnet" | "testnet"; export type BitcoinAddressType = "p2wpkh" | "p2tr"; @@ -120,6 +132,35 @@ export const defaultAddressOf: AddressOf = (family, publicKey, ctx) => { case "starknet": if (!ctx.starknetAccountClassHash) throw new Error("starknet addresses need an account class hash"); return starknetOzAccountAddress(publicKey, ctx.starknetAccountClassHash); + /* + * networks87. Where the format differs per network the vault writes the mainnet form (generic "cosmos" for the + * coin-118 family) and the chain module's receiveAddress() gives each network's own spelling. + */ + case "cosmos": + return cosmosAddress(publicKey, "cosmos"); + case "provenance": + return cosmosAddress(publicKey, "pb"); + case "thorchain": + return cosmosAddress(publicKey, "thor"); + case "initia": + return initiaAddress(publicKey); + case "tron": + return tronAddress(publicKey); + case "xrpl": + return xrplAddress(publicKey); + case "antelope": + // Antelope accounts are names created on chain; until one points at this key, the key is what identifies it. + return antelopePublicKey(publicKey); + case "multiversx": + return multiversxAddress(publicKey); + case "icp": + return icpPrincipal(publicKey); + case "stacks": + return stacksAddress(publicKey, "mainnet"); + case "fuel": + return fuelAddress(publicKey); + case "bitcoincash": + return bitcoincashAddress(publicKey, "mainnet"); default: throw new Error(`addresses for ${String(family)} are not supported`); } diff --git a/packages/vault/src/derive.ts b/packages/vault/src/derive.ts index 0c21e10b..dff89151 100644 --- a/packages/vault/src/derive.ts +++ b/packages/vault/src/derive.ts @@ -29,6 +29,18 @@ export const CURVE_OF: Record = { stellar: "ed25519", tezos: "ed25519", algorand: "bip32-ed25519", // ARC-52 default; "ed25519" with algorandScheme "slip10" (see curveOf) + cosmos: "secp256k1", + provenance: "secp256k1", + thorchain: "secp256k1", + initia: "secp256k1", // ethsecp256k1: same curve, keccak address and digest + tron: "secp256k1", + xrpl: "secp256k1", + antelope: "secp256k1", + multiversx: "ed25519", + icp: "secp256k1", + stacks: "secp256k1", + fuel: "secp256k1", + bitcoincash: "secp256k1", }; /** Which Algorand derivation to follow. See README "Algorand". */ @@ -118,6 +130,31 @@ export function derivationPath(family: Family, index: number, opts: PathOptions return `${accountNodePath("cardano", index)}/0/0`; // CIP-1852 first external (payment) address case "substrate": return substrateJunction(index); + /* networks87: each ecosystem's most used BIP-39 wallet (README "Derivation"). */ + case "cosmos": + return `m/44'/118'/0'/0/${index}`; // Keplr, Leap, Cosmostation: Osmosis, dYdX, ZIGChain (chain-registry slip44 118) + case "provenance": + return `m/44'/505'/0'/0/${index}`; // chain-registry slip44 505 (Keplr, Leap, Provenance Blockchain Wallet) + case "thorchain": + return `m/44'/931'/0'/0/${index}`; // SLIP-44 931 (Keplr, Ctrl/XDEFI, Vultisig) + case "initia": + return `m/44'/60'/0'/0/${index}`; // chain-registry slip44 60, ethsecp256k1 (Initia Wallet, Keplr): the EVM key + case "tron": + return `m/44'/195'/0'/0/${index}`; // TronLink (account i = address index i), TronWeb fromMnemonic + case "xrpl": + return `m/44'/144'/${index}'/0/0`; // xrpl.js Wallet.fromMnemonic (i = 0), Ledger Live, GemWallet + case "antelope": + return `m/44'/194'/0'/0/${index}`; // SLIP-44 194 (EOS/Vaulta), TokenPocket; one key for Vaulta, Telos and XPR + case "multiversx": + return `m/44'/508'/0'/0'/${index}'`; // xPortal, MultiversX DeFi Wallet, sdk-wallet Mnemonic.deriveKey(i) + case "icp": + return `m/44'/223'/0'/0/${index}`; // Plug, dfx identity import, @dfinity/identity-secp256k1 fromSeedPhrase + case "stacks": + return `m/44'/5757'/0'/0/${index}`; // Leather, Xverse (@stacks/wallet-sdk account i) + case "fuel": + return `m/44'/1179993420'/${index}'/0/0`; // Fuel Wallet, fuels-ts WalletManager + case "bitcoincash": + return `m/44'/145'/0'/0/${index}`; // Electron Cash, Paytaca, Bitcoin.com Wallet (P2PKH receive chain) case "starknet": switch (opts.starknetScheme ?? "argent-x") { case "argent-x": diff --git a/packages/vault/src/encodings87.ts b/packages/vault/src/encodings87.ts new file mode 100644 index 00000000..3eb61d09 --- /dev/null +++ b/packages/vault/src/encodings87.ts @@ -0,0 +1,246 @@ +/** + * Address encodings for the networks87 families (Cosmos SDK chains, TRON, XRP Ledger, Antelope, MultiversX, ICP, + * Stacks, Fuel, Bitcoin Cash). Public-key maths only (no secrets), so `Account.address` can be filled without + * importing chain modules. Each is cross-checked against the family's official SDK in test/families87.test.ts. + */ +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { sha224, sha256 } from "@noble/hashes/sha2.js"; +import { ripemd160 } from "@noble/hashes/legacy.js"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { base32nopad, base58, base58xrp, bech32, createBase58check } from "@scure/base"; +import { concat, toHex, utf8 } from "./bytes.js"; + +const hash160 = (b: Uint8Array): Uint8Array => ripemd160(sha256(b)); + +function compressed(publicKey: Uint8Array, what: string): Uint8Array { + if (publicKey.length !== 33 && publicKey.length !== 65) throw new Error(`${what} needs a secp256k1 public key`); + return secp256k1.Point.fromBytes(publicKey).toBytes(true); +} + +function uncompressed(publicKey: Uint8Array, what: string): Uint8Array { + if (publicKey.length !== 33 && publicKey.length !== 65) throw new Error(`${what} needs a secp256k1 public key`); + return secp256k1.Point.fromBytes(publicKey).toBytes(false); +} + +/** The 20 bytes an Ethereum-style key hashes to: keccak256(uncompressed x ‖ y)[12..]. */ +export function ethAddressBytes(publicKey: Uint8Array): Uint8Array { + return keccak_256(uncompressed(publicKey, "ethsecp256k1").subarray(1)).subarray(12); +} + +/* ------------------------------------------------------------------ Cosmos SDK */ + +/** + * Cosmos SDK secp256k1 account: bech32(prefix, RIPEMD-160(SHA-256(compressed key))) (ADR-028 legacy accounts, + * cosmjs `pubkeyToAddress`). Prefixes: cosmos, osmo, dydx, zig, pb / tp (Provenance), thor. + */ +export function cosmosAddress(publicKey: Uint8Array, prefix: string): string { + return bech32.encode(prefix, bech32.toWords(hash160(compressed(publicKey, "cosmos")))); +} + +/** Initia (ethsecp256k1, coin type 60): bech32("init", keccak256(uncompressed)[12..]), the EVM address's bytes. */ +export function initiaAddress(publicKey: Uint8Array, prefix = "init"): string { + return bech32.encode(prefix, bech32.toWords(ethAddressBytes(publicKey))); +} + +/* ------------------------------------------------------------------ TRON */ + +const b58check = createBase58check(sha256); + +/** TRON: base58check(0x41 ‖ keccak256(uncompressed)[12..]), "T…" (https://developers.tron.network/docs/account). */ +export function tronAddress(publicKey: Uint8Array): string { + return b58check.encode(concat(new Uint8Array([0x41]), ethAddressBytes(publicKey))); +} + +/* ------------------------------------------------------------------ XRP Ledger */ + +/** + * XRPL classic address: base58 (Ripple alphabet) check of 0x00 ‖ RIPEMD-160(SHA-256(compressed key)), checksum the + * first 4 bytes of double SHA-256 (https://xrpl.org/docs/concepts/accounts/addresses). + */ +export function xrplAddress(publicKey: Uint8Array): string { + const payload = concat(new Uint8Array([0x00]), hash160(compressed(publicKey, "xrpl"))); + return base58xrp.encode(concat(payload, sha256(sha256(payload)).subarray(0, 4))); +} + +/* ------------------------------------------------------------------ Antelope */ + +/** + * Antelope (Vaulta, Telos, XPR Network) K1 public key in the current format: "PUB_K1_" + base58(key ‖ + * RIPEMD-160(key ‖ "K1")[0..4]) (Spring/Leap fc `public_key_type`, WharfKit `PublicKey.toString()`). + */ +export function antelopePublicKey(publicKey: Uint8Array): string { + const key = compressed(publicKey, "antelope"); + return "PUB_K1_" + base58.encode(concat(key, ripemd160(concat(key, utf8("K1"))).subarray(0, 4))); +} + +/** The legacy "EOS…" spelling of the same key (base58(key ‖ RIPEMD-160(key)[0..4])). */ +export function antelopeLegacyPublicKey(publicKey: Uint8Array, prefix = "EOS"): string { + const key = compressed(publicKey, "antelope"); + return prefix + base58.encode(concat(key, ripemd160(key).subarray(0, 4))); +} + +/* ------------------------------------------------------------------ MultiversX */ + +/** MultiversX: bech32("erd", Ed25519 public key) (https://docs.multiversx.com/developers/data/addresses). */ +export function multiversxAddress(publicKey: Uint8Array): string { + if (publicKey.length !== 32) throw new Error("multiversx needs a 32-byte Ed25519 key"); + return bech32.encode("erd", bech32.toWords(publicKey)); +} + +/* ------------------------------------------------------------------ Internet Computer */ + +/** DER SubjectPublicKeyInfo of an uncompressed secp256k1 key (id-ecPublicKey, secp256k1), as @dfinity/identity-secp256k1. */ +export function icpDerPublicKey(publicKey: Uint8Array): Uint8Array { + const prefix = Uint8Array.from([ + 0x30, 0x56, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, 0x03, 0x42, 0x00, + ]); + return concat(prefix, uncompressed(publicKey, "icp")); +} + +/** Self-authenticating principal bytes: SHA-224(DER key) ‖ 0x02 (IC interface spec, "Principals"). */ +export function icpPrincipalBytes(publicKey: Uint8Array): Uint8Array { + return concat(sha224(icpDerPublicKey(publicKey)), new Uint8Array([0x02])); +} + +/** CRC-32 (IEEE, as zlib). */ +export function crc32(data: Uint8Array): number { + let c = 0xffffffff; + for (const b of data) { + c ^= b; + for (let k = 0; k < 8; k++) c = c & 1 ? (c >>> 1) ^ 0xedb88320 : c >>> 1; + } + return (c ^ 0xffffffff) >>> 0; +} + +const be32 = (n: number) => new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]); + +/** Principal text: base32(CRC-32 ‖ bytes), lower case, groups of 5 joined by "-". */ +export function icpPrincipalText(bytes: Uint8Array): string { + const s = base32nopad.encode(concat(be32(crc32(bytes)), bytes)).toLowerCase(); + return s.match(/.{1,5}/g)!.join("-"); +} + +/** The account's principal as text (what Plug, NNS and the ICRC-1 ledgers show). */ +export function icpPrincipal(publicKey: Uint8Array): string { + return icpPrincipalText(icpPrincipalBytes(publicKey)); +} + +/** ICP ledger account identifier (hex): CRC-32 ‖ SHA-224("\x0Aaccount-id" ‖ principal ‖ subaccount). */ +export function icpAccountIdentifier(principal: Uint8Array, subaccount: Uint8Array = new Uint8Array(32)): string { + if (subaccount.length !== 32) throw new Error("subaccount must be 32 bytes"); + const h = sha224(concat(new Uint8Array([0x0a]), utf8("account-id"), principal, subaccount)); + return toHex(concat(be32(crc32(h)), h)); +} + +/* ------------------------------------------------------------------ Stacks */ + +const C32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + +/** Crockford base32 as c32check uses it (big-endian, leading zero bytes kept as leading "0"s). */ +export function c32encode(data: Uint8Array): string { + let n = 0n; + for (const b of data) n = (n << 8n) | BigInt(b); + let out = ""; + while (n > 0n) { + out = C32[Number(n & 31n)]! + out; + n >>= 5n; + } + let zeros = 0; + while (zeros < data.length && data[zeros] === 0) zeros++; + return "0".repeat(zeros) + out; +} + +/** Stacks address versions: 22 "SP" mainnet single-sig, 26 "ST" testnet single-sig. */ +export const STACKS_VERSION = { mainnet: 22, testnet: 26 } as const; + +/** c32check address: "S" + c32(version) + c32(hash160 ‖ SHA-256²(version ‖ hash160)[0..4]) (SIP-005, c32check). */ +export function c32address(version: number, hash: Uint8Array): string { + if (hash.length !== 20 || version < 0 || version >= 32) throw new Error("bad c32 address input"); + const check = sha256(sha256(concat(new Uint8Array([version]), hash))).subarray(0, 4); + return "S" + C32[version] + c32encode(concat(hash, check)); +} + +/** Stacks single-signature (P2PKH) address of a compressed key. */ +export function stacksAddress(publicKey: Uint8Array, network: "mainnet" | "testnet" = "mainnet"): string { + return c32address(STACKS_VERSION[network], hash160(compressed(publicKey, "stacks"))); +} + +/* ------------------------------------------------------------------ Fuel */ + +/** + * Fuel: the address is SHA-256 of the uncompressed key without its 0x04 prefix (64 bytes), shown as 0x-hex with + * the checksum fuels-ts applies (`Address.toChecksum`: a hex digit is upper case where the matching nibble of + * SHA-256 of the lower-case hex TEXT without "0x" (its UTF-8 bytes) is ≥ 8). + */ +export function fuelAddressBytes(publicKey: Uint8Array): Uint8Array { + return sha256(uncompressed(publicKey, "fuel").subarray(1)); +} + +export function fuelChecksum(hex: string): string { + const lower = hex.toLowerCase().replace(/^0x/, ""); + if (!/^[0-9a-f]{64}$/.test(lower)) throw new Error("fuel addresses are 32 bytes"); + const h = sha256(utf8(lower)); + let out = "0x"; + for (let i = 0; i < 32; i++) { + const byte = h[i]!; + const hi = lower[i * 2]!; + const lo = lower[i * 2 + 1]!; + out += byte >> 4 >= 8 ? hi.toUpperCase() : hi; + out += (byte & 0x0f) >= 8 ? lo.toUpperCase() : lo; + } + return out; +} + +export function fuelAddress(publicKey: Uint8Array): string { + return fuelChecksum(toHex(fuelAddressBytes(publicKey))); +} + +/* ------------------------------------------------------------------ Bitcoin Cash */ + +const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + +function cashPolymod(values: number[]): bigint { + const GEN = [0x98f2bc8e61n, 0x79b76d99e2n, 0xf33e5fb3c4n, 0xae2eabe2a8n, 0x1e4f43e470n]; + let c = 1n; + for (const d of values) { + const c0 = c >> 35n; + c = ((c & 0x07ffffffffn) << 5n) ^ BigInt(d); + for (let i = 0; i < 5; i++) if ((c0 >> BigInt(i)) & 1n) c ^= GEN[i]!; + } + return c ^ 1n; +} + +function toWords5(data: Uint8Array): number[] { + const out: number[] = []; + let acc = 0; + let bits = 0; + for (const b of data) { + acc = (acc << 8) | b; + bits += 8; + while (bits >= 5) { + bits -= 5; + out.push((acc >> bits) & 31); + } + } + if (bits > 0) out.push((acc << (5 - bits)) & 31); + return out; +} + +/** + * CashAddr (https://reference.cash/protocol/blockchain/encoding/cashaddr): prefix ":" + base32(version byte ‖ hash + * ‖ 40-bit BCH checksum). type 0 = P2PKH, 1 = P2SH; 20-byte hashes only. + */ +export function cashAddress(prefix: string, type: 0 | 1, hash: Uint8Array): string { + if (hash.length !== 20) throw new Error("cashaddr hashes are 20 bytes here"); + const payload = toWords5(concat(new Uint8Array([type << 3]), hash)); + const prefixWords = [...prefix].map((ch) => ch.charCodeAt(0) & 31); + const mod = cashPolymod([...prefixWords, 0, ...payload, 0, 0, 0, 0, 0, 0, 0, 0]); + const check: number[] = []; + for (let i = 0; i < 8; i++) check.push(Number((mod >> BigInt(5 * (7 - i))) & 31n)); + return `${prefix}:${[...payload, ...check].map((w) => CHARSET[w]).join("")}`; +} + +/** Bitcoin Cash P2PKH of a compressed key: "bitcoincash:q…" (mainnet) or "bchtest:q…" (testnet / chipnet). */ +export function bitcoincashAddress(publicKey: Uint8Array, network: "mainnet" | "testnet" = "mainnet"): string { + return cashAddress(network === "mainnet" ? "bitcoincash" : "bchtest", 0, hash160(compressed(publicKey, "bitcoincash"))); +} diff --git a/packages/vault/src/index.ts b/packages/vault/src/index.ts index d2a98799..49a0de01 100644 --- a/packages/vault/src/index.ts +++ b/packages/vault/src/index.ts @@ -59,3 +59,28 @@ export { } from "./encodings.js"; export { VaultErrors } from "./errors.js"; export { SYNC_LABEL, SYNC_SIGN_PREFIX, type SyncKeyHandle, type PairingKeyHandle } from "./link.js"; +export { + cosmosAddress, + initiaAddress, + ethAddressBytes, + tronAddress, + xrplAddress, + antelopePublicKey, + antelopeLegacyPublicKey, + multiversxAddress, + icpDerPublicKey, + icpPrincipalBytes, + icpPrincipalText, + icpPrincipal, + icpAccountIdentifier, + c32address, + c32encode, + stacksAddress, + STACKS_VERSION, + fuelAddressBytes, + fuelChecksum, + fuelAddress, + cashAddress, + bitcoincashAddress, + crc32, +} from "./encodings87.js"; diff --git a/packages/vault/src/sign.ts b/packages/vault/src/sign.ts index 04b56375..0ecb63df 100644 --- a/packages/vault/src/sign.ts +++ b/packages/vault/src/sign.ts @@ -19,6 +19,31 @@ export function signEcdsa(digest: Uint8Array, privateKey: Uint8Array): { bytes: return { bytes, recovery }; } +/** + * Antelope (Vaulta, Telos, XPR Network) accepts only "canonical" K1 signatures: neither r nor s may have its top bit + * set or a needless leading zero byte (Spring/Leap fc `is_canonical`; https://github.com/EOSIO/eos/issues/6699). + * Low-S covers s; r needs retries. Like WharfKit's `sign` (which bumps the RFC 6979 personalisation), each retry + * adds a counter as RFC 6979 extra entropy, so the result is still deterministic for a given digest and key. + */ +export function isCanonicalK1(rs: Uint8Array): boolean { + const r = rs.subarray(0, 32); + const s = rs.subarray(32, 64); + return !(r[0]! & 0x80) && !(r[0] === 0 && !(r[1]! & 0x80)) && !(s[0]! & 0x80) && !(s[0] === 0 && !(s[1]! & 0x80)); +} + +export function signEcdsaCanonical(digest: Uint8Array, privateKey: Uint8Array): { bytes: Uint8Array; recovery: number } { + if (digest.length !== 32) throw new Error("ecdsa-secp256k1 signs a 32-byte digest"); + for (let attempt = 0; attempt < 256; attempt++) { + const extraEntropy = attempt === 0 ? false : numTo32(BigInt(attempt)); + const rec = secp256k1.sign(digest, privateKey, { prehash: false, lowS: true, format: "recovered", extraEntropy }); + const recovery = rec[0]!; + const bytes = rec.slice(1); + wipe(rec); + if (isCanonicalK1(bytes)) return { bytes, recovery }; + } + throw new Error("no canonical signature found"); +} + /** * BIP-340 Schnorr. With `taprootTweak` (merkle root, or an EMPTY array for BIP-86 key-path-only spends) * the key is tweaked per BIP-341: d' = (has_even_y(P) ? d : n-d) + H_TapTweak(P_x || merkleRoot). diff --git a/packages/vault/src/vault.ts b/packages/vault/src/vault.ts index 7a746d6b..57f482b2 100644 --- a/packages/vault/src/vault.ts +++ b/packages/vault/src/vault.ts @@ -51,7 +51,7 @@ import { hashWasmArgon2id, type Argon2idFn } from "./kdf.js"; import { VaultErrors } from "./errors.js"; import { passkeyBackup, passkeyWrapKey, type PasskeyPrf } from "./passkey.js"; import { entropyToPhrase, newPhrase, phraseToEntropy, phraseToSeed, type PhraseLength } from "./phrase.js"; -import { signBip32Ed25519, signEcdsa, signEd25519, signSchnorr, signSr25519, signStark } from "./sign.js"; +import { signBip32Ed25519, signEcdsa, signEcdsaCanonical, signEd25519, signSchnorr, signSr25519, signStark } from "./sign.js"; import { systemClock, type Clock, type VaultStorage } from "./storage.js"; import { PairingKeys, TRANSFER_INFO, openTransfer, sealTransfer, syncKeyHandle, type PairingKeyHandle, type SyncKeyHandle } from "./link.js"; @@ -139,6 +139,18 @@ export const FAMILY_SCHEMES: Record = { cardano: ["ed25519"], // BIP32-Ed25519 extended key, verifies as Ed25519 substrate: ["sr25519"], starknet: ["stark-ecdsa"], + cosmos: ["ecdsa-secp256k1"], + provenance: ["ecdsa-secp256k1"], + thorchain: ["ecdsa-secp256k1"], + initia: ["ecdsa-secp256k1"], + tron: ["ecdsa-secp256k1"], + xrpl: ["ecdsa-secp256k1"], + antelope: ["ecdsa-secp256k1"], // canonical K1 signatures only (signEcdsaCanonical) + multiversx: ["ed25519"], + icp: ["ecdsa-secp256k1"], + stacks: ["ecdsa-secp256k1"], + fuel: ["ecdsa-secp256k1"], + bitcoincash: ["ecdsa-secp256k1"], }; /** @@ -496,7 +508,7 @@ export class ClipVault implements Vault { this.touch(); switch (payload.scheme) { case "ecdsa-secp256k1": { - const s = signEcdsa(payload.bytes, key.privateKey); + const s = family === "antelope" ? signEcdsaCanonical(payload.bytes, key.privateKey) : signEcdsa(payload.bytes, key.privateKey); return { scheme: payload.scheme, bytes: s.bytes, recovery: s.recovery, publicKey: toHex(key.publicKey) }; } case "schnorr-secp256k1": { diff --git a/packages/vault/test/families87.test.ts b/packages/vault/test/families87.test.ts new file mode 100644 index 00000000..290730f4 --- /dev/null +++ b/packages/vault/test/families87.test.ts @@ -0,0 +1,315 @@ +/** + * networks87 families: known-answer tests for the public BIP-39 vector "abandon … about", each cross-checked against + * the ecosystem's own official SDK (devDependencies only): cosmjs, xrpl.js, TronWeb, WharfKit, MultiversX sdk-core, + * @dfinity/identity-secp256k1, @stacks/wallet-sdk, fuels-ts and libauth. Literal expected values are what those + * SDKs print for the phrase; the cross-checks re-derive them on every run. + */ +import { beforeAll, describe, expect, it } from "vitest"; +import { mnemonicToEntropy, mnemonicToSeed } from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english.js"; +import { bech32 } from "@scure/base"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { Secp256k1HdWallet } from "@cosmjs/amino"; +import { stringToPath } from "@cosmjs/crypto"; +import { Wallet as XrplWallet } from "xrpl"; +import { TronWeb } from "tronweb"; +import { Bytes, KeyType, PrivateKey as AntelopePrivateKey, PublicKey as AntelopePublicKey, Signature as AntelopeSignature, Checksum256 } from "@wharfkit/antelope"; +import { Mnemonic as MvxMnemonic } from "@multiversx/sdk-core"; +import { Secp256k1KeyIdentity } from "@dfinity/identity-secp256k1"; +import { generateWallet, getStxAddress } from "@stacks/wallet-sdk"; +import { Wallet as FuelWallet } from "fuels"; +import { deriveHdPath, deriveHdPrivateNodeFromSeed, encodeCashAddress, hash160, secp256k1 as libauthSecp } from "@bitauth/libauth"; +import { HDNodeWallet } from "ethers"; +import { toHex } from "../src/bytes.js"; +import { CURVE_OF, deriveFamilyKey, derivationPath, type KeySource } from "../src/derive.js"; +import { defaultAddressOf } from "../src/address.js"; +import { + antelopeLegacyPublicKey, + antelopePublicKey, + bitcoincashAddress, + cashAddress, + cosmosAddress, + fuelAddress, + icpAccountIdentifier, + icpPrincipal, + icpPrincipalBytes, + initiaAddress, + multiversxAddress, + stacksAddress, + tronAddress, + xrplAddress, +} from "../src/encodings87.js"; +import { isCanonicalK1, signEcdsaCanonical } from "../src/sign.js"; +import type { Family } from "@clip-wallet/core"; + +// Public BIP-39 test phrase (official vectors). Never a real wallet. +const ABANDON = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +let src: KeySource; +beforeAll(async () => { + src = { seed: await mnemonicToSeed(ABANDON), entropy: mnemonicToEntropy(ABANDON, wordlist) }; +}); + +const key = (family: Family, i: number) => deriveFamilyKey(src, family, derivationPath(family, i)); +const ctx = { bitcoinNetwork: "testnet", bitcoinAddressType: "p2wpkh" } as const; + +describe("networks87 derivation paths", () => { + it("use each ecosystem's standard SLIP-44 path and curve", () => { + const want: [Family, string, string][] = [ + ["cosmos", "m/44'/118'/0'/0/1", "secp256k1"], + ["provenance", "m/44'/505'/0'/0/1", "secp256k1"], + ["thorchain", "m/44'/931'/0'/0/1", "secp256k1"], + ["initia", "m/44'/60'/0'/0/1", "secp256k1"], + ["tron", "m/44'/195'/0'/0/1", "secp256k1"], + ["xrpl", "m/44'/144'/1'/0/0", "secp256k1"], + ["antelope", "m/44'/194'/0'/0/1", "secp256k1"], + ["multiversx", "m/44'/508'/0'/0'/1'", "ed25519"], + ["icp", "m/44'/223'/0'/0/1", "secp256k1"], + ["stacks", "m/44'/5757'/0'/0/1", "secp256k1"], + ["fuel", "m/44'/1179993420'/1'/0/0", "secp256k1"], + ["bitcoincash", "m/44'/145'/0'/0/1", "secp256k1"], + ]; + for (const [f, path, curve] of want) { + expect(derivationPath(f, 1), f).toBe(path); + expect(CURVE_OF[f], f).toBe(curve); + } + }); +}); + +describe("cosmos SDK (cosmjs Secp256k1HdWallet)", () => { + const cases: [Family, string, string][] = [ + ["cosmos", "cosmos", "118"], + ["cosmos", "osmo", "118"], + ["cosmos", "dydx", "118"], + ["cosmos", "zig", "118"], + ["provenance", "pb", "505"], + ["thorchain", "thor", "931"], + ]; + it.each(cases)("%s %s1… at coin %s matches cosmjs for accounts 0..1", async (family, prefix, coin) => { + for (const i of [0, 1]) { + const w = await Secp256k1HdWallet.fromMnemonic(ABANDON, { hdPaths: [stringToPath(`m/44'/${coin}'/0'/0/${i}`)], prefix }); + const [a] = await w.getAccounts(); + const k = key(family, i); + expect(toHex(k.publicKey)).toBe(toHex(a!.pubkey)); + expect(cosmosAddress(k.publicKey, prefix)).toBe(a!.address); + } + }); + it("published vector: cosmos1… for m/44'/118'/0'/0/0", () => { + expect(defaultAddressOf("cosmos", key("cosmos", 0).publicKey, ctx)).toBe("cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4"); + }); + it("initia is bech32('init') of the EVM address bytes (ethers HDNodeWallet)", () => { + const evm = HDNodeWallet.fromPhrase(ABANDON, undefined, "m/44'/60'/0'/0/0").address; + const bytes = Uint8Array.from(Buffer.from(evm.slice(2), "hex")); + expect(initiaAddress(key("initia", 0).publicKey)).toBe(bech32.encode("init", bech32.toWords(bytes))); + expect(defaultAddressOf("initia", key("initia", 0).publicKey, ctx)).toBe(initiaAddress(key("initia", 0).publicKey)); + }); +}); + +describe("tron (TronWeb)", () => { + it("matches TronWeb.fromMnemonic for accounts 0..2", () => { + for (const i of [0, 1, 2]) { + const t = TronWeb.fromMnemonic(ABANDON, `m/44'/195'/0'/0/${i}`) as { address: string }; + expect(tronAddress(key("tron", i).publicKey)).toBe(t.address); + } + expect(defaultAddressOf("tron", key("tron", 0).publicKey, ctx)).toMatch(/^T[1-9A-HJ-NP-Za-km-z]{33}$/); + }); +}); + +describe("xrpl (xrpl.js Wallet.fromMnemonic)", () => { + it("matches xrpl.js for m/44'/144'/i'/0/0", () => { + for (const i of [0, 1]) { + const w = XrplWallet.fromMnemonic(ABANDON, { derivationPath: `m/44'/144'/${i}'/0/0` }); + const k = key("xrpl", i); + expect(toHex(k.publicKey).toUpperCase()).toBe(w.publicKey); + expect(xrplAddress(k.publicKey)).toBe(w.classicAddress); + } + }); +}); + +describe("antelope (WharfKit)", () => { + it("public key strings match WharfKit for the same private key", () => { + for (const i of [0, 1]) { + const k = key("antelope", i); + const wk = new AntelopePrivateKey(KeyType.K1, Bytes.from(k.privateKey)).toPublic(); + expect(antelopePublicKey(k.publicKey)).toBe(wk.toString()); + expect(antelopeLegacyPublicKey(k.publicKey)).toBe(wk.toLegacyString()); + expect(AntelopePublicKey.from(antelopePublicKey(k.publicKey)).equals(wk)).toBe(true); + } + }); + it("signs canonical K1 signatures that WharfKit verifies and recovers, deterministically", () => { + const k = key("antelope", 0); + const pub = AntelopePublicKey.from(antelopePublicKey(k.publicKey)); + for (let n = 0; n < 40; n++) { + const digest = sha256(new TextEncoder().encode(`clip antelope ${n}`)); + const s = signEcdsaCanonical(digest, k.privateKey); + expect(isCanonicalK1(s.bytes)).toBe(true); + expect(toHex(signEcdsaCanonical(digest, k.privateKey).bytes)).toBe(toHex(s.bytes)); + const sig = AntelopeSignature.from({ type: KeyType.K1, r: s.bytes.slice(0, 32), s: s.bytes.slice(32), recid: s.recovery }); + const d = Checksum256.from(digest); + expect(sig.verifyDigest(d, pub)).toBe(true); + expect(sig.recoverDigest(d).equals(pub)).toBe(true); + // Same digest signed by wharfkit's own key must also be canonical; ours may differ in nonce but must verify. + const plain = new AntelopePrivateKey(KeyType.K1, Bytes.from(k.privateKey)).signDigest(d); + expect(plain.verifyDigest(d, pub)).toBe(true); + } + }); +}); + +describe("multiversx (sdk-core Mnemonic)", () => { + it("matches Mnemonic.deriveKey(i) for accounts 0..2", () => { + const m = MvxMnemonic.fromString(ABANDON); + for (const i of [0, 1, 2]) { + const k = key("multiversx", i); + const pk = m.deriveKey(i).generatePublicKey(); + expect(toHex(k.publicKey)).toBe(pk.hex()); + expect(multiversxAddress(k.publicKey)).toBe(pk.toAddress().toBech32()); + } + }); +}); + +describe("icp (@dfinity/identity-secp256k1)", () => { + it("matches Secp256k1KeyIdentity.fromSeedPhrase (m/44'/223'/0'/0/0)", () => { + const id = Secp256k1KeyIdentity.fromSeedPhrase(ABANDON); + const k = key("icp", 0); + expect(icpPrincipal(k.publicKey)).toBe(id.getPrincipal().toText()); + expect(toHex(icpPrincipalBytes(k.publicKey))).toBe(toHex(id.getPrincipal().toUint8Array())); + expect(icpAccountIdentifier(icpPrincipalBytes(k.publicKey))).toMatch(/^[0-9a-f]{64}$/); + }); + it("account identifier of the anonymous principal matches the IC spec example", () => { + // Anonymous principal 0x04, default subaccount: published in the ICP ledger docs / dfx ledger account-id --of-principal 2vxsx-fae. + expect(icpAccountIdentifier(Uint8Array.from([4]))).toBe("1c7a48ba6a562aa9eaa2481a9049cdf0433b9738c992d698c31d8abf89cadc79"); + }); +}); + +describe("stacks (@stacks/wallet-sdk)", () => { + it("matches generateWallet accounts 0..1 on mainnet and testnet", async () => { + let wallet = await generateWallet({ secretKey: ABANDON, password: "" }); + const { generateNewAccount } = await import("@stacks/wallet-sdk"); + wallet = generateNewAccount(wallet); + for (const i of [0, 1]) { + const k = key("stacks", i); + expect(stacksAddress(k.publicKey, "mainnet")).toBe(getStxAddress(wallet.accounts[i]!, "mainnet")); + expect(stacksAddress(k.publicKey, "testnet")).toBe(getStxAddress(wallet.accounts[i]!, "testnet")); + } + }); +}); + +describe("fuel (fuels-ts Wallet.fromMnemonic)", () => { + it("matches the Fuel Wallet path for accounts 0..1, checksum included", () => { + for (const i of [0, 1]) { + const w = FuelWallet.fromMnemonic(ABANDON, `m/44'/1179993420'/${i}'/0/0`); + const addr = fuelAddress(key("fuel", i).publicKey); + expect(addr.toLowerCase()).toBe(w.address.toB256().toLowerCase()); + expect(addr).toBe(w.address.toString()); + } + }); +}); + +describe("bitcoin cash (libauth)", () => { + it("matches libauth HD derivation + CashAddr on mainnet and testnet", () => { + const node = deriveHdPrivateNodeFromSeed(src.seed, { assumeValidity: true, throwErrors: true } as never) as never; + for (const i of [0, 1]) { + const child = deriveHdPath(node, `m/44'/145'/0'/0/${i}`) as { privateKey: Uint8Array }; + const pub = libauthSecp.derivePublicKeyCompressed(child.privateKey) as Uint8Array; + const k = key("bitcoincash", i); + expect(toHex(k.publicKey)).toBe(toHex(pub)); + const main = encodeCashAddress({ prefix: "bitcoincash", type: "p2pkh", payload: hash160(pub) }); + const test = encodeCashAddress({ prefix: "bchtest", type: "p2pkh", payload: hash160(pub) }); + expect(bitcoincashAddress(k.publicKey, "mainnet")).toBe(typeof main === "string" ? main : (main as { address: string }).address); + expect(bitcoincashAddress(k.publicKey, "testnet")).toBe(typeof test === "string" ? test : (test as { address: string }).address); + } + }); + it("published CashAddr vector (spec test vector, 20-byte P2PKH)", () => { + // https://reference.cash/protocol/blockchain/encoding/cashaddr test vectors: F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9 + const h = Uint8Array.from(Buffer.from("F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9", "hex")); + const r = encodeCashAddress({ prefix: "bitcoincash", type: "p2pkh", payload: h }); + expect(typeof r === "string" ? r : (r as { address: string }).address).toBe("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"); + expect(cashAddress("bitcoincash", 0, h)).toBe("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"); + const t = encodeCashAddress({ prefix: "bchtest", type: "p2pkh", payload: h }); + expect(cashAddress("bchtest", 0, h)).toBe(typeof t === "string" ? t : (t as { address: string }).address); + }); +}); + +describe("ClipVault: the networks87 families through the public API", () => { + const FAST_ARGON2 = { memoryKiB: 256, iterations: 1, parallelism: 1 }; + async function vault() { + const { ClipVault, MemoryStorage } = await import("../src/index.js"); + const v = new ClipVault({ storage: new MemoryStorage(), argon2: FAST_ARGON2, autoLockMs: 0 }); + await v.importPhrase(ABANDON, "correct horse battery staple"); + return v; + } + it("derives every family with the mainnet address forms", async () => { + const v = await vault(); + const addr = async (f: Family) => (await v.deriveAccount(f, 0)).address; + expect(await addr("cosmos")).toBe("cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4"); + expect(await addr("provenance")).toMatch(/^pb1[02-9ac-hj-np-z]{38}$/); + expect(await addr("thorchain")).toMatch(/^thor1[02-9ac-hj-np-z]{38}$/); + expect(await addr("initia")).toMatch(/^init1[02-9ac-hj-np-z]{38}$/); + expect(await addr("tron")).toBe((TronWeb.fromMnemonic(ABANDON, "m/44'/195'/0'/0/0") as { address: string }).address); + expect(await addr("xrpl")).toBe(XrplWallet.fromMnemonic(ABANDON).classicAddress); + expect(await addr("antelope")).toMatch(/^PUB_K1_/); + expect(await addr("multiversx")).toBe(MvxMnemonic.fromString(ABANDON).deriveKey(0).generatePublicKey().toAddress().toBech32()); + expect(await addr("icp")).toBe(Secp256k1KeyIdentity.fromSeedPhrase(ABANDON).getPrincipal().toText()); + expect(await addr("stacks")).toMatch(/^SP[0-9A-Z]{38,39}$/); + expect(await addr("fuel")).toBe(FuelWallet.fromMnemonic(ABANDON).address.toString()); + expect(await addr("bitcoincash")).toMatch(/^bitcoincash:q[02-9ac-hj-np-z]{41}$/); + }); + it("signs only after approval; Antelope signatures are canonical; MultiversX is ed25519", async () => { + const { hashSignablePayload } = await import("../src/index.js"); + const v = await vault(); + for (let n = 0; n < 8; n++) { + const p = { accountId: "antelope:0", scheme: "ecdsa-secp256k1" as const, bytes: sha256(new TextEncoder().encode(`tx ${n}`)) }; + await expect(v.sign({ ...p, approvalId: `none-${n}` })).rejects.toThrow(); + v.registerApproval(`ok-${n}`, [hashSignablePayload(p)], 60_000); + const s = await v.sign({ ...p, approvalId: `ok-${n}` }); + expect(isCanonicalK1(s.bytes)).toBe(true); + } + const p = { accountId: "multiversx:0", scheme: "ed25519" as const, bytes: new TextEncoder().encode("mvx") }; + v.registerApproval("mvx", [hashSignablePayload(p)], 60_000); + const s = await v.sign({ ...p, approvalId: "mvx" }); + expect(s.bytes).toHaveLength(64); + const q = { accountId: "tron:0", scheme: "ed25519" as const, bytes: new TextEncoder().encode("x") }; + v.registerApproval("tron", [hashSignablePayload(q)], 60_000); + await expect(v.sign({ ...q, approvalId: "tron" })).rejects.toThrow(); + }); +}); + +describe("chain modules agree with the vault (paths, curves, addresses per network)", () => { + it("every networks87 module derives where the vault does and spells the vault's key the same way", async () => { + const { createCosmosModule, COSMOS_NETWORKS } = await import("@clip-wallet/chains-cosmos"); + const { createTronModule, TRON_NETWORKS } = await import("@clip-wallet/chains-tron"); + const { createXrplModule, XRPL_NETWORKS } = await import("@clip-wallet/chains-xrpl"); + const { createAntelopeModule, ANTELOPE_NETWORKS } = await import("@clip-wallet/chains-antelope"); + const { createMultiversXModule, MULTIVERSX_NETWORKS } = await import("@clip-wallet/chains-multiversx"); + const { createIcpModule, ICP_NETWORKS } = await import("@clip-wallet/chains-icp"); + const { createStacksModule, STACKS_NETWORKS } = await import("@clip-wallet/chains-stacks"); + const { createFuelModule, FUEL_NETWORKS } = await import("@clip-wallet/chains-fuel"); + const { createBitcoinCashModule, BCH_NETWORKS } = await import("@clip-wallet/chains-bitcoincash"); + const cases = [ + ...(["cosmos", "provenance", "thorchain", "initia"] as const).map((f) => [f, createCosmosModule({ family: f }), COSMOS_NETWORKS.filter((n) => n.family === f)] as const), + ["tron", createTronModule(), TRON_NETWORKS] as const, + ["xrpl", createXrplModule(), XRPL_NETWORKS] as const, + ["antelope", createAntelopeModule(), ANTELOPE_NETWORKS] as const, + ["multiversx", createMultiversXModule(), MULTIVERSX_NETWORKS] as const, + ["icp", createIcpModule(), ICP_NETWORKS] as const, + ["stacks", createStacksModule(), STACKS_NETWORKS] as const, + ["fuel", createFuelModule(), FUEL_NETWORKS] as const, + ["bitcoincash", createBitcoinCashModule(), BCH_NETWORKS] as const, + ]; + for (const [family, mod, nets] of cases) { + expect(mod.family, family).toBe(family); + expect(mod.curve, family).toBe(CURVE_OF[family]); + for (const i of [0, 3]) expect(mod.derivationPath(i), family).toBe(derivationPath(family, i)); + const k = key(family, 0); + const vaultAddr = defaultAddressOf(family, k.publicKey, ctx); + expect(nets.length, family).toBeGreaterThan(0); + for (const n of nets) { + const a = mod.addressFromPublicKey(k.publicKey, n); + // Antelope: the key identifies the account but can't receive (accounts are names), so it isn't an "address". + if (family !== "antelope") expect(mod.isAddress(a), `${family} ${n.id} ${a}`).toBe(true); + // Network-independent spellings must be exactly the vault's. + if (!["cosmos", "provenance", "stacks", "bitcoincash"].includes(family)) expect(a, `${family} ${n.id}`).toBe(vaultAddr); + } + } + }); +}); diff --git a/packages/vault/test/vault-phase2.test.ts b/packages/vault/test/vault-phase2.test.ts index 5921916b..c3643f55 100644 --- a/packages/vault/test/vault-phase2.test.ts +++ b/packages/vault/test/vault-phase2.test.ts @@ -146,7 +146,7 @@ describe("signing per scheme", () => { it("unknown account families are refused", async () => { const { vault } = await imported(); - expect(await code(vault.sign({ accountId: "cosmos:0", scheme: "ed25519", bytes: MSG, approvalId: "a" }))).toBe("vault/unknown-account"); + expect(await code(vault.sign({ accountId: "dogecoin:0", scheme: "ed25519", bytes: MSG, approvalId: "a" }))).toBe("vault/unknown-account"); }); }); @@ -290,7 +290,7 @@ describe("accounts and labels (encrypted metadata)", () => { const a = await vault.addAccount("tezos", "x".repeat(200)); expect(a.label).toHaveLength(64); await expect(vault.setAccountLabel("tezos", 0, 5 as unknown as string)).rejects.toThrow(/string/); - await expect(vault.addAccount("cosmos" as Family)).rejects.toThrow(/unknown family/); + await expect(vault.addAccount("dogecoin" as Family)).rejects.toThrow(/unknown family/); }); }); diff --git a/packages/vault/test/vault.test.ts b/packages/vault/test/vault.test.ts index 10db3d59..61be62ea 100644 --- a/packages/vault/test/vault.test.ts +++ b/packages/vault/test/vault.test.ts @@ -266,7 +266,7 @@ describe("approval binding and signing", () => { it("ecdsa refuses non-32-byte input; unknown accounts are refused", async () => { const [p] = approve(vault, "appr-8", [{ accountId: "evm:0", scheme: "ecdsa-secp256k1", bytes: new Uint8Array(31) }]); expect(await code(vault.sign(p!))).toBe("vault/bad-payload"); - expect(await code(vault.sign({ ...p!, accountId: "cosmos:0" }))).toBe("vault/unknown-account"); + expect(await code(vault.sign({ ...p!, accountId: "dogecoin:0" }))).toBe("vault/unknown-account"); }); it("ed25519 (Solana)", async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9149c89d..39d122bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -262,27 +262,48 @@ importers: '@clip-wallet/chains-algorand': specifier: workspace:* version: link:../../packages/chains-algorand + '@clip-wallet/chains-antelope': + specifier: workspace:* + version: link:../../packages/chains-antelope '@clip-wallet/chains-aptos': specifier: workspace:* version: link:../../packages/chains-aptos '@clip-wallet/chains-bitcoin': specifier: workspace:* version: link:../../packages/chains-bitcoin + '@clip-wallet/chains-bitcoincash': + specifier: workspace:* + version: link:../../packages/chains-bitcoincash '@clip-wallet/chains-cardano': specifier: workspace:* version: link:../../packages/chains-cardano + '@clip-wallet/chains-cosmos': + specifier: workspace:* + version: link:../../packages/chains-cosmos '@clip-wallet/chains-evm': specifier: workspace:* version: link:../../packages/chains-evm + '@clip-wallet/chains-fuel': + specifier: workspace:* + version: link:../../packages/chains-fuel '@clip-wallet/chains-hedera': specifier: workspace:* version: link:../../packages/chains-hedera + '@clip-wallet/chains-icp': + specifier: workspace:* + version: link:../../packages/chains-icp + '@clip-wallet/chains-multiversx': + specifier: workspace:* + version: link:../../packages/chains-multiversx '@clip-wallet/chains-near': specifier: workspace:* version: link:../../packages/chains-near '@clip-wallet/chains-solana': specifier: workspace:* version: link:../../packages/chains-solana + '@clip-wallet/chains-stacks': + specifier: workspace:* + version: link:../../packages/chains-stacks '@clip-wallet/chains-starknet': specifier: workspace:* version: link:../../packages/chains-starknet @@ -301,6 +322,12 @@ importers: '@clip-wallet/chains-ton': specifier: workspace:* version: link:../../packages/chains-ton + '@clip-wallet/chains-tron': + specifier: workspace:* + version: link:../../packages/chains-tron + '@clip-wallet/chains-xrpl': + specifier: workspace:* + version: link:../../packages/chains-xrpl '@clip-wallet/config': specifier: workspace:* version: link:../../packages/config @@ -363,10 +390,10 @@ importers: version: link:../../packages/vault '@creit.tech/stellar-wallets-kit': specifier: 2.7.0 - version: 2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(near-api-js@5.1.1)(react@19.3.0)(supports-color@10.2.2)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + version: 2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(near-api-js@5.1.1)(react@19.3.0)(supports-color@10.2.2)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@hashgraph/hedera-wallet-connect': specifier: 2.1.3 - version: 2.1.3(a31850274c23b83333f7aa05d3b0718f) + version: 2.1.3(1fd548a862f7b06f309bdc65aa5e1220) '@hiero-ledger/sdk': specifier: 2.79.0 version: 2.79.0(bn.js@5.2.1)(expo-crypto@57.0.3(expo@57.0.26))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2) @@ -375,7 +402,7 @@ importers: version: 2.0.0-beta.2(ef8ae08386364f9042ccc110281254fb) '@mysten/dapp-kit': specifier: 1.1.17 - version: 1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0)) + version: 1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(immer@10.1.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0)) '@mysten/sui': specifier: ^2.34.0 version: 2.34.0(typescript@5.9.3) @@ -396,13 +423,13 @@ importers: version: 14.0.3 '@rainbow-me/rainbowkit': specifier: 2.2.11 - version: 2.2.11(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(wagmi@2.19.5(36cbd6af1fba239fc5835c3ea32e5981)) + version: 2.2.11(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(wagmi@2.19.5(f6fa2c3037d24512664382b670f973d0)) '@reown/appkit': specifier: 1.8.24 - version: 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + version: 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-adapter-wagmi': specifier: 1.8.24 - version: 1.8.24(cb2b1022a657ae38d56d762bf274b4d2) + version: 1.8.24(634791c039d0d5e120b0f2de526c4c31) '@solana/wallet-adapter-base': specifier: ^0.9.28 version: 0.9.28(@solana/web3.js@1.99.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -444,13 +471,13 @@ importers: version: 19.3.0(@types/react@19.3.0) '@wagmi/core': specifier: ^3.6.5 - version: 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + version: 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) '@wallet-standard/app': specifier: ^1.1.1 version: 1.1.1 '@walletconnect/ethereum-provider': specifier: 2.25.0 - version: 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + version: 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) algosdk: specifier: ^3.8.0 version: 3.8.0 @@ -468,7 +495,7 @@ importers: version: 19.3.0(react@19.3.0) starknetkit: specifier: 3.4.3 - version: 3.4.3(634430694b27258fc46dd25a8218dcb9) + version: 3.4.3(99f3bbb49cc003434109d2744f216011) typedoc: specifier: ^0.28.20 version: 0.28.20(typescript@5.9.3) @@ -495,7 +522,7 @@ importers: version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) wagmi: specifier: 2.19.5 - version: 2.19.5(36cbd6af1fba239fc5835c3ea32e5981) + version: 2.19.5(f6fa2c3037d24512664382b670f973d0) wxt: specifier: ^0.21.4 version: 0.21.4(esbuild@0.28.2)(rolldown@1.2.12)(rollup@4.63.6)(typescript@5.9.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) @@ -533,27 +560,48 @@ importers: '@clip-wallet/chains-algorand': specifier: workspace:* version: link:../../packages/chains-algorand + '@clip-wallet/chains-antelope': + specifier: workspace:* + version: link:../../packages/chains-antelope '@clip-wallet/chains-aptos': specifier: workspace:* version: link:../../packages/chains-aptos '@clip-wallet/chains-bitcoin': specifier: workspace:* version: link:../../packages/chains-bitcoin + '@clip-wallet/chains-bitcoincash': + specifier: workspace:* + version: link:../../packages/chains-bitcoincash '@clip-wallet/chains-cardano': specifier: workspace:* version: link:../../packages/chains-cardano + '@clip-wallet/chains-cosmos': + specifier: workspace:* + version: link:../../packages/chains-cosmos '@clip-wallet/chains-evm': specifier: workspace:* version: link:../../packages/chains-evm + '@clip-wallet/chains-fuel': + specifier: workspace:* + version: link:../../packages/chains-fuel '@clip-wallet/chains-hedera': specifier: workspace:* version: link:../../packages/chains-hedera + '@clip-wallet/chains-icp': + specifier: workspace:* + version: link:../../packages/chains-icp + '@clip-wallet/chains-multiversx': + specifier: workspace:* + version: link:../../packages/chains-multiversx '@clip-wallet/chains-near': specifier: workspace:* version: link:../../packages/chains-near '@clip-wallet/chains-solana': specifier: workspace:* version: link:../../packages/chains-solana + '@clip-wallet/chains-stacks': + specifier: workspace:* + version: link:../../packages/chains-stacks '@clip-wallet/chains-starknet': specifier: workspace:* version: link:../../packages/chains-starknet @@ -572,6 +620,12 @@ importers: '@clip-wallet/chains-ton': specifier: workspace:* version: link:../../packages/chains-ton + '@clip-wallet/chains-tron': + specifier: workspace:* + version: link:../../packages/chains-tron + '@clip-wallet/chains-xrpl': + specifier: workspace:* + version: link:../../packages/chains-xrpl '@clip-wallet/connect': specifier: workspace:* version: link:../../packages/connect @@ -584,12 +638,33 @@ importers: '@clip-wallet/ui': specifier: workspace:* version: link:../../packages/ui + '@cosmjs/amino': + specifier: ^0.39.0 + version: 0.39.0 + '@cosmjs/crypto': + specifier: ^0.39.0 + version: 0.39.0 + '@cosmjs/encoding': + specifier: ^0.39.0 + version: 0.39.0 + '@cosmjs/stargate': + specifier: ^0.39.0 + version: 0.39.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@hashgraph/hedera-wallet-connect': specifier: 2.1.3 - version: 2.1.3(d1a5b987f8d9e1b2cd9533a0ab02adfc) + version: 2.1.3(8b74697f4445efcecd7327d3adea5b89) '@hiero-ledger/sdk': specifier: ^2.89.1 version: 2.89.1(bn.js@5.2.3)(bufferutil@4.1.0)(expo-crypto@57.0.3(expo@57.0.26))(supports-color@10.2.2)(utf-8-validate@5.0.10) + '@multiversx/sdk-core': + specifier: ^15.4.1 + version: 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + '@multiversx/sdk-dapp': + specifier: 5.7.3 + version: 5.7.3(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@multiversx/sdk-dapp-utils@3.1.0(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(bignumber.js@9.3.1))(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(axios@1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2))(bignumber.js@9.3.1)(bufferutil@4.1.0)(protobufjs@7.6.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vue@3.5.43(typescript@5.9.3))(zod@4.6.5) + '@multiversx/sdk-dapp-utils': + specifier: ^3.1.0 + version: 3.1.0(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(bignumber.js@9.3.1) '@mysten/sui': specifier: ^2.34.0 version: 2.34.0(typescript@5.9.3) @@ -625,10 +700,10 @@ importers: version: 14.0.3(@polkadot/util@14.0.3) '@reown/appkit': specifier: ^1.8.24 - version: 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + version: 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-adapter-wagmi': specifier: ^1.8.24 - version: 1.8.24(75f6087df408135c9a04dd1e7d32adf8) + version: 1.8.24(e1ddbe9b4bf0b2cd292d88ad4f2f62a3) '@scure/base': specifier: 2.2.0 version: 2.2.0 @@ -644,6 +719,15 @@ importers: '@solana/web3.js': specifier: ^1.99.0 version: 1.99.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@stacks/connect': + specifier: 8.2.7 + version: 8.2.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@stacks/encryption': + specifier: 7.6.0 + version: 7.6.0 + '@stacks/transactions': + specifier: 7.6.0 + version: 7.6.0 '@starknet-io/get-starknet-core': specifier: 4.0.8 version: 4.0.8 @@ -676,7 +760,7 @@ importers: version: 19.3.0(@types/react@19.3.0) '@wagmi/core': specifier: ^3.6.5 - version: 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + version: 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) '@wallet-standard/app': specifier: ^1.1.1 version: 1.1.1 @@ -686,6 +770,12 @@ importers: algosdk: specifier: ^3.8.0 version: 3.8.0 + axios: + specifier: ^1.20.0 + version: 1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2) + bignumber.js: + specifier: ^9.3.1 + version: 9.3.1 bip322-js: specifier: 4.0.0 version: 4.0.0(typescript@5.9.3) @@ -701,15 +791,24 @@ importers: events: specifier: 3.3.0 version: 3.3.0 + fuels: + specifier: 0.103.0 + version: 0.103.0(supports-color@10.2.2)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + protobufjs: + specifier: ^7.6.6 + version: 7.6.6 starknet: specifier: ^10.8.0 version: 10.8.0(typescript@5.9.3)(zod@4.6.5) + tronweb: + specifier: ^6.5.1 + version: 6.5.1(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2)(utf-8-validate@5.0.10) viem: specifier: ^2.57.1 version: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) wagmi: specifier: ^3.7.7 - version: 3.7.7(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@tanstack/query-core@5.104.1)(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + version: 3.7.7(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@tanstack/query-core@5.104.1)(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) wxt: specifier: ^0.21.4 version: 0.21.4(esbuild@0.28.2)(rolldown@1.2.12)(rollup@4.63.6)(typescript@5.9.3)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) @@ -733,10 +832,10 @@ importers: version: link:../../../../../packages/kit-modules '@creit.tech/stellar-wallets-kit': specifier: 2.7.0 - version: 2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + version: 2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@hashgraph/hedera-wallet-connect': specifier: 2.1.3 - version: 2.1.3(5dedda258f06b094f7e7ac4ac90bcdfc) + version: 2.1.3(c79748b64936bc6426ffcc8f4d484770) '@hiero-ledger/sdk': specifier: 2.79.0 version: 2.79.0(bn.js@5.2.1)(expo-crypto@57.0.3(expo@57.0.26))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(supports-color@5.5.0) @@ -745,7 +844,7 @@ importers: version: 2.0.0-beta.2(07ce867dca9c238b006ac39cc77bc8a1) '@mysten/dapp-kit': specifier: 1.1.17 - version: 1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1)) + version: 1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1)) '@mysten/sui': specifier: ^2.34.0 version: 2.34.0(typescript@5.9.3) @@ -763,16 +862,16 @@ importers: version: 10.1.4(@near-js/keystores@2.5.1(@near-js/crypto@2.5.1(@near-js/types@2.5.1)(@near-js/utils@2.5.1(@near-js/types@2.5.1)))(@near-js/types@2.5.1))(@near-js/signers@2.5.1(@near-js/crypto@2.5.1(@near-js/types@2.5.1)(@near-js/utils@2.5.1(@near-js/types@2.5.1)))(@near-js/keystores@2.5.1(@near-js/crypto@2.5.1(@near-js/types@2.5.1)(@near-js/utils@2.5.1(@near-js/types@2.5.1)))(@near-js/types@2.5.1))(@near-js/transactions@2.5.1(@near-js/crypto@2.5.1(@near-js/types@2.5.1)(@near-js/utils@2.5.1(@near-js/types@2.5.1)))(@near-js/types@2.5.1)(@near-js/utils@2.5.1(@near-js/types@2.5.1))))(@near-js/tokens@2.5.1)(@near-js/types@2.5.1) '@rainbow-me/rainbowkit': specifier: 2.2.11 - version: 2.2.11(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(128dd19e588211bd4f16254762e0e3f1)) + version: 2.2.11(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(6830db7ba68b15a013c6582bffdd96ea)) '@reactive-dot/core': specifier: 0.72.0 version: 0.72.0(polkadot-api@2.2.2(bufferutil@4.1.0)(esbuild@0.28.2)(rxjs@7.8.2)(supports-color@5.5.0)(utf-8-validate@5.0.10))(typescript@5.9.3)(zod@4.0.5) '@reown/appkit': specifier: 1.8.24 - version: 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + version: 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-adapter-wagmi': specifier: 1.8.24 - version: 1.8.24(8fbf84517274ca6aeadd4115118f2ac2) + version: 1.8.24(8863ad211d7eb04a60a6c3b2601e2c62) '@solana/wallet-adapter-base': specifier: ^0.9.28 version: 0.9.28(@solana/web3.js@1.99.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -823,7 +922,7 @@ importers: version: 6.0.3 connectkit: specifier: 1.9.2 - version: 1.9.2(@babel/core@7.29.7(supports-color@5.5.0))(@tanstack/react-query@5.104.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@19.3.0)(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(128dd19e588211bd4f16254762e0e3f1)) + version: 1.9.2(@babel/core@7.29.7(supports-color@5.5.0))(@tanstack/react-query@5.104.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@19.3.0)(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(6830db7ba68b15a013c6582bffdd96ea)) dot-connect: specifier: 0.31.0 version: 0.31.0(@reactive-dot/core@0.72.0(polkadot-api@2.2.2(bufferutil@4.1.0)(esbuild@0.28.2)(rxjs@7.8.2)(supports-color@5.5.0)(utf-8-validate@5.0.10))(typescript@5.9.3)(zod@4.0.5))(@types/react@19.3.0)(react@18.3.1)(signal-polyfill@0.2.2)(vue@3.5.43(typescript@5.9.3)) @@ -844,7 +943,7 @@ importers: version: 8.9.2 starknetkit: specifier: 3.4.3 - version: 3.4.3(1c2478886b26ffb945b93bfbff4fa192) + version: 3.4.3(9929b9e50ebc9ec98a319d4fa0d43aa0) styled-components: specifier: ^6.3.12 version: 6.5.3(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react@18.3.1) @@ -853,7 +952,7 @@ importers: version: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) wagmi: specifier: 2.19.5 - version: 2.19.5(128dd19e588211bd4f16254762e0e3f1) + version: 2.19.5(6830db7ba68b15a013c6582bffdd96ea) devDependencies: typescript: specifier: ^5.9.3 @@ -1061,6 +1160,25 @@ importers: specifier: ^22.20.5 version: 22.20.5 + packages/chains-antelope: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + '@scure/base': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + '@wharfkit/antelope': + specifier: ^4.0.2 + version: 4.0.2 + packages/chains-aptos: dependencies: '@aptos-labs/ts-sdk': @@ -1098,6 +1216,22 @@ importers: specifier: 2.4.1 version: 2.4.1 + packages/chains-bitcoincash: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + '@bitauth/libauth': + specifier: ^3.0.0 + version: 3.0.0 + packages/chains-cardano: dependencies: '@clip-wallet/core': @@ -1113,6 +1247,37 @@ importers: specifier: 2.4.0 version: 2.4.0 + packages/chains-cosmos: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + '@scure/base': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + '@cosmjs/amino': + specifier: ^0.39.0 + version: 0.39.0 + '@cosmjs/crypto': + specifier: ^0.39.0 + version: 0.39.0 + '@cosmjs/proto-signing': + specifier: ^0.39.0 + version: 0.39.0 + '@types/node': + specifier: ^22.20.5 + version: 22.20.5 + cosmjs-types: + specifier: ^0.11.0 + version: 0.11.0 + packages/chains-evm: dependencies: '@clip-wallet/core': @@ -1129,6 +1294,22 @@ importers: specifier: ^22.20.5 version: 22.20.5 + packages/chains-fuel: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + fuels: + specifier: 0.103.0 + version: 0.103.0(supports-color@10.2.2)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + packages/chains-hedera: dependencies: '@clip-wallet/core': @@ -1151,6 +1332,53 @@ importers: specifier: ^2.57.2 version: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.6.5) + packages/chains-icp: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + '@scure/base': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + '@dfinity/agent': + specifier: ^2.4.1 + version: 2.4.1(@dfinity/candid@2.4.1(@dfinity/principal@2.4.1))(@dfinity/principal@2.4.1) + '@dfinity/candid': + specifier: ^2.4.1 + version: 2.4.1(@dfinity/principal@2.4.1) + '@dfinity/identity-secp256k1': + specifier: ^2.4.1 + version: 2.4.1(@dfinity/candid@2.4.1(@dfinity/principal@2.4.1))(@dfinity/principal@2.4.1) + '@dfinity/principal': + specifier: ^2.4.1 + version: 2.4.1 + + packages/chains-multiversx: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + '@scure/base': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + '@multiversx/sdk-core': + specifier: ^16.0.0 + version: 16.0.0(bignumber.js@9.3.1)(debug@4.4.3(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + packages/chains-near: dependencies: '@clip-wallet/core': @@ -1190,6 +1418,25 @@ importers: specifier: ^8.4.0 version: 8.4.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + packages/chains-stacks: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + '@stacks/encryption': + specifier: 7.6.0 + version: 7.6.0 + '@stacks/transactions': + specifier: 7.6.0 + version: 7.6.0 + packages/chains-starknet: dependencies: '@clip-wallet/core': @@ -1307,6 +1554,50 @@ importers: specifier: ^22.0.0 version: 22.20.5 + packages/chains-tron: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + '@scure/base': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + tronweb: + specifier: ^6.5.1 + version: 6.5.1(bufferutil@4.1.0)(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)(utf-8-validate@6.0.6) + + packages/chains-xrpl: + dependencies: + '@clip-wallet/core': + specifier: workspace:* + version: link:../core + '@noble/curves': + specifier: 2.4.0 + version: 2.4.0 + '@noble/hashes': + specifier: 2.4.0 + version: 2.4.0 + '@scure/base': + specifier: 2.4.0 + version: 2.4.0 + devDependencies: + ripple-binary-codec: + specifier: ^2.11.0 + version: 2.11.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ripple-keypairs: + specifier: ^3.1.0 + version: 3.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + xrpl: + specifier: ^5.3.0 + version: 5.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + packages/config: dependencies: zod: @@ -1327,7 +1618,7 @@ importers: dependencies: '@walletconnect/ethereum-provider': specifier: ^2.0.0 - version: 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) devDependencies: '@solana/wallet-adapter-base': specifier: ^0.9.28 @@ -1346,7 +1637,7 @@ importers: version: 19.3.0 '@wagmi/core': specifier: ^3.6.5 - version: 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) bs58: specifier: ^6.0.0 version: 6.0.0 @@ -1501,27 +1792,48 @@ importers: '@clip-wallet/chains-algorand': specifier: workspace:* version: link:../chains-algorand + '@clip-wallet/chains-antelope': + specifier: workspace:* + version: link:../chains-antelope '@clip-wallet/chains-aptos': specifier: workspace:* version: link:../chains-aptos '@clip-wallet/chains-bitcoin': specifier: workspace:* version: link:../chains-bitcoin + '@clip-wallet/chains-bitcoincash': + specifier: workspace:* + version: link:../chains-bitcoincash '@clip-wallet/chains-cardano': specifier: workspace:* version: link:../chains-cardano + '@clip-wallet/chains-cosmos': + specifier: workspace:* + version: link:../chains-cosmos '@clip-wallet/chains-evm': specifier: workspace:* version: link:../chains-evm + '@clip-wallet/chains-fuel': + specifier: workspace:* + version: link:../chains-fuel '@clip-wallet/chains-hedera': specifier: workspace:* version: link:../chains-hedera + '@clip-wallet/chains-icp': + specifier: workspace:* + version: link:../chains-icp + '@clip-wallet/chains-multiversx': + specifier: workspace:* + version: link:../chains-multiversx '@clip-wallet/chains-near': specifier: workspace:* version: link:../chains-near '@clip-wallet/chains-solana': specifier: workspace:* version: link:../chains-solana + '@clip-wallet/chains-stacks': + specifier: workspace:* + version: link:../chains-stacks '@clip-wallet/chains-starknet': specifier: workspace:* version: link:../chains-starknet @@ -1540,6 +1852,12 @@ importers: '@clip-wallet/chains-ton': specifier: workspace:* version: link:../chains-ton + '@clip-wallet/chains-tron': + specifier: workspace:* + version: link:../chains-tron + '@clip-wallet/chains-xrpl': + specifier: workspace:* + version: link:../chains-xrpl '@clip-wallet/config': specifier: workspace:* version: link:../config @@ -1598,27 +1916,48 @@ importers: '@clip-wallet/chains-algorand': specifier: workspace:* version: link:../chains-algorand + '@clip-wallet/chains-antelope': + specifier: workspace:* + version: link:../chains-antelope '@clip-wallet/chains-aptos': specifier: workspace:* version: link:../chains-aptos '@clip-wallet/chains-bitcoin': specifier: workspace:* version: link:../chains-bitcoin + '@clip-wallet/chains-bitcoincash': + specifier: workspace:* + version: link:../chains-bitcoincash '@clip-wallet/chains-cardano': specifier: workspace:* version: link:../chains-cardano + '@clip-wallet/chains-cosmos': + specifier: workspace:* + version: link:../chains-cosmos '@clip-wallet/chains-evm': specifier: workspace:* version: link:../chains-evm + '@clip-wallet/chains-fuel': + specifier: workspace:* + version: link:../chains-fuel '@clip-wallet/chains-hedera': specifier: workspace:* version: link:../chains-hedera + '@clip-wallet/chains-icp': + specifier: workspace:* + version: link:../chains-icp + '@clip-wallet/chains-multiversx': + specifier: workspace:* + version: link:../chains-multiversx '@clip-wallet/chains-near': specifier: workspace:* version: link:../chains-near '@clip-wallet/chains-solana': specifier: workspace:* version: link:../chains-solana + '@clip-wallet/chains-stacks': + specifier: workspace:* + version: link:../chains-stacks '@clip-wallet/chains-starknet': specifier: workspace:* version: link:../chains-starknet @@ -1637,6 +1976,12 @@ importers: '@clip-wallet/chains-ton': specifier: workspace:* version: link:../chains-ton + '@clip-wallet/chains-tron': + specifier: workspace:* + version: link:../chains-tron + '@clip-wallet/chains-xrpl': + specifier: workspace:* + version: link:../chains-xrpl '@clip-wallet/config': specifier: workspace:* version: link:../config @@ -2391,15 +2736,69 @@ importers: '@aptos-labs/ts-sdk': specifier: ^7.3.0 version: 7.3.0 + '@bitauth/libauth': + specifier: ^3.0.0 + version: 3.0.0 + '@clip-wallet/chains-antelope': + specifier: workspace:* + version: link:../chains-antelope '@clip-wallet/chains-bitcoin': specifier: workspace:* version: link:../chains-bitcoin + '@clip-wallet/chains-bitcoincash': + specifier: workspace:* + version: link:../chains-bitcoincash + '@clip-wallet/chains-cosmos': + specifier: workspace:* + version: link:../chains-cosmos + '@clip-wallet/chains-fuel': + specifier: workspace:* + version: link:../chains-fuel + '@clip-wallet/chains-icp': + specifier: workspace:* + version: link:../chains-icp + '@clip-wallet/chains-multiversx': + specifier: workspace:* + version: link:../chains-multiversx + '@clip-wallet/chains-stacks': + specifier: workspace:* + version: link:../chains-stacks + '@clip-wallet/chains-substrate': + specifier: workspace:* + version: link:../chains-substrate + '@clip-wallet/chains-tron': + specifier: workspace:* + version: link:../chains-tron + '@clip-wallet/chains-xrpl': + specifier: workspace:* + version: link:../chains-xrpl '@clip-wallet/link': specifier: workspace:* version: link:../link + '@cosmjs/amino': + specifier: ^0.39.0 + version: 0.39.0 + '@cosmjs/crypto': + specifier: ^0.39.0 + version: 0.39.0 + '@dfinity/agent': + specifier: ^2.4.1 + version: 2.4.1(@dfinity/candid@2.4.1(@dfinity/principal@2.4.1))(@dfinity/principal@2.4.1) + '@dfinity/candid': + specifier: ^2.4.1 + version: 2.4.1(@dfinity/principal@2.4.1) + '@dfinity/identity-secp256k1': + specifier: ^2.4.1 + version: 2.4.1(@dfinity/candid@2.4.1(@dfinity/principal@2.4.1))(@dfinity/principal@2.4.1) + '@dfinity/principal': + specifier: ^2.4.1 + version: 2.4.1 '@emurgo/cardano-serialization-lib-nodejs': specifier: ^17.0.0 version: 17.0.0 + '@multiversx/sdk-core': + specifier: ^16.0.0 + version: 16.0.0(bignumber.js@9.3.1)(debug@4.4.3(supports-color@8.1.1))(protobufjs@7.6.6)(supports-color@8.1.1) '@mysten/sui': specifier: ^2.33.2 version: 2.34.0(typescript@5.9.3) @@ -2415,6 +2814,12 @@ importers: '@scure/btc-signer': specifier: 2.4.1 version: 2.4.1 + '@stacks/transactions': + specifier: ^7.6.0 + version: 7.6.0 + '@stacks/wallet-sdk': + specifier: ^7.6.0 + version: 7.6.0 '@stellar/stellar-base': specifier: ^15.0.0 version: 15.0.0 @@ -2433,6 +2838,9 @@ importers: '@ton/ton': specifier: ^16.3.0 version: 16.3.0(@ton/core@0.63.1(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) + '@wharfkit/antelope': + specifier: ^4.0.2 + version: 4.0.2 algosdk: specifier: ^3.8.0 version: 3.8.0 @@ -2442,6 +2850,9 @@ importers: ethers: specifier: ^6.16.0 version: 6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + fuels: + specifier: ^0.103.0 + version: 0.103.0(supports-color@8.1.1)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) micro-key-producer: specifier: ^0.10.2 version: 0.10.2 @@ -2451,6 +2862,12 @@ importers: starknet: specifier: ^10.8.0 version: 10.8.0(typescript@5.9.3)(zod@4.6.5) + tronweb: + specifier: ^6.5.1 + version: 6.5.1(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(utf-8-validate@6.0.6) + xrpl: + specifier: ^5.3.0 + version: 5.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) services/backup: dependencies: @@ -3309,6 +3726,10 @@ packages: resolution: {integrity: sha512-R0XPZ/IQhU2TtetSFI9vI+7kJOJYNiCncn5ixEBW+/LNaZCo2HK37Mq3pRNzrM4FryuAkyeqY7Ujmj3I3e3t9g==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} + '@bitauth/libauth@3.0.0': + resolution: {integrity: sha512-3yoL31XpnhAnf5nDVMFk4xPqebxDwXrgYAwpa31ARJnV5A/eXWlpNYvCd6FTZPFM4VvKfjCBi+jRCrw1hOZ0Jg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@bitcoin-js/tiny-secp256k1-asmjs@2.2.4': resolution: {integrity: sha512-Lo62disBIDwPrYAmMsSjEmqak41yb0OFGQVLdktXmcQLgtC1BI5Sd1eHSxNREKZmxMUXevtsgEhGB1DvvatRmQ==} engines: {node: '>=14.0.0'} @@ -3755,6 +4176,39 @@ packages: peerDependencies: commander: ~15.0.0 + '@cosmjs/amino@0.39.0': + resolution: {integrity: sha512-56WqsZ4xbdJE4KWJTPbw7hUzePflRxRLuxoBlUsa8qIiFumpzZRW0Ycb7IloEF8wAmdgsdfrG1hSJSBlqdFE7A==} + + '@cosmjs/crypto@0.39.0': + resolution: {integrity: sha512-ATRhSXN8w3fvUkj9xzLHwvzylDvvn4f3cC1CQwhQc2OxyzpEEFACS3wHS6iwdJJS99acV8dq+oVOYflUqI0brQ==} + + '@cosmjs/encoding@0.39.0': + resolution: {integrity: sha512-+poEaeM8YjGNVtrHLQNWqkhEeDxapjrdpnPCT+JCRh8YNbeHEdftzZLCH5VCBSOtvo7PF0gK1B7sbQbBl6q5pQ==} + + '@cosmjs/json-rpc@0.39.0': + resolution: {integrity: sha512-slyo76IYkTuSxrzxvF1s1ScFPIGnW7PbNSx8lr++NzdyPluqe053+cSqsxLUioXb5qI9Nxny2fWxOK8isSn+cg==} + + '@cosmjs/math@0.39.0': + resolution: {integrity: sha512-FSLy/oDF+BtOP/J60RsjL5W4MCKiCfBjSoeV5xj6qg2g8N884Ue853iuWanjqGkQJwkXCp+JbeK8Mv9j6AaYHw==} + + '@cosmjs/proto-signing@0.39.0': + resolution: {integrity: sha512-jthQ2PA092fvrbFaZRRKuHV7awOsayjDLIBACaGIblFj9yJCcrvhFp+8YV87ia990DjKmWJc3sstHvQZ3yK/EQ==} + + '@cosmjs/socket@0.39.0': + resolution: {integrity: sha512-mvA+/ycMn7dnjjdooSPq516poFpSARRXhFhKjCXxGUmBZA+74vLDLbtD6VGzHd08A2JSZBSiAhVOZpjYqgV78A==} + + '@cosmjs/stargate@0.39.0': + resolution: {integrity: sha512-dQtucU9czF2NUUbEKs19PMfv+EusDFKWHMVK6Hinytmpq9sdk5JUIln+g/8k+UjKcV4HPv6S0AywbhE+tl84kQ==} + + '@cosmjs/stream@0.39.0': + resolution: {integrity: sha512-h56it+ku5F6nlO94VyVAyz3pQqYsGRJ8v+5Y5erXYlQ0Qmr+rLrlSMCiViFEMK9INRSilAypdI5hKPCzotiqRg==} + + '@cosmjs/tendermint-rpc@0.39.0': + resolution: {integrity: sha512-KowP/6P6Tx+OnGoimYnxuSBIfhB+vWqWER2pZpEIOHId8ZfcjQ9JS2F+Ah+WmZ9D+bKgn4pJ5ImAS13TeShIEw==} + + '@cosmjs/utils@0.39.0': + resolution: {integrity: sha512-h7fy7Tbcl9v8ABntp8+kqw2VmUus2HbnRJFyzTkM7byRktLtECHYNMsztwyl1rdHkzc8Nc2xs6K/56d7a+75aw==} + '@creit.tech/stellar-wallets-kit@2.7.0': resolution: {integrity: sha512-gH+eCIUKvE7DZx9ZFCFog1JSpi5Jf28wHTLa67CzAXoJFQkcqkoQ9j2LilHrZPla8M43MjW0COQiFvrlS/pLMg==} @@ -3802,6 +4256,27 @@ packages: resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} engines: {node: '>=20.19.0'} + '@dfinity/agent@2.4.1': + resolution: {integrity: sha512-IczFFOUDGfMTdQ83yiCvGtvHr1IIB80lWBP0ZYRLogs6NVt8t6HYcMlu1sgT+9VivhT7iwX4pktPFxxOkO3COw==} + deprecated: 'This package has been deprecated. Use @icp-sdk/core/agent instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5' + peerDependencies: + '@dfinity/candid': ^2.4.1 + '@dfinity/principal': ^2.4.1 + + '@dfinity/candid@2.4.1': + resolution: {integrity: sha512-kOaIKfhR2PYN8vD4M0Pc4s/7wb1nKjlTJUw+5E9jh26T03fITIZmaafIuwlX+wmdxwIT9Xoy7PlsxOEpzv203A==} + deprecated: 'This package has been deprecated. Use @icp-sdk/core/candid instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5' + peerDependencies: + '@dfinity/principal': ^2.4.1 + + '@dfinity/identity-secp256k1@2.4.1': + resolution: {integrity: sha512-YP6AYObQc3bADwVhjqQ90V6mIKyN0vDISRBk9izKpiPStK29lbxF8zLkqY7wYhgrxDNvtSlmd+VMQrvbE7K3lg==} + deprecated: 'This package has been deprecated. Use @icp-sdk/core/identity/secp256k1 instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5' + + '@dfinity/principal@2.4.1': + resolution: {integrity: sha512-Cz6XQVOwq0TXDBClPbcidDd4SqK1lfr1/Kn34ruDD13xVQ4iaP1iCntzS9O97+vGpY/6jwDtKd32Gn5YJ9BQNw==} + deprecated: 'This package has been deprecated. Use @icp-sdk/core/principal instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5' + '@dnsquery/dns-packet@6.1.1': resolution: {integrity: sha512-WXTuFvL3G+74SchFAtz3FgIYVOe196ycvGsMgvSH/8Goptb1qpIQtIuM4SOK9G9lhMWYpHxnXyy544ZhluFOew==} engines: {node: '>=6'} @@ -3978,6 +4453,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.3': + resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} @@ -4008,6 +4489,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.3': + resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.7': resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} @@ -4038,6 +4525,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.3': + resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.7': resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} @@ -4068,6 +4561,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.3': + resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.7': resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} @@ -4098,6 +4597,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.3': + resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.7': resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} @@ -4128,6 +4633,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.3': + resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.7': resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} @@ -4158,6 +4669,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.3': + resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} @@ -4188,6 +4705,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.3': + resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} @@ -4218,6 +4741,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.3': + resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.7': resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} @@ -4248,6 +4777,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.3': + resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.7': resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} @@ -4278,6 +4813,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.3': + resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.7': resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} @@ -4308,6 +4849,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.3': + resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.7': resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} @@ -4338,6 +4885,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.3': + resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.7': resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} @@ -4368,6 +4921,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.3': + resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.7': resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} @@ -4398,6 +4957,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.3': + resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.7': resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} @@ -4428,6 +4993,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.3': + resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.7': resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} @@ -4458,6 +5029,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.3': + resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.7': resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} @@ -4482,6 +5059,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.25.3': + resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} @@ -4512,6 +5095,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.3': + resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} @@ -4536,6 +5125,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.25.3': + resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} @@ -4566,6 +5161,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.3': + resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} @@ -4620,6 +5221,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.3': + resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.7': resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} @@ -4650,6 +5257,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.3': + resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.7': resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} @@ -4680,6 +5293,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.3': + resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.7': resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} @@ -4710,6 +5329,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.3': + resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.7': resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} @@ -5014,6 +5639,77 @@ packages: '@foxglove/crc@0.0.3': resolution: {integrity: sha512-DjIZsnL3CyP/yQ/vUYA9cjrD0a/8YXejI5ZmsaOiT16cLfZcTwaCxIN01/ys4jsy+dZCQ/9DnWFn7AEFbiMDaA==} + '@fuel-ts/abi-coder@0.103.0': + resolution: {integrity: sha512-tErLljHqz8Tnpndijd3XiGSbsBanFCeq2Bbl5DBjbVrZ737m5BqsYVYgBjSDkRmrRxz/uFFmXl1qLEU3MWNtPQ==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/abi-typegen@0.103.0': + resolution: {integrity: sha512-XO9zai8MomXjCHnoa7K/gfO/Nco6Atmu8ptPJ1rHkhbftP75jXp5owflVFe+e+zvIXPA/UsZkM/uxJN0fB2njg==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + hasBin: true + + '@fuel-ts/account@0.103.0': + resolution: {integrity: sha512-FI4iPK6yYtdU8d+pwknyVtOLAsxP0raybH3gPAWhYR7Bp4C4iA6dXnGXQcznBEW52Ez++5LjqvejJjZkIOzohw==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/address@0.103.0': + resolution: {integrity: sha512-Yb/mpU+4D4a6OdTLAvVeEFncNSLtW4NjToxdceDIv/y45TDFdubwcmMw+7gA/fDcn+6p88mojrcCEcRrnDg2qQ==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/contract@0.103.0': + resolution: {integrity: sha512-pdYxN7beDNn+JYRmQ1/1TYEK0QcWB4WvQsmAWlESE2WzmlMVDqoa4U1U5rsh5uZde1/Jb3TikXJeSWpyRa4J5Q==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/crypto@0.103.0': + resolution: {integrity: sha512-8DWeYl1TkXY7Ga/2X1zI4Jv6Ua7mFHRgArwaSV0ckhk+Fu+KK4K9diUAge10TRUe11byG3cX2ObcVIlNtthnPg==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/errors@0.103.0': + resolution: {integrity: sha512-IvWB/Q7rRNYI/GrLuCD4EiGHOi5a+9a7P3AXMZ8gqE2PA5yYKUPQlI87xTvB9f9ZTiX1H/XwdCKprmKp6Nh16g==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/hasher@0.103.0': + resolution: {integrity: sha512-P1EU4iHb7biGSqIldNEgkULcmrY11xdF9fE4Ja3EUZTViI1ekUt2a7r8402CkafLjBpheKzRpsdyL2pqkxJqdw==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/math@0.103.0': + resolution: {integrity: sha512-djUGWHC45GN8xiI4r20eg3hs1pHcZzkNLLFoX3Mg4gcEyPesr0dt+TdvyFGr/tBqoEuS+IBAS2rFB6v1f3Q+Xg==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/merkle@0.103.0': + resolution: {integrity: sha512-A80K7SZgOcnl0SuxurR+6bOAbKPM/gOMlcHDqeAU2vsClG2Zp3Apu1+v7PZJvPgi8JRUHDuDWlqpkNQvLRDvdQ==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/program@0.103.0': + resolution: {integrity: sha512-GXvFpNsyg0VpDm8DXmpE+2oYSufprcPHciiwET1NpexlCGIdiPhBRDA3hqq6P+n+ovNNMNWHgwNxu77Yle9lxA==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/recipes@0.103.0': + resolution: {integrity: sha512-Ei3BTFQXM5Q+7PLln/ChdGis0oHJghdEjr7C8w/3dOGMcTVXwzQOg8kzNzXoYdDMgRxFBG6oX5Wi2JY+x8ShLg==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/script@0.103.0': + resolution: {integrity: sha512-JN5fY5pNM/lnZlU6ZDKNExK0N5C/9I3/o8FbmZBh3i1pyNxDaceEbEX8YMu/PkWXlG5mbiDXYFNWRmzlV8rZ6g==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/transactions@0.103.0': + resolution: {integrity: sha512-oOZ/2Q1r+2823MDsMdD/pU3TwVmOfdaBjbudC+oCT+fidBZMAVRQfF/i4SWDtPfSsRuZRD0qDUA4zicYpxphiA==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@fuel-ts/utils@0.103.0': + resolution: {integrity: sha512-vYPb0vySLq0PZ0ytdWQnSeG1KjRBB90Fet114iS23sAd+Q1DZ75SHcgekPHkA0Hn4mTrE0Blch+d9tLnLeLrdw==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + peerDependencies: + vitest: 3.0.9 + + '@fuel-ts/versions@0.103.0': + resolution: {integrity: sha512-+omgQvyrOY3e41CdeFBbYl3wWkAimLIKx4lwT+wWWH+/Hxqx5J6oy102ETlr7r93u25X2a68K+USZlR6v7JXMA==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + hasBin: true + + '@fuels/vm-asm@0.62.0': + resolution: {integrity: sha512-W2XoFIzHtHmWaTKzGKhDzkuQHCJO3j/wU3x1ngyleucsjCv3nUUqkRoSPhEo6wTGRqwiq6cmoqOk+FEi4vmxww==} + '@gemini-wallet/core@0.3.2': resolution: {integrity: sha512-Z4aHi3ECFf5oWYWM3F1rW83GJfB9OvhBYPTmb5q+VyK3uvzvS48lwo+jwh2eOoCRWEuT/crpb9Vwp2QaS5JqgQ==} peerDependencies: @@ -5413,6 +6109,10 @@ packages: '@internationalized/string@3.2.10': resolution: {integrity: sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -5637,12 +6337,18 @@ packages: '@ledgerhq/hw-transport-mocker@6.35.0': resolution: {integrity: sha512-ib6CKTYh+fbwahgAuKUrVvl9zHSf9rfLcnalcWQvlY9A4NQ4dkz0P+QlLaR8GeHVNPg29nhzpCQN7+hRlzSU+g==} + '@ledgerhq/hw-transport-web-ble@6.35.0': + resolution: {integrity: sha512-nc4FAWZ0GtZO9ydDCKlvDzOEVdUXlGL/0nWPIOeCE5xxuXug5iR0uhC4Bl0qwlUCEnhewmXwLJsCFcCdr/N4QQ==} + '@ledgerhq/hw-transport-webhid@6.36.0': resolution: {integrity: sha512-1mKWm3LyGOgmaYlAiqbmaGoupOZHbj2Kow5sXLxKZzQa4kFvQuUdinYOWhs7T8hqJyAkz9WlHYyKM7aIs8kjNg==} '@ledgerhq/hw-transport-webusb@6.29.12': resolution: {integrity: sha512-mMGKPYAUz9MNcURe+hSTSHwqPwCli6D0lCl15Z4hDOpcqhZ26vwoeWVKeQp53NNCetHOl0lauPkN43Gt9pIggg==} + '@ledgerhq/hw-transport-webusb@6.35.0': + resolution: {integrity: sha512-8fBNqzQnyR3pKLE2AVxCAc54nHMEKz9/eAYLDENVRuDp+A23lBJYWErxSyoZ6riCM19jIhRvtHjB0o4PXpa2tg==} + '@ledgerhq/hw-transport@6.31.12': resolution: {integrity: sha512-FO5LRIXYC8ELtaohlO8qK0b3TfHUNBZ3+CXKPHiHj2jJwrxPf4s5kcgBYrmzuf1C/1vfrMOjzyty6OgrMIbU6Q==} @@ -5670,6 +6376,10 @@ packages: '@libp2p/interface@3.3.0': resolution: {integrity: sha512-SXahM/4IgpiFKTtocbXYSTA1wEUVjktmT1yBCzBbc2Vgsu1VBCg6SvsFmXo2Hbeyev3D8EU+y3uaCKDB43C/Vg==} + '@lifeomic/axios-fetch@3.1.0': + resolution: {integrity: sha512-C6ceAnh8W19KuekFsCiK1A+AMBirQFTnoEqMZQ7HE6VXJ18zjGofdXxLU8RTo2gZp/yZK5ufmPwIvRtejj1gxg==} + engines: {node: '>=12'} + '@lightsparkdev/core@1.5.3': resolution: {integrity: sha512-4bh42oEeJrLYUAl19p5Adxqmn3VvV7Z5Vpqn7ZF9XZwi5nPKeXXc3DMo0+GabJhlUvrk8AtNaXD1nVCMTFLTzA==} engines: {node: '>=18'} @@ -5906,6 +6616,82 @@ packages: '@multiformats/multiaddr@13.0.3': resolution: {integrity: sha512-mEqqJ4r3a/uuFMTpRkU316wGNIDQNhuVWpm+ebKTQeYsfv9jXbPONWM6VVnj3KGUrwfsX7GZOyp4TFqEA2SPCw==} + '@multiversx/sdk-bls-wasm@0.3.5': + resolution: {integrity: sha512-c0tIdQUnbBLSt6NYU+OpeGPYdL0+GV547HeHT8Xc0BKQ7Cj0v82QUoA2QRtWrR1G4MNZmLsIacZSsf6DrIS2Bw==} + engines: {node: '>=8.9.0'} + + '@multiversx/sdk-core@15.4.1': + resolution: {integrity: sha512-1G/GS7fwED9Pl0NpwbcdXSi+986ey/OUGZycWuDCXPMMWx/qFQ0V5YMEotEqma8UM7uioVW3O5AnI7+EWOmsnQ==} + peerDependencies: + bignumber.js: ^9.0.1 + protobufjs: ^7.5.6 + + '@multiversx/sdk-core@16.0.0': + resolution: {integrity: sha512-qSsm85+3dfLrPY0uMBrLPcqfPNI/sCy7D4idF0QvvRbvauRIGS80N7tfFAolucQflK+rXgRpsR5fUyVmMhSpoQ==} + peerDependencies: + bignumber.js: ^9.0.1 + protobufjs: ^7.5.6 + + '@multiversx/sdk-dapp-ui@0.1.26': + resolution: {integrity: sha512-rf/QepdAJ+5nL98+RfcIfjJfOBsezehZsNxcHjf/uoleP3H0dWIR/A3RsoLo2Pg8OoMZQL3MttgLNFj/JKCqgA==} + + '@multiversx/sdk-dapp-utils@3.1.0': + resolution: {integrity: sha512-m3u2ymm+ME/+6Hbu0iNmi+CQbuImNOc8GeX+KtdW0tihQrMwEuWIECj/U5EapEFbGMlNbPb/YmwiMMfQ1bk/fQ==} + peerDependencies: + '@multiversx/sdk-core': ^14.0.0 || ^15.0.0 + bignumber.js: ^9.x + + '@multiversx/sdk-dapp@5.7.3': + resolution: {integrity: sha512-un5Hz76E9nhN52ZQaF8kO35gmX3rnXbQF/gjH2C71rVlCD3zktKa+or8R7V3oLecIqo4suYh7no1OrJiDEBwbg==} + peerDependencies: + '@multiversx/sdk-core': ^14.x || ^15.x + '@multiversx/sdk-dapp-utils': ^3.x + axios: '>=1.20.0 <2' + bignumber.js: ^9.x + protobufjs: ^7.5.5 + + '@multiversx/sdk-extension-provider@5.1.2': + resolution: {integrity: sha512-jxu18WKnc8ZLaLdl6ePcLStG2aZB1f9W0m2wPJt2UbHnPMhnk/okoYZYWlHfiF/L8WZGzQn8VXwcmras/vdwlw==} + peerDependencies: + '@multiversx/sdk-core': ^14.0.0 || ^15.0.0 + + '@multiversx/sdk-hw-provider@8.2.0': + resolution: {integrity: sha512-WSPv72ykmPj/H9swxkD8lBbOSjnFAClGMxKntgDy5L9Uj3nUrEDbJRo0btW9ocJxRCVmkkcNoy2ze/Vp2AZngQ==} + peerDependencies: + '@multiversx/sdk-core': ^14.0.0 || ^15.0.0 + + '@multiversx/sdk-native-auth-client@2.0.1': + resolution: {integrity: sha512-J4RxpsfNnWBKOPz0vvhnReSf8V6mci11Pe3NT2awhrpBhtAvkSBUGIzgoGVf/eumrhyReRmKyMjo+u4UhGokbg==} + peerDependencies: + axios: '>=1.20.0 <2' + + '@multiversx/sdk-transaction-decoder@1.0.2': + resolution: {integrity: sha512-j43QsKquu8N51WLmVlJ7dV2P3A1448R7/ktvl8r3i6wRMpfdtzDPNofTdHmMRT7DdQdvs4+RNgz8hVKL11Etsw==} + + '@multiversx/sdk-wallet-connect-provider@6.1.5': + resolution: {integrity: sha512-jDnLoTpr8LA+De2bp5Wv1mFmfguk5cWav9QVCWMZt9SAkYcdoskF8dYJt5Rqs40UhxPS2yy+YrRBN1KWBhhC2w==} + peerDependencies: + '@multiversx/sdk-core': ^14.0.0 || ^15.0.0 + + '@multiversx/sdk-web-wallet-cross-window-provider@3.2.2': + resolution: {integrity: sha512-CBMrf+oNKoBa/YFDTqzItLrERaW4n0pJIlGsP67H0Vr6bUlFLIlc30qsOGvWNoZvMDeDxvhvR7crayypETJSJw==} + engines: {node: '>=18'} + peerDependencies: + '@multiversx/sdk-core': ^14.0.0 || ^15.0.0 + + '@multiversx/sdk-web-wallet-iframe-provider@4.0.1': + resolution: {integrity: sha512-zIIKX4G1xYk7XSBQsHuihOag7pUAVGMZ1q3vug9XpwBwcQvgcSwC5RHzrYQusPpVdkTHIn3qa3JHqjwNj1VtGA==} + engines: {node: '>=18'} + peerDependencies: + '@multiversx/sdk-core': ^14.0.0 || ^15.0.0 + '@multiversx/sdk-web-wallet-cross-window-provider': ^3.x + + '@multiversx/sdk-webview-provider@3.2.7': + resolution: {integrity: sha512-cPDJ4N7i9IDYP+z/M/vXzgjCiAr9JSQof4yp2nzQ67EwPyFPW4fenmHvzyhtD3KYdGc48OmJ0Nk/zv2+nwQnig==} + peerDependencies: + '@multiversx/sdk-core': ^14.0.0 || ^15.0.0 + '@multiversx/sdk-web-wallet-cross-window-provider': ^3.x + '@mysten/bcs@2.1.2': resolution: {integrity: sha512-fv6lyuZMTHN0xRlu4GYiV/UXUrdJDOHbXm1KfZYEU0sy/t+22yTrTVtWnAQv1Km7eyB3qHhwIrAG2aem4JMocw==} @@ -6158,6 +6944,10 @@ packages: resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.2': + resolution: {integrity: sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==} + engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.7': resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} @@ -6170,9 +6960,18 @@ packages: resolution: {integrity: sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==} engines: {node: '>= 20.19.0'} + '@noble/ed25519@1.7.3': + resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} + '@noble/ed25519@3.1.0': resolution: {integrity: sha512-pfcObRY3CtvwfaG9Mt5XqZdKmAQppl37tHUeuBhDUbiwJBCVY4/A4lbMvb1xKhMDx96AqAqZpMWuBX1HulhX4g==} + '@noble/hashes@1.1.5': + resolution: {integrity: sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==} + + '@noble/hashes@1.3.0': + resolution: {integrity: sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==} + '@noble/hashes@1.3.2': resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} engines: {node: '>= 16'} @@ -6217,6 +7016,9 @@ packages: resolution: {integrity: sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A==} engines: {node: '>= 20.19.0'} + '@noble/secp256k1@1.7.1': + resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} + '@noble/secp256k1@1.7.2': resolution: {integrity: sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ==} @@ -6335,6 +7137,10 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@playwright/test@1.63.0': resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} engines: {node: '>=20'} @@ -7265,6 +8071,9 @@ packages: viem: '>=2.55.13' wagmi: '>=2.19.5' + '@reown/appkit-common@1.7.17': + resolution: {integrity: sha512-zfrlNosQ5XBGC7OBG56+lur0nJWCdRKoWVlUnr0dCVVfBmHIgdhFkRNzDqrX/zGqg4OoWDQLO7qaGiijRskfBQ==} + '@reown/appkit-common@1.7.8': resolution: {integrity: sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==} @@ -7277,6 +8086,9 @@ packages: '@reown/appkit-common@1.8.24': resolution: {integrity: sha512-BoAGwKwGm4WJ4nkOPk4OsIVGf9NmdINbk5QaCo50j6QJIaIAcL7GVnsV322NBiSuBxobmwbZ3lIezsZCd3G45A==} + '@reown/appkit-controllers@1.7.17': + resolution: {integrity: sha512-rYgXf3nAzxgu1s10rSfibpAqnm/Y3wyY47v6BpN98Y57NArWqxYXhBtdRQL1ZKpSTV9OmrzwMxPNKePOmFgxZQ==} + '@reown/appkit-controllers@1.7.8': resolution: {integrity: sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==} @@ -7289,6 +8101,9 @@ packages: '@reown/appkit-controllers@1.8.24': resolution: {integrity: sha512-ZEB17sTYfNSMGO5lkWWGn4Q53N0tOSFAqHZt4f5Sxj8certjFXNAcKNjf0Z+bxNgU4Pjv3tisZ+6S4ShOxe6Zw==} + '@reown/appkit-pay@1.7.17': + resolution: {integrity: sha512-RukQ5oZ+zGzWy9gu4butVcscZ9GB9/h6zmQFXDo9qkAbOicwZKaLR5XMKrjLQIYisu+ODV/ff6NuxnUYs+/r9Q==} + '@reown/appkit-pay@1.7.8': resolution: {integrity: sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==} @@ -7301,6 +8116,9 @@ packages: '@reown/appkit-pay@1.8.24': resolution: {integrity: sha512-/djGjg2xfSLDxI9Bk0DGxplgzg9npFCa5bY+OUTXaSCBPDNFKMBD8e/EAq+UL43fv/wfJewuK0F4FiLnlVDR8w==} + '@reown/appkit-polyfills@1.7.17': + resolution: {integrity: sha512-vWRIYS+wc2ByWKn76KMV7zxqTvQ+512KwXAKQcRulu13AdKvnBbr0eYx+ctvSKL+kZoAp9zj4R3RulX3eXnJ8Q==} + '@reown/appkit-polyfills@1.7.8': resolution: {integrity: sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==} @@ -7313,6 +8131,9 @@ packages: '@reown/appkit-polyfills@1.8.24': resolution: {integrity: sha512-HQr6p5lvx73Q+NHSNQpw9IHuxkpT9ri+z5Hpa/W3hrFsA+Z20x5+H3oQoUivEqQGV2LVVQ75v6h7dfWBnRKy1g==} + '@reown/appkit-scaffold-ui@1.7.17': + resolution: {integrity: sha512-7nk8DEHQf9/7Ij8Eo85Uj1D/3M9Ybq/LjXyePyaGusZ9E8gf4u/UjKpQK7cTfMNsNl4nrB2mBI9Tk/rwNECdCg==} + '@reown/appkit-scaffold-ui@1.7.8': resolution: {integrity: sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==} @@ -7325,6 +8146,14 @@ packages: '@reown/appkit-scaffold-ui@1.8.24': resolution: {integrity: sha512-bA5fJQAYAejwWeNUvOQM3luGE1mK9jfzvixx/tNZnatBOavUX07793hXrXksi82QpxOM/H9notgwGGeryDK00w==} + '@reown/appkit-siwx@1.7.17': + resolution: {integrity: sha512-frTTDnj5111+ZNNyHmEWeXiX0IWFlRhP240kmxKTamLElc2PdLUfQq/1yX8Y3bUBHryISjcQYzEtWSEI2oRYKA==} + peerDependencies: + lit: 3.3.0 + + '@reown/appkit-ui@1.7.17': + resolution: {integrity: sha512-7lscJjtFZIfdcUv5zAsmgiFG2dMziQE0IfqY3U/H5qhnGW8v4ITcTi1gNS3A4lQrNDbcA083LecfVdyKnTdi1A==} + '@reown/appkit-ui@1.7.8': resolution: {integrity: sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==} @@ -7337,6 +8166,14 @@ packages: '@reown/appkit-ui@1.8.24': resolution: {integrity: sha512-Ex4/s9TtFZfrj0vjNBeH3uy23bIkZbOikcfJ3rInjGAN2ujyrb0ed9ZUWaiNTveVh06hjglYdgQ7B6nYDTBxSw==} + '@reown/appkit-universal-connector@1.7.17': + resolution: {integrity: sha512-2LqcKuEURwoHFBYE+6BdsUsPQ5bCN8xXuqxGJeEkAJ95apXTWyLLlpadVofKRh7dzM4lf0Uam8NpWogYWPxnnQ==} + + '@reown/appkit-utils@1.7.17': + resolution: {integrity: sha512-QWzHTmSDFy90Bp5pUUQASzcjnJXPiEvasJV68j3PZifenTPDCfFW+VsiHduWNodTHAA/rZ12O3uBQE+stM3xmQ==} + peerDependencies: + valtio: 2.1.5 + '@reown/appkit-utils@1.7.8': resolution: {integrity: sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==} peerDependencies: @@ -7357,6 +8194,9 @@ packages: peerDependencies: valtio: 2.1.7 + '@reown/appkit-wallet@1.7.17': + resolution: {integrity: sha512-tgIqHZZJISGCir0reQ/pXcIKXuP7JNqSuEDunfi5whNJi6z27h3g468RGk1Zo+MC//DRnQb01xMrv+iWRr8mCQ==} + '@reown/appkit-wallet@1.7.8': resolution: {integrity: sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==} @@ -7369,6 +8209,9 @@ packages: '@reown/appkit-wallet@1.8.24': resolution: {integrity: sha512-MgJ5H25m7PgHwPzzv5nKnzdrMI6F+9zU3oIYsIWT/I/h4EfnN2COt+WuI7uX2Z2J2pWyHi0uDrysRgHU7DPAOw==} + '@reown/appkit@1.7.17': + resolution: {integrity: sha512-gME4Ery7HGTNEGzLckWP7qfD2ec/1UEuUkcGskGeisUnGcAsPH9z2deFFX1szialsgzTNU4/H5ZGdWqZQA8p2w==} + '@reown/appkit@1.7.8': resolution: {integrity: sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==} @@ -7496,11 +8339,21 @@ packages: cpu: [arm64] os: [android] + '@rollup/rollup-darwin-arm64@4.44.0': + resolution: {integrity: sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-arm64@4.63.6': resolution: {integrity: sha512-fwaSNrSHp9PJuEh4bKtd0mYC+G3FeVDjSTmsSiSlIYu7TFCpxrfB6lk7Ao8cIbswFyklPB1ZDUV8LkLBM15VWw==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-x64@4.44.0': + resolution: {integrity: sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.63.6': resolution: {integrity: sha512-QR2tx26gCeGh4eAGnUasfnJuRc+d7Vc0Jo1/fYtKGuiKqCuhbVpbNEvqgYGBwGDZGyS04jj+zdHi9W8uvb1Sdw==} cpu: [x64] @@ -7528,12 +8381,24 @@ packages: os: [linux] libc: [musl] + '@rollup/rollup-linux-arm64-gnu@4.44.0': + resolution: {integrity: sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-arm64-gnu@4.63.6': resolution: {integrity: sha512-inQYLPVIUvYk+s7zpxQldqwrajUNs+t4TJjPGzsdPTvsT8gsQXVcW/sQFolHZ3YSWVk0nMMwU7TtRi0Ow+QMuw==} cpu: [arm64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-arm64-musl@4.44.0': + resolution: {integrity: sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-arm64-musl@4.63.6': resolution: {integrity: sha512-sH+0MV1HmDC1q7+bQnlN9ywKBmHqkA2EdD/hHYrrz654ybc+vNC1FRNyzuYKicMQVtZZ4X2jz0osJYmsOtnj4g==} cpu: [arm64] @@ -7582,12 +8447,24 @@ packages: os: [linux] libc: [glibc] + '@rollup/rollup-linux-x64-gnu@4.44.0': + resolution: {integrity: sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-x64-gnu@4.63.6': resolution: {integrity: sha512-Ezx2E5D6Siz805j41x91JiGbzuCm8i2U98U37SQ7ytY737+wsJq+P9nPL6UmqDww491zO7MTivYOTrtmTTeCSw==} cpu: [x64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-x64-musl@4.44.0': + resolution: {integrity: sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==} + cpu: [x64] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-x64-musl@4.63.6': resolution: {integrity: sha512-8EZ1Q3PB7yUjvR4MbFAnCBF1wF0XRe5QQqbtlJXP7ZX5C8iYlSQJugUhj9oS0gbUP4jp9qGhaIgFnV9Nvod86Q==} cpu: [x64] @@ -7604,6 +8481,11 @@ packages: cpu: [arm64] os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.44.0': + resolution: {integrity: sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.63.6': resolution: {integrity: sha512-D3eEyLSIim5WF5fHbwnx+ryk0delN7fJyBrDTO3wVCCNBZ/cA8aCGwLS3SiuK+vRI4E7dqikAlStGuR20dBMuw==} cpu: [arm64] @@ -7619,6 +8501,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.44.0': + resolution: {integrity: sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.63.6': resolution: {integrity: sha512-0RAxO1pS/6lY1mSoD2Apm+6aPyfUlm+OWKgncIdj/eVN9I+pDY3lKgD7xsUNFei1u7MZQETGOo6m8POXMDp53A==} cpu: [x64] @@ -7672,6 +8559,9 @@ packages: '@scure/base@2.4.0': resolution: {integrity: sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==} + '@scure/bip32@1.1.3': + resolution: {integrity: sha512-dSH3+LCWONlSNQuF34xZrG6Xas7tp2jSSqHb/pMfXWM0vKE4JZOtK3uJfoWouUVW5IGlls75HkXmYLldZ8ySgQ==} + '@scure/bip32@1.4.0': resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} @@ -7687,6 +8577,9 @@ packages: '@scure/bip32@2.4.0': resolution: {integrity: sha512-i3DS0CptAocyvqE4n3SUkpzeQK4vJMFwWLofTwRiiKo2aWojBOfyMCgfKw9HVpO6fSY5AK86sHS/Uzn8kK9Few==} + '@scure/bip39@1.1.0': + resolution: {integrity: sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==} + '@scure/bip39@1.3.0': resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} @@ -9132,6 +10025,45 @@ packages: '@stablelib/xsalsa20@2.0.1': resolution: {integrity: sha512-qFfcqrIqcZo40bAEpDSAbIcfE+8WFo+4HtzmlO+G8ObKukIaWGIROvz64NEYv50lQodY5OoQV3koC2EXXI9ofw==} + '@stacks/auth@7.6.0': + resolution: {integrity: sha512-uxhoX0ITDdxxRAWx7KE0WdQKLansoBSBLuVjGbrs/CKsrkNpwPerdw+9RFlP3U0mK53HLyKZlCX37DWqthG1SA==} + + '@stacks/common@6.16.0': + resolution: {integrity: sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==} + + '@stacks/common@7.6.0': + resolution: {integrity: sha512-VUW8WDmZP4wXUNWVvpFerV+7LQ0PqIn8YOs8AjSUMA7ZOXIRLxJHoKxjfiHzJqazy7KlyryPW4kSNBCr7tLqhQ==} + + '@stacks/connect-ui@8.1.2': + resolution: {integrity: sha512-C3T1QmEGJocnmsamQnKJvlvHFfjr/FDebLSokrAAwTr6x76JPS5xuACB7u3a7+vVMUdINV+pFvfn442oic2R5w==} + + '@stacks/connect@8.2.7': + resolution: {integrity: sha512-O9S4CCeMHJS1xnE+bPEca6zexVDgpyz4ibyi9G0qysaoyFj6eJah+xFfiuKShie4lm9CTHN+6eqVWrjcanvRxQ==} + + '@stacks/encryption@7.6.0': + resolution: {integrity: sha512-1D2yazEcC5blc/xZkCPXQRbiAozXiVisLdxdFF3bVvNocUZ7yWkW7SOhhNve8RJ0Yf0XBkG6IbnBa3F1vyOyXA==} + + '@stacks/network@6.17.0': + resolution: {integrity: sha512-numHbfKjwco/rbkGPOEz8+FcJ2nBnS/tdJ8R422Q70h3SiA9eqk9RjSzB8p4JP8yW1SZvW+eihADHfMpBuZyfw==} + + '@stacks/network@7.6.0': + resolution: {integrity: sha512-Blm85nsEbvJoFIFaDp0QE9WMC0Hf+o3Yb0oIDwWu3PPgpVSdndb/bJQUeT6eJTs0ibS26A2E/5d5L0fb4Ff/lA==} + + '@stacks/profile@7.6.0': + resolution: {integrity: sha512-gFXKl3eLi1vWiCcZ3F5QFBq/X+vYn3TKSbe1iHRW4VdhIB+WFedpJ4h2WrqTzRepRNQzd2z6kl2pKuyD1x+fuA==} + + '@stacks/storage@7.6.0': + resolution: {integrity: sha512-gNJEwqDzP0Wwfuocy98GjMEn2bnKJYGYbTxJ3uK04v7gks+M58I0cTMzC38CkJWLaVO1xRtNZ3VfVi5zybabkQ==} + + '@stacks/transactions@6.17.0': + resolution: {integrity: sha512-FUah2BRgV66ApLcEXGNGhwyFTRXqX5Zco3LpiM3essw8PF0NQlHwwdPgtDko5RfrJl3LhGXXe/30nwsfNnB3+g==} + + '@stacks/transactions@7.6.0': + resolution: {integrity: sha512-s7F7eJtQVnZoB79j8pY9SLKhlvvdYZZD1NSDRWrFjzSJTDmxptL8c50S563WFja6KOhFOgg1jd+p0XWTxFAVyA==} + + '@stacks/wallet-sdk@7.6.0': + resolution: {integrity: sha512-NsvGOBB/AlpZEIGsjH7ltc2XrmiHw6jzvtR+fkxQAmOxsu6EaZzdnaq4S9jRcxUOzMmQWCYB0Q26FZyY298XJQ==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -9189,6 +10121,30 @@ packages: engines: {node: '>=22.12.0'} hasBin: true + '@stencil/core@4.45.2': + resolution: {integrity: sha512-CNkPVZsqyNMjFMX9bEkMJsvBbQxjLw480OaoamoqiLPdQCFjJQReeOjnWX1A5ivSn3UYEIJ6MoN5VXDkaHGt8Q==} + engines: {node: '>=16.0.0', npm: '>=7.10.0'} + hasBin: true + + '@stencil/react-output-target@1.6.2': + resolution: {integrity: sha512-hnN0EkF/aWIqIUZhJ4rUszqKzzS8TSThSXNxkK9xLtlKIUef/sep8ekRlHOelq7RiUBPP4eTBv6d5N3X4T35KQ==} + peerDependencies: + '@stencil/core': '>=3 || >=4.0.0 || ^5.0.0 || >=5.0.0-alpha.17 <5.0.0-next.0' + react: ^18 || ^19 + react-dom: ^18 || ^19 + + '@stencil/vue-output-target@0.14.3': + resolution: {integrity: sha512-2/u1rP+6ob3ymvIWGAYZKIm9LlpaJCgmoWHGchkOBOVoReCHfyK7wvQ0xtatOgrS7PCMVhKpTRwzine1xm9gjA==} + peerDependencies: + '@stencil/core': '>=2.0.0 || >=3 || >=4.0.0 || ^5.0.0 || >=5.0.0-alpha.17 <5.0.0-next.0' + vue: ^3.4.38 + vue-router: ^4.5.0 || ^5.0.0 + peerDependenciesMeta: + '@stencil/core': + optional: true + vue-router: + optional: true + '@stricahq/bip32ed25519@1.1.2': resolution: {integrity: sha512-YfjVRvJzTWodEbrl4TQlL9Isxg1mrZr9hoPxY75JpgFAS0l6MT8R6Lev21TbQtIJy5LfHMUoK25UICKXFmYvKQ==} @@ -9489,6 +10445,9 @@ packages: '@trpc/server@10.45.4': resolution: {integrity: sha512-5MuyK5sDuFVGHOT9EMVHgRjP5I5j3RqGymcb5D47UOp8tzn6LH0g1lEgYrAsOZ12vmk1gpxMw/v/y4xHHK/Qdg==} + '@ts-morph/common@0.29.0': + resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} + '@turnkey/api-key-stamper@0.4.5': resolution: {integrity: sha512-8UeYt/2WtMrK2uSFzjiRXdCVc9SmKlMVuA4f1Z+SlxcsW0wlEpNM/7bd8N4VVVAjoE8Yy50Vhk3ylfQFtlaKsQ==} engines: {node: '>=18.0.0'} @@ -9614,6 +10573,9 @@ packages: '@types/base32-encoding@1.0.2': resolution: {integrity: sha512-6kXiZ8gETqBU/B9ddcw15nwacX4iX9mLZTU0kghWK5u+OdjfJg6vxHh/vXoURWTyLSzs2jKgcq1lS3S/Tvl4mw==} + '@types/bn.js@5.1.6': + resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==} + '@types/bn.js@5.2.0': resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==} @@ -9809,12 +10771,18 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node@11.11.6': resolution: {integrity: sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==} '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.20.5': resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==} @@ -9839,6 +10807,9 @@ packages: '@types/qrcode@1.5.6': resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/qs@6.9.10': + resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} + '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: @@ -10214,6 +11185,10 @@ packages: resolution: {integrity: sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==} engines: {node: '>=18'} + '@walletconnect/core@2.21.5': + resolution: {integrity: sha512-CxGbio1TdCkou/TYn8X6Ih1mUX3UtFTk+t618/cIrT3VX5IjQW09n9I/pVafr7bQbBtm9/ATr7ugUEMrLu5snA==} + engines: {node: '>=18'} + '@walletconnect/core@2.23.0': resolution: {integrity: sha512-W++xuXf+AsMPrBWn1It8GheIbCTp1ynTQP+aoFB86eUwyCtSiK7UQsn/+vJZdwElrn+Ptp2A0RqQx2onTMVHjQ==} engines: {node: '>=18.20.8'} @@ -10222,6 +11197,10 @@ packages: resolution: {integrity: sha512-yTyymn9mFaDZkUfLfZ3E9VyaSDPeHAXlrPxQRmNx2zFsEt/25GmTU2A848aomimLxZnAG2jNLhxbJ8I0gyNV+w==} engines: {node: '>=18.20.8'} + '@walletconnect/core@2.23.9': + resolution: {integrity: sha512-ws4WG8LeagUo2ERRo02HryXRcpwIRmCQ3pHLW5gWbVReLXXIpgk6ZAfID3fEGHevIwwnHSGww+nNhNpdXyiq0g==} + engines: {node: '>=18.20.8'} + '@walletconnect/core@2.25.0': resolution: {integrity: sha512-ZcZauyQtSBhwwIbXIVTojcg/iWf1QLnf06zSJNGqHAJHHvWedg2Hpj+E2hinKRkPEqeaqf/ltkwOBOpF1Lcuzg==} engines: {node: '>=18.20.8'} @@ -10340,12 +11319,19 @@ packages: resolution: {integrity: sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==} deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' + '@walletconnect/sign-client@2.21.5': + resolution: {integrity: sha512-IAs/IqmE1HVL9EsvqkNRU4NeAYe//h9NwqKi7ToKYZv4jhcC3BBemUD1r8iQJSTHMhO41EKn1G9/DiBln3ZiwQ==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' + '@walletconnect/sign-client@2.23.0': resolution: {integrity: sha512-Nzf5x/LnQgC0Yjk0NmkT8kdrIMcScpALiFm9gP0n3CulL+dkf3HumqWzdoTmQSqGPxwHu/TNhGOaRKZLGQXSqw==} '@walletconnect/sign-client@2.23.7': resolution: {integrity: sha512-SX61lzb1bTl/LijlcHQttnoHPBzzoY5mW9ArR6qhFtDNDTS7yr2rcH7rCngxHlYeb4rAYcWLHgbiGSrdKxl/mg==} + '@walletconnect/sign-client@2.23.9': + resolution: {integrity: sha512-Xj+hw4E6mGRyhCdVOT/RMgnG+up/Y3v0ho5PlkVozvXWeVSqHNh9DmjLuU97a7OACoGd/oHBF6g3NVqD7MgCMQ==} + '@walletconnect/sign-client@2.25.0': resolution: {integrity: sha512-oOnOZpaoMTjzPlj+PY530zBc0wLKMH1qkFStu+UbfZ162iPQcikE29QXb0bCKiKgqiRZUWacBmsQBdLpYFVhOg==} @@ -10368,6 +11354,9 @@ packages: '@walletconnect/types@2.21.1': resolution: {integrity: sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==} + '@walletconnect/types@2.21.5': + resolution: {integrity: sha512-kpTXbenKeMdaz6mgMN/jKaHHbu6mdY3kyyrddzE/mthOd2KLACVrZr7hrTf+Fg2coPVen5d1KKyQjyECEdzOCw==} + '@walletconnect/types@2.23.0': resolution: {integrity: sha512-9ZEOJyx/kNVCRncDHh3Qr9eH7Ih1dXBFB4k1J8iEudkv3t4GhYpXhqIt2kNdQWluPb1BBB4wEuckAT96yKuA8g==} @@ -10388,6 +11377,10 @@ packages: resolution: {integrity: sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==} deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' + '@walletconnect/universal-provider@2.21.5': + resolution: {integrity: sha512-SMXGGXyj78c8Ru2f665ZFZU24phn0yZyCP5Ej7goxVQxABwqWKM/odj3j/IxZv+hxA8yU13yxaubgVefnereqw==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' + '@walletconnect/universal-provider@2.23.7': resolution: {integrity: sha512-6UicU/Mhr/1bh7MNoajypz7BhigORbHpP1LFTf8FYLQGDqzmqHMqmMH2GDAImtaY2sFTi2jBvc22tLl8VMze/A==} @@ -10406,12 +11399,18 @@ packages: '@walletconnect/utils@2.21.1': resolution: {integrity: sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==} + '@walletconnect/utils@2.21.5': + resolution: {integrity: sha512-RSPSxPvGMuvfGhd5au1cf9cmHB/KVVLFotJR9ltisjFABGtH2215U5oaVp+a7W18QX37aemejRkvacqOELVySA==} + '@walletconnect/utils@2.23.0': resolution: {integrity: sha512-bVyv4Hl+/wVGueZ6rEO0eYgDy5deSBA4JjpJHAMOdaNoYs05NTE1HymV2lfPQQHuqc7suYexo9jwuW7i3JLuAA==} '@walletconnect/utils@2.23.7': resolution: {integrity: sha512-3p38gNrkVcIiQixVrlsWSa66Gjs5PqHOug2TxDgYUVBW5NcKjwQA08GkC6CKBQUfr5iaCtbfy6uZJW1LKSIvWQ==} + '@walletconnect/utils@2.23.9': + resolution: {integrity: sha512-C5TltCs8UPypNiteYnKSv8+ZDK2EjVDyXCxN6kA9bkA+j6KGsNIV7l9MUA8WBAvE5Gi5EcBdhD3R9Hpo/1HHqQ==} + '@walletconnect/utils@2.25.0': resolution: {integrity: sha512-vNjBaO18BTS97F/SIJt6k/d3l0ngk5hGPEnouRNYbeE/KeG8xzQNZB07iAujhFWS2NNlPQ8/jqYz8PVpeFS2fw==} @@ -10436,6 +11435,10 @@ packages: '@webext-core/match-patterns@2.0.0': resolution: {integrity: sha512-EsyMMKfQuSE4rWCjP/TifwvUWlhC/OhiGn5OK3p9F0d57UmtLib0nzguzPKyxM1/kZV7SkE+jYXkE8gsdPRLmw==} + '@wharfkit/antelope@4.0.2': + resolution: {integrity: sha512-XDkPghnEpfNqmFw3dT3l4WjjS+HesfjdweV7281UfwINeglSY4lDTM5FaVIf7hWCm0QAHM1nUrDPJxXi6hl7bQ==} + engines: {node: '>=20.19.0'} + '@wxt-dev/browser@0.3.4': resolution: {integrity: sha512-NhLuaIz0LBwudxexRN/siiUOvVvwXhvYH6zM2GARG7Ag5UEpgpQV556vi7nzWPwR2ayBHD97mlodqd5OiOMg4Q==} @@ -10456,6 +11459,17 @@ packages: resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==} engines: {node: '>=14.6'} + '@xrplf/isomorphic@1.0.2': + resolution: {integrity: sha512-ncZUdMXr6VlSXtdoiDi0jTH+gBrgGxwVeEidhoegII3PmyErbQsyj6e+j7acmR4LW/lvBkPkzb9QzRfJH0n3rA==} + engines: {node: '>=18.0.0'} + + '@xrplf/mpt-crypto@0.1.1': + resolution: {integrity: sha512-Wv9wbk60DRGWvI+B9DITwfWygCWDfEjv8yWxiLJyDf/G8UPkfhVlUKIflF9nMaBFMygPRkXrpO1jNqTj6/vJow==} + engines: {node: '>= 18'} + + '@xrplf/secret-numbers@3.0.0': + resolution: {integrity: sha512-qpGhAZXv5noMDjCtfzq5NK0y5rrdwTVjKhhPcAYSE+a/gogBOgqdpCKyieprVVPCnmVmJnGeRoZKBAqpCGegsA==} + '@xstate/react@1.6.3': resolution: {integrity: sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ==} peerDependencies: @@ -10747,6 +11761,9 @@ packages: async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -11050,6 +12067,10 @@ packages: resolution: {integrity: sha512-3hf42BysHnUqmZO7mK6e5X/hs1AvyEJIhdVLbG/Mxn/fhFnhGxOO37mWbMHg1RT4TxqcPKXgqj9/bp1YG0GBXA==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} + base64-arraybuffer@0.2.0: + resolution: {integrity: sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -11058,6 +12079,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bech32@1.1.4: + resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} + bech32@2.0.0: resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} @@ -11087,6 +12111,9 @@ packages: bignumber.js@9.1.1: resolution: {integrity: sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==} + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -11108,6 +12135,9 @@ packages: bip32-path@0.4.2: resolution: {integrity: sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==} + bip322-js@2.0.0: + resolution: {integrity: sha512-wyewxyCLl+wudZWiyvA46SaNQL41dVDJ+sx4HvD6zRXScHzAycwuKEMmbvr2qN+P/IIYArF4XVqlyZVnjutELQ==} + bip322-js@4.0.0: resolution: {integrity: sha512-84DYLDeOSFG8M9S49DIimmoZjYGiAkbRBkthUd0G8PQ3Fh8+878JlKBkk7dNwiE4RM/FZqlFGxxG91K3wcWJ3Q==} engines: {node: '>=20.19.0'} @@ -11125,6 +12155,9 @@ packages: bip39@3.1.0: resolution: {integrity: sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==} + bip66@1.1.5: + resolution: {integrity: sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==} + bip68@1.0.4: resolution: {integrity: sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==} engines: {node: '>=4.5.0'} @@ -11143,9 +12176,19 @@ packages: resolution: {integrity: sha512-ugLk9SR+2gvhpNA7kqQL1eVDKyBNo/rbr+df72nrZj4OOdZvQRHDVtyogPGXAU2MLQimLL7EbUdz95QGh6Th5Q==} engines: {node: '>=18.0.0'} + bitcoinjs-message@2.2.0: + resolution: {integrity: sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==} + engines: {node: '>=0.10'} + + blake2b-wasm@1.1.7: + resolution: {integrity: sha512-oFIHvXhlz/DUgF0kq5B1CqxIDjIJwh9iDeUUGQUcvgiGz7Wdw03McEO7CfLBy7QKGdsydcMCgO9jFNBAFCtFcA==} + blake2b-wasm@2.4.0: resolution: {integrity: sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==} + blake2b@2.1.3: + resolution: {integrity: sha512-pkDss4xFVbMb4270aCyGD3qLv92314Et+FsKzilCLxDz5DuZ2/1g3w4nmBbu6nKApPspnjG7JcwTjGZnduB1yg==} + blake2b@2.1.4: resolution: {integrity: sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==} @@ -11187,6 +12230,10 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + borc@2.1.2: + resolution: {integrity: sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==} + engines: {node: '>=4'} + borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -11236,6 +12283,9 @@ packages: brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + browserslist@4.29.3: resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -11262,6 +12312,10 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equals@1.0.4: + resolution: {integrity: sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==} + engines: {node: '>=0.10.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -11269,6 +12323,9 @@ packages: resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} engines: {node: '>=4.0'} + buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -11317,6 +12374,10 @@ packages: magicast: optional: true + c32check@2.0.0: + resolution: {integrity: sha512-rpwfAcS/CMqo0oCqDf3r9eeLgScRE3l/xHDCXhM3UyrfvIn7PrLq63uHh7yYbv8NzaZn5MVsVhIRpQ+5GZ5HyA==} + engines: {node: '>=8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -11531,6 +12592,10 @@ packages: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} + cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} + engines: {node: '>= 0.2.0'} + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -11557,6 +12622,9 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + collect-v8-coverage@1.0.3: resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} @@ -11598,6 +12666,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -11734,6 +12806,10 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} + cosmjs-types@0.11.0: + resolution: {integrity: sha512-kDSkgHpRTrg1413jCNehT3P21+EBxZWFMBr9JEzVfmPiNdtuwAoLAkCYo7c7i/pTakAwyHsXbxOg8kkD+AN33w==} + engines: {node: '>=20.19'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -12147,6 +13223,9 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + delimit-stream@0.1.0: + resolution: {integrity: sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -12324,6 +13403,10 @@ packages: dprint-node@1.0.8: resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==} + drbg.js@1.0.1: + resolution: {integrity: sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==} + engines: {node: '>=0.10'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -12334,6 +13417,9 @@ packages: duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + eciesjs@0.4.18: resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} @@ -12346,6 +13432,9 @@ packages: resolution: {integrity: sha512-q74N80jaqlSkOTx1Wki43KdQrGaz00CVHxU1fkTv4AJRCTu388uUJV+r4ZxaP1HKGLF7ygMic3lU7dt5o3r+Gg==} engines: {node: '>=20.0.0'} + ed25519-hd-key@1.1.2: + resolution: {integrity: sha512-/0y9y6N7vM6Kj5ASr9J9wcMVDTtygxSOvYX+PJiMD7VcxCx2G03V5bLRl8Dug9EgkLFsLhGqBtQWQRcElEeWTA==} + ed25519-hd-key@2.0.0: resolution: {integrity: sha512-xWAbQeTHA2DkyirTUYOaOaizcNY/CVNYwprMrnKn0GO1O4LKRXby1nUcj+blr2bT5z44IcyNpA1QPvzLVqHCsA==} @@ -12436,6 +13525,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encode-utf8@1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} @@ -12550,6 +13642,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.25.3: + resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} @@ -12637,6 +13734,10 @@ packages: resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==} engines: {node: '>=14.0.0'} + ethers@6.17.0: + resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==} + engines: {node: '>=14.0.0'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -12672,6 +13773,9 @@ packages: resolution: {integrity: sha512-D6bTRWh6KahHTK/m4WnjPQyEinNPf9eFLEZSEoj7d6fTibspnAVYfzHvirL7u/aoX5d9YYfIkBVAhmigUELk9w==} engines: {node: '>=20.0.0'} + evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -12991,6 +14095,9 @@ packages: fetch-nodeshim@0.4.10: resolution: {integrity: sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -13056,6 +14163,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + forge-light@1.1.4: resolution: {integrity: sha512-Nr0xdu93LJawgBZVU/tC+A+4pbKqigdY5PRBz8CXNm4e5saAZIqU2Qe9+nVFtVO5TWCHSgvI0LaZZuatgE5J1g==} engines: {node: '>= 6.13.0'} @@ -13136,6 +14247,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fuels@0.103.0: + resolution: {integrity: sha512-jV/EqE1/kibZzC1jqW4BhRyM7/ndX6+3W176crVs+260T48aa9rkNT1+TNqdmXewmjusqJ7La4GR1Cx7h/ZPcw==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + hasBin: true + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -13212,6 +14328,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -13235,6 +14356,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + google-protobuf@3.21.4: + resolution: {integrity: sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -13256,12 +14380,27 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql-request@6.1.0: + resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} + peerDependencies: + graphql: 14 - 16 + + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-ws@5.16.2: resolution: {integrity: sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==} engines: {node: '>=10'} peerDependencies: graphql: '>=0.11 <=16' + graphql@16.10.0: + resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + graphql@16.14.2: resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -13276,6 +14415,11 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + happy-dom@20.14.5: resolution: {integrity: sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==} engines: {node: '>=20.0.0'} @@ -13393,6 +14537,9 @@ packages: htm@3.1.1: resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + html-dom-parser@5.1.8: + resolution: {integrity: sha512-MCIUng//mF2qTtGHXJWr6OLfHWmg3Pm8ezpfiltF83tizPWY17JxT4dRLE8lykJ5bChJELoY3onQKPbufJHxYA==} + html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -13410,6 +14557,15 @@ packages: html-parse-stringify@3.1.0: resolution: {integrity: sha512-E0oAXcELOtsXe+BmpJ2EZyedbldPpriV5vICzEuo6xjC/D1lDukOI7KrpfQGF2Qc4wWEy0nk3bFORS2K5ZAhFQ==} + html-react-parser@5.2.17: + resolution: {integrity: sha512-m+K/7Moq1jodAB4VL0RXSOmtwLUYoAsikZhwd+hGQe5Vtw2dbWfpFd60poxojMU0Tsh9w59mN1QLEcoHz0Dx9w==} + peerDependencies: + '@types/react': 0.14 || 15 || 16 || 17 || 18 || 19 + react: 0.14 || 15 || 16 || 17 || 18 || 19 + peerDependenciesMeta: + '@types/react': + optional: true + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -13499,6 +14655,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -13536,6 +14695,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -13731,6 +14893,10 @@ packages: resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} engines: {node: '>=20'} + iso-url@0.4.7: + resolution: {integrity: sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==} + engines: {node: '>=10'} + iso-url@1.2.1: resolution: {integrity: sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==} engines: {node: '>=12'} @@ -13789,6 +14955,9 @@ packages: peerDependencies: react: ^19.0.0 + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jake@10.9.4: resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} engines: {node: '>=10'} @@ -14079,6 +15248,9 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json-text-sequence@0.1.1: + resolution: {integrity: sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==} + json2mq@0.2.0: resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} @@ -14114,6 +15286,10 @@ packages: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true + keccak@3.0.2: + resolution: {integrity: sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==} + engines: {node: '>=10.0.0'} + keccak@3.0.4: resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} engines: {node: '>=10.0.0'} @@ -14285,6 +15461,9 @@ packages: linkify-it@5.0.2: resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + lit-element@3.3.3: resolution: {integrity: sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==} @@ -14324,28 +15503,52 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.capitalize@4.2.1: + resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + lodash.inrange@3.3.6: + resolution: {integrity: sha512-NqOT/CVclLZKet0ALwg1joQ/XnfxePGf8L9WKvzOcTDAI0axG3Ej24qDGwLk0ejN4AVfFvs6vY5AWBB53o5piw==} + + lodash.isempty@4.4.0: + resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.range@3.2.0: + resolution: {integrity: sha512-Fgkb7SinmuzqgIhNhAElo0BL/R1rHCnhwSZf78omqSwvWqD0kD2ssOAutQonDKH/ldS8BxA72ORYI09qAY9CYg==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash.trimend@4.18.0: + resolution: {integrity: sha512-8w2M3nZAWLN1OX/6mTPCwRlZiD/LhVyPV9l7DEbkd9wybExvg9AcCjbD19swj6oVzX5hcMZHp3/Y1b4Sl3sHKg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -14761,6 +15964,11 @@ packages: engines: {node: '>=10'} hasBin: true + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -14815,6 +16023,9 @@ packages: nan@2.29.0: resolution: {integrity: sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==} + nanoassert@1.1.0: + resolution: {integrity: sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==} + nanoassert@2.0.0: resolution: {integrity: sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==} @@ -14855,6 +16066,9 @@ packages: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nice-grpc-client-middleware-retry@3.1.16: resolution: {integrity: sha512-8EbOCUZZ1Uq1pCfTD8dB2zMU1AYrilRvm9al5VSUi444eqhPhxElnI7iqP6ZY/ixb1NGsvMpfbU5FW/3L7DdFw==} @@ -15093,6 +16307,10 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-is@1.1.6: resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} engines: {node: '>= 0.4'} @@ -15217,6 +16435,22 @@ packages: typescript: optional: true + ox@0.7.1: + resolution: {integrity: sha512-+k9fY9PRNuAMHRFIUbiK9Nt5seYHHzSQs9Bj+iMETcGtlpS7SmBzcGSVUQO3+nqGLEiNK4598pHNFlVRaZbRsg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.8.1: + resolution: {integrity: sha512-e+z5epnzV+Zuz91YYujecW8cF01mzmrUtWotJ0oEPym/G82uccs7q0WDHTYL3eiONbTUEvcZrptAKLgTBD3u2A==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + ox@0.9.17: resolution: {integrity: sha512-rKAnhzhRU3Xh3hiko+i1ZxywZ55eWQzeS/Q4HRKLx2PqfHOolisZHErSsJVipGlmQKHW5qwOED/GighEw9dbLg==} peerDependencies: @@ -15289,6 +16523,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} @@ -15298,6 +16535,9 @@ packages: pako@2.2.0: resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + pako@3.0.2: + resolution: {integrity: sha512-uBv6IT2aT1A78iU6dpNEbf6+CyhlV/6g9JlJs9kpgjFGFhruIICVRysF/W0SLzXg5+hCl+KroH7e4YUyfEmgLg==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -15328,6 +16568,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} @@ -15350,6 +16593,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -15455,6 +16702,9 @@ packages: resolution: {integrity: sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==} engines: {node: '>=16.0.0'} + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + playwright-core@1.63.0: resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} engines: {node: '>=20'} @@ -15503,6 +16753,10 @@ packages: popmotion@11.0.5: resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==} + portfinder@1.0.32: + resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} + engines: {node: '>= 0.12.0'} + porto@0.2.35: resolution: {integrity: sha512-gu9FfjjvvYBgQXUHWTp6n3wkTxVtEcqFotM7i3GEZeoQbvLGbssAicCz6hFZ8+xggrJWwi/RLmbwNra50SMmUQ==} hasBin: true @@ -15686,6 +16940,9 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} @@ -15774,6 +17031,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -15801,6 +17062,9 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + ramda@0.30.1: + resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -16170,6 +17434,9 @@ packages: '@types/react': optional: true + react-property@2.0.2: + resolution: {integrity: sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==} + react-qrcode-logo@4.1.0: resolution: {integrity: sha512-drcieitJFX3u66XpFn+4PxsCzrQo5nAMXsyOtiuTZCJPUQwGwysIlr14MjKbXDhBNz3iOqM8/yV5K1mllt/mTw==} peerDependencies: @@ -16253,6 +17520,10 @@ packages: '@types/react': optional: true + react-style-stringify@1.2.0: + resolution: {integrity: sha512-88JZckqgbfXJaGcDQoTFKRmBwHBF0Ddaxz3PL9Q+vywAJruBY+NdN+ZiKSBe7r/pWtjbt0naZdtH5oNI1X3FLA==} + engines: {node: '>=14.0.0'} + react-test-renderer@19.2.3: resolution: {integrity: sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw==} peerDependencies: @@ -16341,6 +17612,9 @@ packages: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} + readonly-date-esm@2.0.0: + resolution: {integrity: sha512-adlyzz144ofU22kjnnRIN0HPPqIbc5IZvMmMuVtEgMY4mKgNyKqOVb4Fa2GUzE2By4TEPOYoGqDoKRyqmeEuPQ==} + real-require@0.1.0: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} @@ -16475,6 +17749,14 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + ripemd160-min@0.0.6: + resolution: {integrity: sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==} + engines: {node: '>=8'} + ripemd160@2.0.3: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} @@ -16483,10 +17765,22 @@ packages: resolution: {integrity: sha512-Qa3+9wKVvpL/xYtT6+wANsn0A1QcC5CT6IMZbRJZ/1lGt7gmwIfsrCuz1X0+LCEO7zgb+3UT1I1dc0k/5dwKQQ==} engines: {node: '>= 10'} + ripple-address-codec@5.0.1: + resolution: {integrity: sha512-JQHLKuVJV8lv9Qobmn4aUM2Dpv9WRRLKnNWfM8tN02fAbUtG8mUPsu9q9UYX8P76G4qzytEc5ZKMp/3JggNYmw==} + engines: {node: '>= 18'} + ripple-binary-codec@1.11.0: resolution: {integrity: sha512-g7+gs3T+NfoeW6vIq5dcN0CkIT4t/zwRzFxz8X2RzfbrWRnewPUKqQbmBgs05tXLX5NuWPaneiaAVpFpYBcdfw==} engines: {node: '>= 10'} + ripple-binary-codec@2.11.0: + resolution: {integrity: sha512-qad61CYAqIi8H7DuPnuDpj40wrXY8wYrc1YbuDcA9G/NZG0Uu8unHNbTC6Itz8SlOSL0P/nF/8HTFW/1caVlTg==} + engines: {node: '>= 18'} + + ripple-keypairs@3.1.0: + resolution: {integrity: sha512-yeqN768gaHStQ76qwaJ2TGxIGMXA4c9tg32n1zMHUWbyfEKXA70XOUH8SQdnAnIQNCASd8hzBZ8Edoqcd+a+IA==} + engines: {node: '>= 18'} + roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -16584,9 +17878,15 @@ packages: scheduler@0.28.0: resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + schema-inspector@2.1.0: + resolution: {integrity: sha512-3bmQVhbA01/EW8cZin4vIpqlpNU2SIy4BhKCfCgogJ3T/L76dLx3QAE+++4o+dNT33sa+SN9vOJL7iHiHFjiNg==} + scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + scryptsy@2.1.0: + resolution: {integrity: sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -16596,6 +17896,10 @@ packages: search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + secp256k1@3.8.1: + resolution: {integrity: sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==} + engines: {node: '>=4.0.0'} + secp256k1@4.0.5: resolution: {integrity: sha512-SQZi5+/uiJIFPYbeRrVuu77Sr3bFOTq0oCQs67CqYwdmg0lhnqi/8djSWhzNO3GKGOqxBYCdx8zJJv0zUwDDvw==} engines: {node: '>=18.0.0'} @@ -16716,6 +18020,22 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -16734,6 +18054,9 @@ packages: peerDependencies: signal-polyfill: ^0.2.0 + simple-cbor@0.4.1: + resolution: {integrity: sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==} + simple-plist@1.3.1: resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} @@ -16769,6 +18092,10 @@ packages: smoldot@3.3.2: resolution: {integrity: sha512-Zl4h/0gsw8cfTZzuJ7LV7mtR6QjxltwYjMY7MsVw0oXBXrLK8zyOS6DS9Vjsy57pX1vBMg6UVhHxjbH3W905zA==} + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + socket.io-client@4.8.4: resolution: {integrity: sha512-7wjKAvVXYOJsbmUgkWbnhPD4bFpkqrUR0++XOjpGFmqDqsqV1CI93VTraeExLxpjxEqrWgFoSRZu1ihjLWfkAA==} engines: {node: '>=10.0.0'} @@ -16946,6 +18273,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@8.3.0: resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} engines: {node: '>=20'} @@ -17001,6 +18332,12 @@ packages: structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + style-value-types@5.0.0: resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==} @@ -17105,6 +18442,10 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 + symbol-observable@2.0.3: + resolution: {integrity: sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==} + engines: {node: '>=0.10'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -17200,6 +18541,9 @@ packages: tiny-async-pool@1.3.0: resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-open@1.3.0: resolution: {integrity: sha512-GUFS8yjJZq0oWqCKCJVHcBgMpmi2WEGXY1le3E5ncR0DsgTII5uUyxtfk8/vGyDDGBE42UYHR6Ocvlk8mkXSRg==} @@ -17260,6 +18604,12 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + toqr@0.1.1: resolution: {integrity: sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==} @@ -17289,6 +18639,9 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + tronweb@6.5.1: + resolution: {integrity: sha512-mqa3WIBq7sYtkkIywa2Jop3UwJX6EgIrBkFpm3sSjppStTOTtYeMJyeIHJVvFFCaYN4vjU1FDjQPSvHFTcuzEw==} + trpc-browser@1.4.4: resolution: {integrity: sha512-5ZH/LqSoF008W8KU+4tfQ+g3F4Yl6BywDUnSlx5jT6fKa87mm8PkIXfhjGN1k6LoxjpMjy82bxvQzNLkJ3vUvw==} peerDependencies: @@ -17318,6 +18671,9 @@ packages: ts-mixer@6.0.4: resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + ts-morph@28.0.0: + resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} + ts-poet@6.12.0: resolution: {integrity: sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==} @@ -17398,6 +18754,10 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} + type-fest@4.34.1: + resolution: {integrity: sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==} + engines: {node: '>=16'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -17470,6 +18830,11 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + uhyphen@0.2.0: resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} @@ -17520,6 +18885,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -17882,6 +19250,10 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + validator@13.15.23: + resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + engines: {node: '>= 0.10'} + valtio@1.11.2: resolution: {integrity: sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw==} engines: {node: '>=12.20.0'} @@ -17906,6 +19278,18 @@ packages: react: optional: true + valtio@2.1.5: + resolution: {integrity: sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + react: '>=18.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + valtio@2.1.7: resolution: {integrity: sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==} engines: {node: '>=12.20.0'} @@ -17942,6 +19326,22 @@ packages: typescript: optional: true + viem@2.31.0: + resolution: {integrity: sha512-U7OMQ6yqK+bRbEIarf2vqxL7unSEQvNxvML/1zG7suAmKuJmipqdVTVJGKBCJiYsm/EremyO2FS4dHIPpGv+eA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + viem@2.32.0: + resolution: {integrity: sha512-pHwKXQSyEWX+8ttOQJdU5dSBfYd6L9JxARY/Sx0MBj3uF/Zaiqt6o1SbzjFjQXkNzWSgtxK7H89ZI1SMIA2iLQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + viem@2.57.2: resolution: {integrity: sha512-ZbPvu+5fvRsbFh+OymDz0GyEySsSco8GWrEAiHM4ckhEY1vJTPy0tcLVkn0cs70dPiUOjLxkQBnfVyt9YkJ7cA==} peerDependencies: @@ -18296,6 +19696,9 @@ packages: wif@5.0.0: resolution: {integrity: sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workerd@1.20260815.1: resolution: {integrity: sha512-8bArFkHmlp7qFEKVPyNzDzHzS35gc2fg0PYBcDtaNLF7UCDryCX2BQnpkUkTHYIy824IRrHOTwOEoTj0sUO2Fg==} engines: {node: '>=16'} @@ -18319,6 +19722,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -18386,6 +19793,18 @@ packages: utf-8-validate: optional: true + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -18462,9 +19881,16 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} + xrpl@5.3.0: + resolution: {integrity: sha512-sdyi7qqJ13U+nMllHgxJTWbQm/Tk8uzhBmeTKwhI9WxkHuekPfbvJ3BDopXO2vMfsty7lBja0ACSHEUKLz3F/w==} + engines: {node: '>=20.19.0'} + xstate@4.38.3: resolution: {integrity: sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==} + xstream@11.14.0: + resolution: {integrity: sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -18525,6 +19951,9 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + yup@1.6.1: + resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==} + zdog@1.1.3: resolution: {integrity: sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==} @@ -18549,6 +19978,25 @@ packages: zod@4.6.5: resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + zone-file@2.0.0-beta.3: + resolution: {integrity: sha512-6tE3PSRcpN5lbTTLlkLez40WkNPc9vw/u1J2j6DBiy0jcVX48nCkWrx2EC+bWHqC2SLp069Xw4AdnYn/qp/W5g==} + engines: {node: '>=10'} + + zustand@4.4.7: + resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + zustand@5.0.0: resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} engines: {node: '>=12.20.0'} @@ -18628,12 +20076,12 @@ snapshots: defu: 6.1.7 many-keys-map: 3.0.3 - '@aave/account@0.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(128dd19e588211bd4f16254762e0e3f1))': + '@aave/account@0.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(6830db7ba68b15a013c6582bffdd96ea))': optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - wagmi: 2.19.5(128dd19e588211bd4f16254762e0e3f1) + wagmi: 2.19.5(6830db7ba68b15a013c6582bffdd96ea) '@adobe/css-tools@4.5.0': {} @@ -19260,12 +20708,12 @@ snapshots: '@aptos-labs/ts-sdk': 7.3.0 '@wallet-standard/core': 1.1.2 - '@argent/x-ui@1.109.1(4e84717f2760c66a48b36c0f5fc6bcaa)': + '@argent/x-ui@1.109.1(349d5131ce3221b5191115dc655d9f9b)': dependencies: - '@chakra-ui/react': 2.10.10(@emotion/react@11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@emotion/react': 11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0) - '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@18.3.1) + '@chakra-ui/react': 2.10.10(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2) + '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0) '@scure/bip39': 1.6.0 '@starknet-io/types-js': 0.8.4 '@zxcvbn-ts/core': 3.0.4 @@ -19275,10 +20723,10 @@ snapshots: clsx: 2.1.1 colord: 2.10.0 embla-carousel: 8.6.0 - embla-carousel-react: 8.6.0(react@18.3.1) + embla-carousel-react: 8.6.0(react@19.3.0) embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0) emittery: 1.2.1 - framer-motion: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + framer-motion: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) history: 5.3.0 i18next: 24.2.3(typescript@5.9.3) lightweight-charts: 4.2.3 @@ -19286,26 +20734,25 @@ snapshots: object-hash: 3.0.0 popmotion: 11.0.5 react: 18.3.1 - react-copy-to-clipboard: 5.1.1(react@18.3.1) + react-copy-to-clipboard: 5.1.1(react@19.3.0) react-dom: 18.3.1(react@18.3.1) - react-i18next: 15.4.1(i18next@24.2.3(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react@18.3.1) - react-router-dom: 7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-spring-bottom-sheet: 3.5.0-alpha.0(@react-three/fiber@9.8.1(1b8196e6ab68073e033beecf79b81774))(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3) - react-virtuoso: 4.18.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - starknet: 8.9.2 - swr: 1.3.0(react@18.3.1) + react-i18next: 15.4.1(i18next@24.2.3(typescript@5.9.3))(react-dom@19.3.0(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0) + react-router-dom: 7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react-spring-bottom-sheet: 3.5.0-alpha.0(@react-three/fiber@9.8.1(9de97b7269446073e8ab8cc6ac08c6e3))(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3) + react-virtuoso: 4.18.16(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + swr: 1.3.0(react@19.3.0) tailwind-merge: 3.6.0 tailwindcss: 3.4.17 tailwindcss-animate: 1.0.7(tailwindcss@3.4.17) url-join: 5.0.0 - zod: 4.0.5 + zod: 4.6.5 - '@argent/x-ui@1.109.1(7b6fb343122df9d748def04b3b23f38c)': + '@argent/x-ui@1.109.1(6faa20acdd5a370ff8c45a11746c495d)': dependencies: - '@chakra-ui/react': 2.10.10(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(react@19.3.0)(supports-color@10.2.2) - '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0) + '@chakra-ui/react': 2.10.10(@emotion/react@11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(react@18.3.1)(supports-color@5.5.0) + '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@18.3.1) '@scure/bip39': 1.6.0 '@starknet-io/types-js': 0.8.4 '@zxcvbn-ts/core': 3.0.4 @@ -19315,10 +20762,10 @@ snapshots: clsx: 2.1.1 colord: 2.10.0 embla-carousel: 8.6.0 - embla-carousel-react: 8.6.0(react@19.3.0) + embla-carousel-react: 8.6.0(react@18.3.1) embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0) emittery: 1.2.1 - framer-motion: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + framer-motion: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) history: 5.3.0 i18next: 24.2.3(typescript@5.9.3) lightweight-charts: 4.2.3 @@ -19326,18 +20773,19 @@ snapshots: object-hash: 3.0.0 popmotion: 11.0.5 react: 18.3.1 - react-copy-to-clipboard: 5.1.1(react@19.3.0) + react-copy-to-clipboard: 5.1.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-i18next: 15.4.1(i18next@24.2.3(typescript@5.9.3))(react-dom@19.3.0(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0) - react-router-dom: 7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - react-spring-bottom-sheet: 3.5.0-alpha.0(@react-three/fiber@9.8.1(43ebe24fc3ae5dca528188986d089789))(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3) - react-virtuoso: 4.18.16(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - swr: 1.3.0(react@19.3.0) + react-i18next: 15.4.1(i18next@24.2.3(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react@18.3.1) + react-router-dom: 7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-spring-bottom-sheet: 3.5.0-alpha.0(@react-three/fiber@9.8.1(0d1a6e9022da08806fd1513b41507824))(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3) + react-virtuoso: 4.18.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + starknet: 8.9.2 + swr: 1.3.0(react@18.3.1) tailwind-merge: 3.6.0 tailwindcss: 3.4.17 tailwindcss-animate: 1.0.7(tailwindcss@3.4.17) url-join: 5.0.0 - zod: 4.6.5 + zod: 4.0.5 '@asamuzakjp/css-color@7.1.2': dependencies: @@ -20861,7 +22309,7 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@coinbase/cdp-sdk': 1.57.1(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 @@ -20871,7 +22319,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@4.6.5) preact: 10.24.2 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - zustand: 5.0.3(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@19.3.0)) transitivePeerDependencies: - '@types/react' - '@x402/core' @@ -20890,7 +22338,7 @@ snapshots: - zod optional: true - '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@coinbase/cdp-sdk': 1.57.1(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 @@ -20900,7 +22348,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@4.6.5) preact: 10.24.2 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - zustand: 5.0.3(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) transitivePeerDependencies: - '@types/react' - '@x402/core' @@ -20918,7 +22366,7 @@ snapshots: - utf-8-validate - zod - '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@coinbase/cdp-sdk': 1.57.1(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 @@ -20928,7 +22376,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@4.0.5) preact: 10.24.2 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - zustand: 5.0.3(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + zustand: 5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' - '@x402/core' @@ -20946,7 +22394,7 @@ snapshots: - utf-8-validate - zod - '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@coinbase/cdp-sdk': 1.57.1(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 @@ -20956,7 +22404,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) transitivePeerDependencies: - '@types/react' - '@x402/core' @@ -20991,6 +22439,8 @@ snapshots: multiformats: 13.4.2 uint8arrays: 5.1.1 + '@bitauth/libauth@3.0.0': {} + '@bitcoin-js/tiny-secp256k1-asmjs@2.2.4': dependencies: uint8array-tools: 0.0.7 @@ -21553,14 +23003,14 @@ snapshots: '@cartridge/controller-wasm@0.3.19': {} - '@cartridge/controller@0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@cartridge/controller@0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@cartridge/controller-wasm': 0.3.19 '@cartridge/penpal': 6.2.4 '@starknet-io/types-js': 0.9.1 '@telegram-apps/sdk': 2.11.3 '@turnkey/sdk-browser': 4.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@walletconnect/ethereum-provider': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/ethereum-provider': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) bs58: 6.0.0 cbor-x: 1.6.6 ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -21605,14 +23055,14 @@ snapshots: - utf-8-validate - zod - '@cartridge/controller@0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@cartridge/controller@0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@cartridge/controller-wasm': 0.3.19 '@cartridge/penpal': 6.2.4 '@starknet-io/types-js': 0.9.1 '@telegram-apps/sdk': 2.11.3 '@turnkey/sdk-browser': 4.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@walletconnect/ethereum-provider': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@walletconnect/ethereum-provider': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) bs58: 6.0.0 cbor-x: 1.6.6 ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -22085,7 +23535,7 @@ snapshots: - preact-render-to-string - supports-color - '@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -22094,7 +23544,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@4.0.5) preact: 10.24.2 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - zustand: 5.0.3(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + zustand: 5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -22105,7 +23555,7 @@ snapshots: - utf-8-validate - zod - '@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -22114,7 +23564,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@4.6.5) preact: 10.24.2 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - zustand: 5.0.3(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -22129,7 +23579,91 @@ snapshots: dependencies: commander: 15.0.0 - '@creit.tech/stellar-wallets-kit@2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(near-api-js@5.1.1)(react@19.3.0)(supports-color@10.2.2)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@cosmjs/amino@0.39.0': + dependencies: + '@cosmjs/crypto': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/utils': 0.39.0 + + '@cosmjs/crypto@0.39.0': + dependencies: + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/utils': 0.39.0 + '@noble/ciphers': 2.4.0 + '@noble/curves': 2.4.0 + '@noble/hashes': 2.4.0 + '@scure/bip39': 2.4.0 + + '@cosmjs/encoding@0.39.0': + dependencies: + '@scure/base': 2.4.0 + base64-js: 1.5.1 + readonly-date-esm: 2.0.0 + + '@cosmjs/json-rpc@0.39.0': + dependencies: + '@cosmjs/stream': 0.39.0 + xstream: 11.14.0 + + '@cosmjs/math@0.39.0': {} + + '@cosmjs/proto-signing@0.39.0': + dependencies: + '@cosmjs/amino': 0.39.0 + '@cosmjs/crypto': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/utils': 0.39.0 + cosmjs-types: 0.11.0 + + '@cosmjs/socket@0.39.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/stream': 0.39.0 + isomorphic-ws: 4.0.1(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@5.0.10) + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/stargate@0.39.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/amino': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/proto-signing': 0.39.0 + '@cosmjs/stream': 0.39.0 + '@cosmjs/tendermint-rpc': 0.39.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@cosmjs/utils': 0.39.0 + cosmjs-types: 0.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/stream@0.39.0': + dependencies: + xstream: 11.14.0 + + '@cosmjs/tendermint-rpc@0.39.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/crypto': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/json-rpc': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/socket': 0.39.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@cosmjs/stream': 0.39.0 + '@cosmjs/utils': 0.39.0 + readonly-date-esm: 2.0.0 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/utils@0.39.0': {} + + '@creit.tech/stellar-wallets-kit@2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(near-api-js@5.1.1)(react@19.3.0)(supports-color@10.2.2)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@albedo-link/intent': 0.12.0 '@creit.tech/xbull-wallet-connect': 0.4.0 @@ -22140,7 +23674,7 @@ snapshots: '@lobstrco/signer-extension-api': 2.0.0 '@metamask/connect-stellar': 0.3.0 '@preact/signals': 2.9.0(preact@10.29.8) - '@reown/appkit': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@stellar/freighter-api': 6.0.0 '@stellar/stellar-sdk': 17.2.1(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2) '@trezor/connect-web': 10.0.0-beta.1(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@5.0.10) @@ -22191,7 +23725,7 @@ snapshots: - utf-8-validate - zod - '@creit.tech/stellar-wallets-kit@2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@creit.tech/stellar-wallets-kit@2.7.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@albedo-link/intent': 0.12.0 '@creit.tech/xbull-wallet-connect': 0.4.0 @@ -22202,7 +23736,7 @@ snapshots: '@lobstrco/signer-extension-api': 2.0.0 '@metamask/connect-stellar': 0.3.0 '@preact/signals': 2.9.0(preact@10.29.8) - '@reown/appkit': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@stellar/freighter-api': 6.0.0 '@stellar/stellar-sdk': 17.2.1(debug@4.4.1(supports-color@5.5.0))(supports-color@5.5.0) '@trezor/connect-web': 10.0.0-beta.1(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@5.0.10) @@ -22287,6 +23821,38 @@ snapshots: '@csstools/css-tokenizer@4.0.2': {} + '@dfinity/agent@2.4.1(@dfinity/candid@2.4.1(@dfinity/principal@2.4.1))(@dfinity/principal@2.4.1)': + dependencies: + '@dfinity/candid': 2.4.1(@dfinity/principal@2.4.1) + '@dfinity/principal': 2.4.1 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + base64-arraybuffer: 0.2.0 + borc: 2.1.2 + buffer: 6.0.3 + simple-cbor: 0.4.1 + + '@dfinity/candid@2.4.1(@dfinity/principal@2.4.1)': + dependencies: + '@dfinity/principal': 2.4.1 + + '@dfinity/identity-secp256k1@2.4.1(@dfinity/candid@2.4.1(@dfinity/principal@2.4.1))(@dfinity/principal@2.4.1)': + dependencies: + '@dfinity/agent': 2.4.1(@dfinity/candid@2.4.1(@dfinity/principal@2.4.1))(@dfinity/principal@2.4.1) + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + asn1js: 3.0.10 + bs58check: 3.0.1 + transitivePeerDependencies: + - '@dfinity/candid' + - '@dfinity/principal' + + '@dfinity/principal@2.4.1': + dependencies: + '@noble/hashes': 1.8.0 + '@dnsquery/dns-packet@6.1.1': dependencies: '@leichtgewicht/ip-codec': 2.0.5 @@ -22600,6 +24166,9 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.25.3': + optional: true + '@esbuild/aix-ppc64@0.27.7': optional: true @@ -22615,6 +24184,9 @@ snapshots: '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.25.3': + optional: true + '@esbuild/android-arm64@0.27.7': optional: true @@ -22630,6 +24202,9 @@ snapshots: '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.25.3': + optional: true + '@esbuild/android-arm@0.27.7': optional: true @@ -22645,6 +24220,9 @@ snapshots: '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.25.3': + optional: true + '@esbuild/android-x64@0.27.7': optional: true @@ -22660,6 +24238,9 @@ snapshots: '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.25.3': + optional: true + '@esbuild/darwin-arm64@0.27.7': optional: true @@ -22675,6 +24256,9 @@ snapshots: '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.25.3': + optional: true + '@esbuild/darwin-x64@0.27.7': optional: true @@ -22690,6 +24274,9 @@ snapshots: '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.25.3': + optional: true + '@esbuild/freebsd-arm64@0.27.7': optional: true @@ -22705,6 +24292,9 @@ snapshots: '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.25.3': + optional: true + '@esbuild/freebsd-x64@0.27.7': optional: true @@ -22720,6 +24310,9 @@ snapshots: '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.25.3': + optional: true + '@esbuild/linux-arm64@0.27.7': optional: true @@ -22735,6 +24328,9 @@ snapshots: '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.25.3': + optional: true + '@esbuild/linux-arm@0.27.7': optional: true @@ -22750,6 +24346,9 @@ snapshots: '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.25.3': + optional: true + '@esbuild/linux-ia32@0.27.7': optional: true @@ -22765,6 +24364,9 @@ snapshots: '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.25.3': + optional: true + '@esbuild/linux-loong64@0.27.7': optional: true @@ -22780,6 +24382,9 @@ snapshots: '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.25.3': + optional: true + '@esbuild/linux-mips64el@0.27.7': optional: true @@ -22795,6 +24400,9 @@ snapshots: '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.25.3': + optional: true + '@esbuild/linux-ppc64@0.27.7': optional: true @@ -22810,6 +24418,9 @@ snapshots: '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.25.3': + optional: true + '@esbuild/linux-riscv64@0.27.7': optional: true @@ -22825,6 +24436,9 @@ snapshots: '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.25.3': + optional: true + '@esbuild/linux-s390x@0.27.7': optional: true @@ -22840,6 +24454,9 @@ snapshots: '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.25.3': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true @@ -22852,6 +24469,9 @@ snapshots: '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.25.3': + optional: true + '@esbuild/netbsd-arm64@0.27.7': optional: true @@ -22867,6 +24487,9 @@ snapshots: '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.25.3': + optional: true + '@esbuild/netbsd-x64@0.27.7': optional: true @@ -22879,6 +24502,9 @@ snapshots: '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.25.3': + optional: true + '@esbuild/openbsd-arm64@0.27.7': optional: true @@ -22894,6 +24520,9 @@ snapshots: '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.25.3': + optional: true + '@esbuild/openbsd-x64@0.27.7': optional: true @@ -22921,6 +24550,9 @@ snapshots: '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.25.3': + optional: true + '@esbuild/sunos-x64@0.27.7': optional: true @@ -22936,6 +24568,9 @@ snapshots: '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.25.3': + optional: true + '@esbuild/win32-arm64@0.27.7': optional: true @@ -22951,6 +24586,9 @@ snapshots: '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.25.3': + optional: true + '@esbuild/win32-ia32@0.27.7': optional: true @@ -22966,6 +24604,9 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.25.3': + optional: true + '@esbuild/win32-x64@0.27.7': optional: true @@ -24265,6 +25906,500 @@ snapshots: '@foxglove/crc@0.0.3': {} + '@fuel-ts/abi-coder@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + type-fest: 4.34.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/abi-coder@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + type-fest: 4.34.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/abi-coder@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + type-fest: 4.34.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/abi-typegen@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + commander: 13.1.0 + glob: 10.4.5 + handlebars: 4.7.8 + mkdirp: 3.0.1 + ramda: 0.30.1 + rimraf: 5.0.10 + transitivePeerDependencies: + - vitest + + '@fuel-ts/abi-typegen@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + commander: 13.1.0 + glob: 10.4.5 + handlebars: 4.7.8 + mkdirp: 3.0.1 + ramda: 0.30.1 + rimraf: 5.0.10 + transitivePeerDependencies: + - vitest + + '@fuel-ts/abi-typegen@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + commander: 13.1.0 + glob: 10.4.5 + handlebars: 4.7.8 + mkdirp: 3.0.1 + ramda: 0.30.1 + rimraf: 5.0.10 + transitivePeerDependencies: + - vitest + + '@fuel-ts/account@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/merkle': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + '@fuels/vm-asm': 0.62.0 + '@noble/curves': 1.8.1 + events: 3.3.0 + graphql: 16.10.0 + graphql-request: 6.1.0(graphql@16.10.0) + graphql-tag: 2.12.6(graphql@16.10.0) + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/account@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/merkle': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + '@fuels/vm-asm': 0.62.0 + '@noble/curves': 1.8.1 + events: 3.3.0 + graphql: 16.10.0 + graphql-request: 6.1.0(graphql@16.10.0) + graphql-tag: 2.12.6(graphql@16.10.0) + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/account@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/merkle': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + '@fuels/vm-asm': 0.62.0 + '@noble/curves': 1.8.1 + events: 3.3.0 + graphql: 16.10.0 + graphql-request: 6.1.0(graphql@16.10.0) + graphql-tag: 2.12.6(graphql@16.10.0) + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/address@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/address@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/address@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/contract@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/merkle': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuels/vm-asm': 0.62.0 + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/contract@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/merkle': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuels/vm-asm': 0.62.0 + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/contract@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/merkle': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuels/vm-asm': 0.62.0 + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/crypto@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/crypto@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/crypto@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/errors@0.103.0': + dependencies: + '@fuel-ts/versions': 0.103.0 + + '@fuel-ts/hasher@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/hasher@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/hasher@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@noble/hashes': 1.7.1 + transitivePeerDependencies: + - vitest + + '@fuel-ts/math@0.103.0': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@types/bn.js': 5.1.6 + bn.js: 5.2.1 + + '@fuel-ts/merkle@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + transitivePeerDependencies: + - vitest + + '@fuel-ts/merkle@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + transitivePeerDependencies: + - vitest + + '@fuel-ts/merkle@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + transitivePeerDependencies: + - vitest + + '@fuel-ts/program@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuels/vm-asm': 0.62.0 + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/program@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuels/vm-asm': 0.62.0 + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/program@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuels/vm-asm': 0.62.0 + ramda: 0.30.1 + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/recipes@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/abi-typegen': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/contract': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/recipes@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/abi-typegen': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/contract': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/recipes@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/abi-typegen': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/contract': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/script@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/script@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/script@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - encoding + - vitest + + '@fuel-ts/transactions@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - vitest + + '@fuel-ts/transactions@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - vitest + + '@fuel-ts/transactions@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + transitivePeerDependencies: + - vitest + + '@fuel-ts/utils@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/versions': 0.103.0 + fflate: 0.8.2 + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + + '@fuel-ts/utils@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/versions': 0.103.0 + fflate: 0.8.2 + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + + '@fuel-ts/utils@0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + dependencies: + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/math': 0.103.0 + '@fuel-ts/versions': 0.103.0 + fflate: 0.8.2 + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + + '@fuel-ts/versions@0.103.0': + dependencies: + chalk: 4.1.2 + cli-table: 0.3.11 + + '@fuels/vm-asm@0.62.0': {} + '@gemini-wallet/core@0.3.2(supports-color@10.2.2)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))': dependencies: '@metamask/rpc-errors': 7.0.2(supports-color@10.2.2) @@ -24302,6 +26437,10 @@ snapshots: graphql: 17.0.2 typescript: 5.9.3 + '@graphql-typed-document-node/core@3.2.0(graphql@16.10.0)': + dependencies: + graphql: 16.10.0 + '@graphql-typed-document-node/core@3.2.0(graphql@17.0.2)': dependencies: graphql: 17.0.2 @@ -24391,35 +26530,35 @@ snapshots: '@harmoniclabs/plutus-data': 1.2.4(@harmoniclabs/bytestring@1.0.0)(@harmoniclabs/cbor@1.6.0) '@harmoniclabs/uint8array-utils': 1.0.4 - '@hashgraph/hedera-wallet-connect@2.1.3(5dedda258f06b094f7e7ac4ac90bcdfc)': - dependencies: - '@hiero-ledger/proto': 2.31.0(ansi-regex@6.2.2)(ansi-styles@6.2.3)(debug@4.4.1(supports-color@5.5.0))(protobufjs@8.0.1)(strip-ansi@7.1.2) - '@hiero-ledger/sdk': 2.79.0(bn.js@5.2.1)(expo-crypto@57.0.3(expo@57.0.26))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(supports-color@5.5.0) - '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/walletkit': 1.6.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@walletconnect/modal': 2.7.0(@types/react@19.3.0)(react@18.3.1) - ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - - '@hashgraph/hedera-wallet-connect@2.1.3(a31850274c23b83333f7aa05d3b0718f)': + '@hashgraph/hedera-wallet-connect@2.1.3(1fd548a862f7b06f309bdc65aa5e1220)': dependencies: '@hiero-ledger/proto': 2.31.0(ansi-regex@6.2.2)(ansi-styles@6.2.3)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.5.4)(strip-ansi@7.1.2) '@hiero-ledger/sdk': 2.79.0(bn.js@5.2.1)(expo-crypto@57.0.3(expo@57.0.26))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(supports-color@10.2.2) - '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/walletkit': 1.6.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@walletconnect/modal': 2.7.0(@types/react@19.3.0)(react@19.3.0) - ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ethers: 6.17.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@hashgraph/hedera-wallet-connect@2.1.3(d1a5b987f8d9e1b2cd9533a0ab02adfc)': + '@hashgraph/hedera-wallet-connect@2.1.3(8b74697f4445efcecd7327d3adea5b89)': dependencies: - '@hiero-ledger/proto': 2.31.0(ansi-regex@6.2.2)(ansi-styles@7.0.0)(debug@4.4.1(supports-color@10.2.2))(protobufjs@8.6.6)(strip-ansi@7.1.2) + '@hiero-ledger/proto': 2.31.0(ansi-regex@6.2.2)(ansi-styles@7.0.0)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(strip-ansi@7.1.2) '@hiero-ledger/sdk': 2.89.1(bn.js@5.2.3)(bufferutil@4.1.0)(expo-crypto@57.0.3(expo@57.0.26))(supports-color@10.2.2)(utf-8-validate@5.0.10) - '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/walletkit': 1.6.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@walletconnect/modal': 2.7.0(@types/react@19.3.0)(react@19.3.0) - ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ethers: 6.17.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + + '@hashgraph/hedera-wallet-connect@2.1.3(c79748b64936bc6426ffcc8f4d484770)': + dependencies: + '@hiero-ledger/proto': 2.31.0(ansi-regex@6.2.2)(ansi-styles@6.2.3)(debug@4.4.1(supports-color@5.5.0))(protobufjs@8.0.1)(strip-ansi@7.1.2) + '@hiero-ledger/sdk': 2.79.0(bn.js@5.2.1)(expo-crypto@57.0.3(expo@57.0.26))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(supports-color@5.5.0) + '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/walletkit': 1.6.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@walletconnect/modal': 2.7.0(@types/react@19.3.0)(react@18.3.1) + ethers: 6.17.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@headlessui/react@2.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -24564,6 +26703,15 @@ snapshots: protobufjs: 8.0.1 strip-ansi: 7.1.2 + '@hiero-ledger/proto@2.31.0(ansi-regex@6.2.2)(ansi-styles@7.0.0)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(strip-ansi@7.1.2)': + dependencies: + ansi-regex: 6.2.2 + ansi-styles: 7.0.0 + debug: 4.4.1(supports-color@10.2.2) + long: 5.3.1 + protobufjs: 7.6.6 + strip-ansi: 7.1.2 + '@hiero-ledger/proto@2.31.0(ansi-regex@6.2.2)(ansi-styles@7.0.0)(debug@4.4.1(supports-color@10.2.2))(protobufjs@8.6.6)(strip-ansi@7.1.2)': dependencies: ansi-regex: 6.2.2 @@ -24913,6 +27061,15 @@ snapshots: dependencies: '@swc/helpers': 0.5.23 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 @@ -25470,6 +27627,14 @@ snapshots: events: 3.3.0 rxjs: 7.8.2 + '@ledgerhq/hw-transport-web-ble@6.35.0': + dependencies: + '@ledgerhq/devices': 8.16.0 + '@ledgerhq/errors': 6.37.0 + '@ledgerhq/hw-transport': 6.35.5 + '@ledgerhq/logs': 6.19.0 + rxjs: 7.8.2 + '@ledgerhq/hw-transport-webhid@6.36.0': dependencies: '@ledgerhq/devices': 8.16.0 @@ -25484,6 +27649,13 @@ snapshots: '@ledgerhq/hw-transport': 6.36.0 '@ledgerhq/logs': 6.19.0 + '@ledgerhq/hw-transport-webusb@6.35.0': + dependencies: + '@ledgerhq/devices': 8.16.0 + '@ledgerhq/errors': 6.37.0 + '@ledgerhq/hw-transport': 6.35.5 + '@ledgerhq/logs': 6.19.0 + '@ledgerhq/hw-transport@6.31.12': dependencies: '@ledgerhq/devices': 8.6.1 @@ -25550,6 +27722,10 @@ snapshots: progress-events: 1.1.0 uint8arraylist: 3.0.2 + '@lifeomic/axios-fetch@3.1.0': + dependencies: + '@types/node-fetch': 2.6.13 + '@lightsparkdev/core@1.5.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: '@noble/curves': 1.9.7 @@ -26308,12 +28484,244 @@ snapshots: uint8-varint: 3.0.0 uint8arrays: 6.1.1 + '@multiversx/sdk-bls-wasm@0.3.5': + optional: true + + '@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)': + dependencies: + '@multiversx/sdk-transaction-decoder': 1.0.2 + '@noble/ed25519': 1.7.3 + '@noble/hashes': 1.3.0 + bech32: 1.1.4 + bignumber.js: 9.3.1 + blake2b: 2.1.3 + buffer: 6.0.3 + ed25519-hd-key: 1.1.2 + ed2curve: 0.3.0 + json-bigint: 1.0.0 + keccak: 3.0.2 + protobufjs: 7.6.6 + scryptsy: 2.1.0 + tweetnacl: 1.0.3 + uuid: 8.3.2 + optionalDependencies: + '@multiversx/sdk-bls-wasm': 0.3.5 + axios: 1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2) + bip39: 3.1.0 + transitivePeerDependencies: + - debug + - supports-color + + '@multiversx/sdk-core@16.0.0(bignumber.js@9.3.1)(debug@4.4.3(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)': + dependencies: + '@multiversx/sdk-transaction-decoder': 1.0.2 + '@noble/ed25519': 1.7.3 + '@noble/hashes': 1.3.0 + bech32: 1.1.4 + bignumber.js: 9.3.1 + blake2b: 2.1.3 + buffer: 6.0.3 + ed25519-hd-key: 1.1.2 + ed2curve: 0.3.0 + json-bigint: 1.0.0 + keccak: 3.0.2 + protobufjs: 7.6.6 + scryptsy: 2.1.0 + tweetnacl: 1.0.3 + uuid: 8.3.2 + optionalDependencies: + '@multiversx/sdk-bls-wasm': 0.3.5 + axios: 1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) + bip39: 3.1.0 + transitivePeerDependencies: + - debug + - supports-color + + '@multiversx/sdk-core@16.0.0(bignumber.js@9.3.1)(debug@4.4.3(supports-color@8.1.1))(protobufjs@7.6.6)(supports-color@8.1.1)': + dependencies: + '@multiversx/sdk-transaction-decoder': 1.0.2 + '@noble/ed25519': 1.7.3 + '@noble/hashes': 1.3.0 + bech32: 1.1.4 + bignumber.js: 9.3.1 + blake2b: 2.1.3 + buffer: 6.0.3 + ed25519-hd-key: 1.1.2 + ed2curve: 0.3.0 + json-bigint: 1.0.0 + keccak: 3.0.2 + protobufjs: 7.6.6 + scryptsy: 2.1.0 + tweetnacl: 1.0.3 + uuid: 8.3.2 + optionalDependencies: + '@multiversx/sdk-bls-wasm': 0.3.5 + axios: 1.20.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) + bip39: 3.1.0 + transitivePeerDependencies: + - debug + - supports-color + + '@multiversx/sdk-dapp-ui@0.1.26(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vue@3.5.43(typescript@5.9.3))': + dependencies: + '@stencil/core': 4.45.2 + '@stencil/react-output-target': 1.6.2(@stencil/core@4.45.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@stencil/vue-output-target': 0.14.3(@stencil/core@4.45.2)(vue@3.5.43(typescript@5.9.3)) + classnames: 2.5.1 + lodash.capitalize: 4.2.1 + lodash.inrange: 3.3.6 + lodash.range: 3.2.0 + qrcode: 1.5.4 + transitivePeerDependencies: + - '@types/react' + - react + - react-dom + - vue + - vue-router + optional: true + + '@multiversx/sdk-dapp-utils@3.1.0(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(bignumber.js@9.3.1)': + dependencies: + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + bignumber.js: 9.3.1 + + '@multiversx/sdk-dapp@5.7.3(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@multiversx/sdk-dapp-utils@3.1.0(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(bignumber.js@9.3.1))(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(axios@1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2))(bignumber.js@9.3.1)(bufferutil@4.1.0)(protobufjs@7.6.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vue@3.5.43(typescript@5.9.3))(zod@4.6.5)': + dependencies: + '@lifeomic/axios-fetch': 3.1.0 + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + '@multiversx/sdk-dapp-utils': 3.1.0(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(bignumber.js@9.3.1) + '@multiversx/sdk-extension-provider': 5.1.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)) + '@multiversx/sdk-hw-provider': 8.2.0(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)) + '@multiversx/sdk-native-auth-client': 2.0.1(axios@1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2)) + '@multiversx/sdk-wallet-connect-provider': 6.1.5(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@multiversx/sdk-web-wallet-cross-window-provider': 3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)) + '@multiversx/sdk-web-wallet-iframe-provider': 4.0.1(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@multiversx/sdk-web-wallet-cross-window-provider@3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))) + '@multiversx/sdk-webview-provider': 3.2.7(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@multiversx/sdk-web-wallet-cross-window-provider@3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))) + axios: 1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2) + bignumber.js: 9.3.1 + immer: 10.1.1 + linkifyjs: 4.3.3 + lodash.isempty: 4.4.0 + lodash.isequal: 4.5.0 + lodash.isstring: 4.0.1 + lodash.startcase: 4.4.0 + lodash.trimend: 4.18.0 + lodash.uniq: 4.5.0 + protobufjs: 7.6.6 + socket.io-client: 4.8.3(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + zustand: 4.4.7(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0) + optionalDependencies: + '@multiversx/sdk-dapp-ui': 0.1.26(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vue@3.5.43(typescript@5.9.3)) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - react + - react-dom + - supports-color + - typescript + - uploadthing + - utf-8-validate + - vue + - vue-router + - zod + + '@multiversx/sdk-extension-provider@5.1.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))': + dependencies: + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + + '@multiversx/sdk-hw-provider@8.2.0(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))': + dependencies: + '@ledgerhq/devices': 8.16.0 + '@ledgerhq/errors': 6.37.0 + '@ledgerhq/hw-transport': 6.35.5 + '@ledgerhq/hw-transport-web-ble': 6.35.0 + '@ledgerhq/hw-transport-webhid': 6.36.0 + '@ledgerhq/hw-transport-webusb': 6.35.0 + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + buffer: 6.0.3 + platform: 1.3.6 + + '@multiversx/sdk-native-auth-client@2.0.1(axios@1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2))': + dependencies: + axios: 1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2) + + '@multiversx/sdk-transaction-decoder@1.0.2': + dependencies: + bech32: 2.0.0 + + '@multiversx/sdk-wallet-connect-provider@6.1.5(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + '@walletconnect/sign-client': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/utils': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.6.5) + bech32: 1.1.4 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@multiversx/sdk-web-wallet-cross-window-provider@3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))': + dependencies: + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + qs: 6.11.2 + + '@multiversx/sdk-web-wallet-iframe-provider@4.0.1(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@multiversx/sdk-web-wallet-cross-window-provider@3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)))': + dependencies: + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + '@multiversx/sdk-web-wallet-cross-window-provider': 3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)) + '@types/jest': 29.5.14 + '@types/qs': 6.9.10 + qs: 6.11.2 + + '@multiversx/sdk-webview-provider@3.2.7(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2))(@multiversx/sdk-web-wallet-cross-window-provider@3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)))': + dependencies: + '@multiversx/sdk-core': 15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2) + '@multiversx/sdk-web-wallet-cross-window-provider': 3.2.2(@multiversx/sdk-core@15.4.1(bignumber.js@9.3.1)(debug@4.4.1(supports-color@10.2.2))(protobufjs@7.6.6)(supports-color@10.2.2)) + '@mysten/bcs@2.1.2': dependencies: '@mysten/utils': 0.4.1 '@scure/base': 2.4.0 - '@mysten/dapp-kit@1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))': + '@mysten/dapp-kit@1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))': dependencies: '@mysten/slush-wallet': 1.3.2(@mysten/sui@2.34.0(typescript@5.9.3))(typescript@5.9.3) '@mysten/sui': 2.34.0(typescript@5.9.3) @@ -26328,7 +28736,7 @@ snapshots: '@vanilla-extract/recipes': 0.5.7(@vanilla-extract/css@1.21.2(babel-plugin-macros@3.1.0)) clsx: 2.1.1 react: 18.3.1 - zustand: 5.0.15(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + zustand: 5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -26338,7 +28746,7 @@ snapshots: - typescript - use-sync-external-store - '@mysten/dapp-kit@1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))': + '@mysten/dapp-kit@1.1.17(@mysten/sui@2.34.0(typescript@5.9.3))(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(immer@10.1.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))': dependencies: '@mysten/slush-wallet': 1.3.2(@mysten/sui@2.34.0(typescript@5.9.3))(typescript@5.9.3) '@mysten/sui': 2.34.0(typescript@5.9.3) @@ -26353,7 +28761,7 @@ snapshots: '@vanilla-extract/recipes': 0.5.7(@vanilla-extract/css@1.21.2(babel-plugin-macros@3.1.0)) clsx: 2.1.1 react: 19.3.0 - zustand: 5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -26775,6 +29183,10 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/curves@1.9.2': + dependencies: + '@noble/hashes': 1.8.0 + '@noble/curves@1.9.7': dependencies: '@noble/hashes': 1.8.0 @@ -26787,8 +29199,14 @@ snapshots: dependencies: '@noble/hashes': 2.4.0 + '@noble/ed25519@1.7.3': {} + '@noble/ed25519@3.1.0': {} + '@noble/hashes@1.1.5': {} + + '@noble/hashes@1.3.0': {} + '@noble/hashes@1.3.2': {} '@noble/hashes@1.3.3': {} @@ -26815,6 +29233,8 @@ snapshots: '@noble/curves': 2.2.0 '@noble/hashes': 2.2.0 + '@noble/secp256k1@1.7.1': {} + '@noble/secp256k1@1.7.2': {} '@nodelib/fs.scandir@2.1.5': @@ -26962,6 +29382,9 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@playwright/test@1.63.0': dependencies: playwright: 1.63.0 @@ -28139,7 +30562,7 @@ snapshots: '@radix-ui/rect@1.1.3': {} - '@rainbow-me/rainbowkit@2.2.11(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(128dd19e588211bd4f16254762e0e3f1))': + '@rainbow-me/rainbowkit@2.2.11(@tanstack/react-query@5.104.1(react@18.3.1))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(6830db7ba68b15a013c6582bffdd96ea))': dependencies: '@tanstack/react-query': 5.104.1(react@18.3.1) '@vanilla-extract/css': 1.20.1(babel-plugin-macros@3.1.0) @@ -28152,13 +30575,13 @@ snapshots: react-remove-scroll: 2.7.2(@types/react@19.3.0)(react@18.3.1) ua-parser-js: 2.0.10 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - wagmi: 2.19.5(128dd19e588211bd4f16254762e0e3f1) + wagmi: 2.19.5(6830db7ba68b15a013c6582bffdd96ea) transitivePeerDependencies: - '@types/react' - babel-plugin-macros - typescript - '@rainbow-me/rainbowkit@2.2.11(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(wagmi@2.19.5(36cbd6af1fba239fc5835c3ea32e5981))': + '@rainbow-me/rainbowkit@2.2.11(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(wagmi@2.19.5(f6fa2c3037d24512664382b670f973d0))': dependencies: '@tanstack/react-query': 5.104.1(react@19.3.0) '@vanilla-extract/css': 1.20.1(babel-plugin-macros@3.1.0) @@ -28171,7 +30594,7 @@ snapshots: react-remove-scroll: 2.7.2(@types/react@19.3.0)(react@19.3.0) ua-parser-js: 2.0.10 viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - wagmi: 2.19.5(36cbd6af1fba239fc5835c3ea32e5981) + wagmi: 2.19.5(f6fa2c3037d24512664382b670f973d0) transitivePeerDependencies: - '@types/react' - babel-plugin-macros @@ -28709,23 +31132,23 @@ snapshots: '@react-spring/types': 9.7.5 react: 19.3.0 - '@react-spring/three@9.7.5(@react-three/fiber@9.8.1(1b8196e6ab68073e033beecf79b81774))(react@18.3.1)(three@0.186.1)': + '@react-spring/three@9.7.5(@react-three/fiber@9.8.1(0d1a6e9022da08806fd1513b41507824))(react@18.3.1)(three@0.186.1)': dependencies: '@react-spring/animated': 9.7.5(react@18.3.1) '@react-spring/core': 9.7.5(react@18.3.1) '@react-spring/shared': 9.7.5(react@18.3.1) '@react-spring/types': 9.7.5 - '@react-three/fiber': 9.8.1(1b8196e6ab68073e033beecf79b81774) + '@react-three/fiber': 9.8.1(0d1a6e9022da08806fd1513b41507824) react: 18.3.1 three: 0.186.1 - '@react-spring/three@9.7.5(@react-three/fiber@9.8.1(43ebe24fc3ae5dca528188986d089789))(react@19.3.0)(three@0.186.1)': + '@react-spring/three@9.7.5(@react-three/fiber@9.8.1(9de97b7269446073e8ab8cc6ac08c6e3))(react@19.3.0)(three@0.186.1)': dependencies: '@react-spring/animated': 9.7.5(react@19.3.0) '@react-spring/core': 9.7.5(react@19.3.0) '@react-spring/shared': 9.7.5(react@19.3.0) '@react-spring/types': 9.7.5 - '@react-three/fiber': 9.8.1(43ebe24fc3ae5dca528188986d089789) + '@react-three/fiber': 9.8.1(9de97b7269446073e8ab8cc6ac08c6e3) react: 19.3.0 three: 0.186.1 @@ -28771,7 +31194,7 @@ snapshots: react-zdog: 1.2.2 zdog: 1.1.3 - '@react-three/fiber@9.8.1(1b8196e6ab68073e033beecf79b81774)': + '@react-three/fiber@9.8.1(0d1a6e9022da08806fd1513b41507824)': dependencies: '@babel/runtime': 7.29.7 '@types/webxr': 0.5.24 @@ -28784,7 +31207,7 @@ snapshots: suspend-react: 0.1.3(react@18.3.1) three: 0.186.1 use-sync-external-store: 1.7.0(react@18.3.1) - zustand: 5.0.15(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.7.0(react@18.3.1)) + zustand: 5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.7.0(react@18.3.1)) optionalDependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@5.5.0))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@18.3.1(react@18.3.1))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(utf-8-validate@5.0.10) expo-asset: 57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3) @@ -28795,7 +31218,7 @@ snapshots: - '@types/react' - immer - '@react-three/fiber@9.8.1(43ebe24fc3ae5dca528188986d089789)': + '@react-three/fiber@9.8.1(9de97b7269446073e8ab8cc6ac08c6e3)': dependencies: '@babel/runtime': 7.29.7 '@types/webxr': 0.5.24 @@ -28808,7 +31231,7 @@ snapshots: suspend-react: 0.1.3(react@19.3.0) three: 0.186.1 use-sync-external-store: 1.7.0(react@19.3.0) - zustand: 5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)) + zustand: 5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)) optionalDependencies: expo: 57.0.26(@babel/core@7.29.7(supports-color@10.2.2))(@expo/dom-webview@57.0.1)(bufferutil@4.1.0)(react-dom@19.3.0(react@19.3.0))(react-native-webview@13.16.1(patch_hash=997d0e7cd0d47a1dc6e30ab8342650cd4761be6d0b59f2198ffb77a202e226de)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10) expo-asset: 57.0.18(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3) @@ -28836,22 +31259,22 @@ snapshots: '@reactive-dot/utils@0.10.3': {} - '@reown/appkit-adapter-wagmi@1.8.24(75f6087df408135c9a04dd1e7d32adf8)': + '@reown/appkit-adapter-wagmi@1.8.24(634791c039d0d5e120b0f2de526c4c31)': dependencies: - '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-polyfills': 1.8.24 - '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - wagmi: 3.7.7(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@tanstack/query-core@5.104.1)(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + wagmi: 2.19.5(f6fa2c3037d24512664382b670f973d0) optionalDependencies: - '@wagmi/connectors': 8.2.0(87153c2f809509eb916a8e14d6dfd1b7) + '@wagmi/connectors': 8.2.0(6c8dc751a4562f1028bdb7866d5ae208) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -28896,22 +31319,22 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-adapter-wagmi@1.8.24(8fbf84517274ca6aeadd4115118f2ac2)': + '@reown/appkit-adapter-wagmi@1.8.24(8863ad211d7eb04a60a6c3b2601e2c62)': dependencies: - '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-polyfills': 1.8.24 - '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) + '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - wagmi: 2.19.5(128dd19e588211bd4f16254762e0e3f1) + wagmi: 2.19.5(6830db7ba68b15a013c6582bffdd96ea) optionalDependencies: - '@wagmi/connectors': 8.2.0(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)))(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) + '@wagmi/connectors': 8.2.0(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)))(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -28956,22 +31379,22 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-adapter-wagmi@1.8.24(cb2b1022a657ae38d56d762bf274b4d2)': + '@reown/appkit-adapter-wagmi@1.8.24(e1ddbe9b4bf0b2cd292d88ad4f2f62a3)': dependencies: - '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-polyfills': 1.8.24 - '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - wagmi: 2.19.5(36cbd6af1fba239fc5835c3ea32e5981) + wagmi: 3.7.7(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@tanstack/query-core@5.104.1)(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) optionalDependencies: - '@wagmi/connectors': 8.2.0(87153c2f809509eb916a8e14d6dfd1b7) + '@wagmi/connectors': 8.2.0(6c8dc751a4562f1028bdb7866d5ae208) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -29016,6 +31439,28 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-common@1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@reown/appkit-common@1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: big.js: 6.2.2 @@ -29159,6 +31604,41 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-controllers@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-wallet': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + valtio: 2.1.5(@types/react@19.3.0)(react@19.3.0) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@reown/appkit-controllers@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -29264,13 +31744,83 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-controllers@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-controllers@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-controllers@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) - viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -29299,13 +31849,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit-controllers@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: - '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) - viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -29334,13 +31884,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-controllers@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: - '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) - viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -29369,10 +31919,10 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-controllers@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: - '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -29404,10 +31954,10 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit-controllers@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: - '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) @@ -29439,48 +31989,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-pay@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: - '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) - viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@types/react' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - encoding - - ioredis - - react - - typescript - - uploadthing - - utf-8-validate - - zod - - '@reown/appkit-controllers@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': - dependencies: - '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) - viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-ui': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-utils': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + lit: 3.3.0 + valtio: 2.1.5(@types/react@19.3.0)(react@19.3.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -29581,12 +32097,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) transitivePeerDependencies: @@ -29626,12 +32142,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) transitivePeerDependencies: @@ -29671,12 +32187,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) transitivePeerDependencies: @@ -29716,12 +32232,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-pay@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) transitivePeerDependencies: @@ -29761,12 +32277,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-pay@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) transitivePeerDependencies: @@ -29806,12 +32322,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit-pay@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) transitivePeerDependencies: @@ -29851,12 +32367,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-pay@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) transitivePeerDependencies: @@ -29896,12 +32412,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit-pay@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) lit: 3.3.0 valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) transitivePeerDependencies: @@ -29941,6 +32457,10 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-polyfills@1.7.17': + dependencies: + buffer: 6.0.3 + '@reown/appkit-polyfills@1.7.8': dependencies: buffer: 6.0.3 @@ -29957,6 +32477,43 @@ snapshots: dependencies: buffer: 6.0.3 + '@reown/appkit-scaffold-ui@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + dependencies: + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-ui': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-utils': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-wallet': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + lit: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - valtio + - zod + '@reown/appkit-scaffold-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -30031,13 +32588,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5)': + '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30078,13 +32635,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30125,13 +32682,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': + '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30172,13 +32729,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30219,13 +32776,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + '@reown/appkit-scaffold-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30266,13 +32823,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': + '@reown/appkit-scaffold-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30313,13 +32870,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + '@reown/appkit-scaffold-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30360,13 +32917,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': + '@reown/appkit-scaffold-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -30407,6 +32964,83 @@ snapshots: - valtio - zod + '@reown/appkit-siwx@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + dependencies: + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-scaffold-ui': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-ui': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-utils': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + bip322-js: 2.0.0 + bs58: 6.0.0 + lit: 3.3.0 + tweetnacl: 1.0.3 + viem: 2.32.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - valtio + - zod + optional: true + + '@reown/appkit-ui@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-wallet': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@reown/appkit-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -30585,12 +33219,84 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-ui@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@phosphor-icons/webcomponents': 2.1.5 + '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@phosphor-icons/webcomponents': 2.1.5 + '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@phosphor-icons/webcomponents': 2.1.5 - '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -30621,12 +33327,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@phosphor-icons/webcomponents': 2.1.5 - '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -30657,12 +33363,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@phosphor-icons/webcomponents': 2.1.5 - '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -30693,14 +33399,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit-universal-connector@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: - '@phosphor-icons/webcomponents': 2.1.5 - '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - lit: 3.3.0 - qrcode: 1.5.3 + '@reown/appkit': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/types': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + bs58: 6.0.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -30723,20 +33428,24 @@ snapshots: - db0 - encoding - ioredis + - lit - react - typescript - uploadthing - utf-8-validate - zod - '@reown/appkit-ui@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit-utils@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: - '@phosphor-icons/webcomponents': 2.1.5 - '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - lit: 3.3.0 - qrcode: 1.5.3 + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-polyfills': 1.7.17 + '@reown/appkit-wallet': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@wallet-standard/wallet': 1.1.0 + '@walletconnect/logger': 2.1.2 + '@walletconnect/universal-provider': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + valtio: 2.1.5(@types/react@19.3.0)(react@19.3.0) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -30841,7 +33550,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5)': + '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -30853,7 +33562,7 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) transitivePeerDependencies: @@ -30893,7 +33602,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -30905,7 +33614,7 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) transitivePeerDependencies: @@ -30945,7 +33654,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': + '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) @@ -30957,7 +33666,7 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) transitivePeerDependencies: @@ -30997,7 +33706,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76)': + '@reown/appkit-utils@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -31009,7 +33718,7 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: @@ -31049,7 +33758,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + '@reown/appkit-utils@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -31061,8 +33770,8 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) transitivePeerDependencies: @@ -31102,7 +33811,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': + '@reown/appkit-utils@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) @@ -31114,8 +33823,8 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) transitivePeerDependencies: @@ -31155,7 +33864,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': + '@reown/appkit-utils@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -31167,8 +33876,8 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) transitivePeerDependencies: @@ -31208,7 +33917,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': + '@reown/appkit-utils@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) @@ -31220,8 +33929,8 @@ snapshots: valtio: 2.1.7(@types/react@19.3.0)(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) transitivePeerDependencies: @@ -31261,6 +33970,17 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-wallet@1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-polyfills': 1.7.17 + '@walletconnect/logger': 2.1.2 + zod: 3.22.4 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + '@reown/appkit-wallet@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) @@ -31305,6 +34025,53 @@ snapshots: - typescript - utf-8-validate + '@reown/appkit@1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@reown/appkit-common': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-controllers': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-polyfills': 1.7.17 + '@reown/appkit-scaffold-ui': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-ui': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-utils': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-wallet': 1.7.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + bs58: 6.0.0 + semver: 7.7.2 + valtio: 2.1.5(@types/react@19.3.0)(react@19.3.0) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + optionalDependencies: + '@lit/react': 1.0.8(@types/react@19.3.0) + '@reown/appkit-siwx': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.5(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - lit + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@reown/appkit@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -31391,15 +34158,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) + '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.6.5) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) bs58: 6.0.0 @@ -31445,15 +34212,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) bs58: 6.0.0 @@ -31499,15 +34266,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) bs58: 6.0.0 @@ -31553,15 +34320,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-controllers': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) '@reown/appkit-ui': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@3.25.76) '@reown/appkit-wallet': 1.8.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 @@ -31607,15 +34374,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-polyfills': 1.8.21 - '@reown/appkit-scaffold-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-scaffold-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) bs58: 6.0.0 @@ -31661,15 +34428,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit@1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-pay': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-polyfills': 1.8.21 - '@reown/appkit-scaffold-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-scaffold-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-ui': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.21(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-wallet': 1.8.21(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) bs58: 6.0.0 @@ -31715,15 +34482,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@reown/appkit@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@reown/appkit-polyfills': 1.8.24 - '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@19.3.0))(zod@4.6.5) '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) bs58: 6.0.0 @@ -31769,15 +34536,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@reown/appkit@1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: '@reown/appkit-common': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-controllers': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit-pay': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@reown/appkit-polyfills': 1.8.24 - '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-scaffold-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-ui': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) + '@reown/appkit-utils': 1.8.24(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.3.0)(react@18.3.1))(zod@4.0.5) '@reown/appkit-wallet': 1.8.24(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) bs58: 6.0.0 @@ -31986,9 +34753,15 @@ snapshots: '@rollup/rollup-android-arm64@4.63.6': optional: true + '@rollup/rollup-darwin-arm64@4.44.0': + optional: true + '@rollup/rollup-darwin-arm64@4.63.6': optional: true + '@rollup/rollup-darwin-x64@4.44.0': + optional: true + '@rollup/rollup-darwin-x64@4.63.6': optional: true @@ -32004,9 +34777,15 @@ snapshots: '@rollup/rollup-linux-arm-musleabihf@4.63.6': optional: true + '@rollup/rollup-linux-arm64-gnu@4.44.0': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.63.6': optional: true + '@rollup/rollup-linux-arm64-musl@4.44.0': + optional: true + '@rollup/rollup-linux-arm64-musl@4.63.6': optional: true @@ -32031,9 +34810,15 @@ snapshots: '@rollup/rollup-linux-s390x-gnu@4.63.6': optional: true + '@rollup/rollup-linux-x64-gnu@4.44.0': + optional: true + '@rollup/rollup-linux-x64-gnu@4.63.6': optional: true + '@rollup/rollup-linux-x64-musl@4.44.0': + optional: true + '@rollup/rollup-linux-x64-musl@4.63.6': optional: true @@ -32043,6 +34828,9 @@ snapshots: '@rollup/rollup-openharmony-arm64@4.63.6': optional: true + '@rollup/rollup-win32-arm64-msvc@4.44.0': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.63.6': optional: true @@ -32052,6 +34840,9 @@ snapshots: '@rollup/rollup-win32-x64-gnu@4.63.6': optional: true + '@rollup/rollup-win32-x64-msvc@4.44.0': + optional: true + '@rollup/rollup-win32-x64-msvc@4.63.6': optional: true @@ -32153,6 +34944,12 @@ snapshots: '@scure/base@2.4.0': {} + '@scure/bip32@1.1.3': + dependencies: + '@noble/hashes': 1.1.5 + '@noble/secp256k1': 1.7.2 + '@scure/base': 1.1.9 + '@scure/bip32@1.4.0': dependencies: '@noble/curves': 1.4.2 @@ -32183,6 +34980,11 @@ snapshots: '@noble/hashes': 2.4.0 '@scure/base': 2.4.0 + '@scure/bip39@1.1.0': + dependencies: + '@noble/hashes': 1.1.5 + '@scure/base': 1.1.9 + '@scure/bip39@1.3.0': dependencies: '@noble/hashes': 1.4.0 @@ -34189,6 +36991,157 @@ snapshots: '@stablelib/salsa20': 2.0.1 '@stablelib/wipe': 2.0.1 + '@stacks/auth@7.6.0': + dependencies: + '@noble/secp256k1': 1.7.1 + '@stacks/common': 7.6.0 + '@stacks/encryption': 7.6.0 + '@stacks/network': 7.6.0 + '@stacks/profile': 7.6.0 + cross-fetch: 3.2.0 + jsontokens: 4.0.1 + transitivePeerDependencies: + - encoding + + '@stacks/common@6.16.0': + dependencies: + '@types/bn.js': 5.2.0 + '@types/node': 18.19.130 + + '@stacks/common@7.6.0': {} + + '@stacks/connect-ui@8.1.2': + dependencies: + '@stencil/core': 4.45.2 + + '@stacks/connect@8.2.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@reown/appkit': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit-universal-connector': 1.7.17(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(lit@3.3.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@scure/base': 1.2.6 + '@stacks/common': 7.6.0 + '@stacks/connect-ui': 8.1.2 + '@stacks/network': 7.6.0 + '@stacks/network-v6': '@stacks/network@6.17.0' + '@stacks/profile': 7.6.0 + '@stacks/transactions': 7.6.0 + '@stacks/transactions-v6': '@stacks/transactions@6.17.0' + type-fest: 5.10.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - lit + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@stacks/encryption@7.6.0': + dependencies: + '@noble/hashes': 1.1.5 + '@noble/secp256k1': 1.7.1 + '@scure/bip39': 1.1.0 + '@stacks/common': 7.6.0 + base64-js: 1.5.1 + bs58: 5.0.0 + ripemd160-min: 0.0.6 + varuint-bitcoin: 1.1.2 + + '@stacks/network@6.17.0': + dependencies: + '@stacks/common': 6.16.0 + cross-fetch: 3.2.0 + transitivePeerDependencies: + - encoding + + '@stacks/network@7.6.0': + dependencies: + '@stacks/common': 7.6.0 + cross-fetch: 3.2.0 + transitivePeerDependencies: + - encoding + + '@stacks/profile@7.6.0': + dependencies: + '@stacks/common': 7.6.0 + '@stacks/network': 7.6.0 + '@stacks/transactions': 7.6.0 + jsontokens: 4.0.1 + schema-inspector: 2.1.0 + zone-file: 2.0.0-beta.3 + transitivePeerDependencies: + - encoding + + '@stacks/storage@7.6.0': + dependencies: + '@stacks/auth': 7.6.0 + '@stacks/common': 7.6.0 + '@stacks/encryption': 7.6.0 + '@stacks/network': 7.6.0 + base64-js: 1.5.1 + jsontokens: 4.0.1 + transitivePeerDependencies: + - encoding + + '@stacks/transactions@6.17.0': + dependencies: + '@noble/hashes': 1.1.5 + '@noble/secp256k1': 1.7.1 + '@stacks/common': 6.16.0 + '@stacks/network': 6.17.0 + c32check: 2.0.0 + lodash.clonedeep: 4.5.0 + transitivePeerDependencies: + - encoding + + '@stacks/transactions@7.6.0': + dependencies: + '@noble/hashes': 1.1.5 + '@noble/secp256k1': 1.7.1 + '@stacks/common': 7.6.0 + '@stacks/network': 7.6.0 + c32check: 2.0.0 + lodash.clonedeep: 4.5.0 + transitivePeerDependencies: + - encoding + + '@stacks/wallet-sdk@7.6.0': + dependencies: + '@scure/bip32': 1.1.3 + '@scure/bip39': 1.1.0 + '@stacks/auth': 7.6.0 + '@stacks/common': 7.6.0 + '@stacks/encryption': 7.6.0 + '@stacks/network': 7.6.0 + '@stacks/profile': 7.6.0 + '@stacks/storage': 7.6.0 + '@stacks/transactions': 7.6.0 + c32check: 2.0.0 + jsontokens: 4.0.1 + zone-file: 2.0.0-beta.3 + transitivePeerDependencies: + - encoding + '@standard-schema/spec@1.1.0': {} '@starknet-io/get-starknet-core@4.0.8': @@ -34289,6 +37242,37 @@ snapshots: - debug - supports-color + '@stencil/core@4.45.2': + optionalDependencies: + '@rollup/rollup-darwin-arm64': 4.44.0 + '@rollup/rollup-darwin-x64': 4.44.0 + '@rollup/rollup-linux-arm64-gnu': 4.44.0 + '@rollup/rollup-linux-arm64-musl': 4.44.0 + '@rollup/rollup-linux-x64-gnu': 4.44.0 + '@rollup/rollup-linux-x64-musl': 4.44.0 + '@rollup/rollup-win32-arm64-msvc': 4.44.0 + '@rollup/rollup-win32-x64-msvc': 4.44.0 + + '@stencil/react-output-target@1.6.2(@stencil/core@4.45.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@lit/react': 1.0.8(@types/react@19.3.0) + '@stencil/core': 4.45.2 + html-react-parser: 5.2.17(@types/react@19.3.0)(react@19.3.0) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + react-style-stringify: 1.2.0 + ts-morph: 28.0.0 + transitivePeerDependencies: + - '@types/react' + optional: true + + '@stencil/vue-output-target@0.14.3(@stencil/core@4.45.2)(vue@3.5.43(typescript@5.9.3))': + dependencies: + vue: 3.5.43(typescript@5.9.3) + optionalDependencies: + '@stencil/core': 4.45.2 + optional: true + '@stricahq/bip32ed25519@1.1.2': dependencies: blakejs: 1.2.1 @@ -34704,6 +37688,13 @@ snapshots: '@trpc/server@10.45.4': {} + '@ts-morph/common@0.29.0': + dependencies: + minimatch: 10.2.6 + path-browserify: 1.0.1 + tinyglobby: 0.2.17 + optional: true + '@turnkey/api-key-stamper@0.4.5': dependencies: '@noble/curves': 1.9.7 @@ -34896,6 +37887,10 @@ snapshots: dependencies: '@types/node': 22.20.5 + '@types/bn.js@5.1.6': + dependencies: + '@types/node': 22.20.5 + '@types/bn.js@5.2.0': dependencies: '@types/node': 22.20.5 @@ -35133,10 +38128,19 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 22.20.5 + form-data: 4.0.6 + '@types/node@11.11.6': {} '@types/node@12.20.55': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@22.20.5': dependencies: undici-types: 6.21.0 @@ -35166,6 +38170,8 @@ snapshots: dependencies: '@types/node': 22.20.5 + '@types/qs@6.9.10': {} + '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: '@types/react': 19.3.0 @@ -35592,18 +38598,18 @@ snapshots: transitivePeerDependencies: - typescript - '@wagmi/connectors@6.2.0(428aff905596a60eebcce6893d38b837)': + '@wagmi/connectors@6.2.0(2a6925cfdca15d9c0c10695e8004f135)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@gemini-wallet/core': 0.3.2(supports-color@5.5.0)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(supports-color@5.5.0)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) cbw-sdk: '@coinbase/wallet-sdk@3.9.3(supports-color@5.5.0)' - porto: 0.2.35(ae15c160b8a2c0f4aa0d1c8501d5a081) + porto: 0.2.35(3196dd928d628e279521689dd6a3f17e) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) optionalDependencies: typescript: 5.9.3 @@ -35650,18 +38656,18 @@ snapshots: - wagmi - zod - '@wagmi/connectors@6.2.0(d8fa758b0acd7fca8d227b76a9c6f906)': + '@wagmi/connectors@6.2.0(6df5f7c1f725c65a94ed8b4dd6e309e9)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@base-org/account': 2.4.0(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@gemini-wallet/core': 0.3.2(supports-color@10.2.2)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) cbw-sdk: '@coinbase/wallet-sdk@3.9.3(supports-color@10.2.2)' - porto: 0.2.35(7042b70557cb9848dbae83cba6f51b7c) + porto: 0.2.35(3fb600e36ebb746f6a0395f199e75848) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) optionalDependencies: typescript: 5.9.3 @@ -35708,34 +38714,34 @@ snapshots: - wagmi - zod - '@wagmi/connectors@8.2.0(87153c2f809509eb916a8e14d6dfd1b7)': + '@wagmi/connectors@8.2.0(6c8dc751a4562f1028bdb7866d5ae208)': dependencies: - '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) optionalDependencies: - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@walletconnect/ethereum-provider': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/ethereum-provider': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) typescript: 5.9.3 - '@wagmi/connectors@8.2.0(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)))(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))': + '@wagmi/connectors@8.2.0(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)))(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))': dependencies: - '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) + '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) optionalDependencies: - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) typescript: 5.9.3 optional: true - '@wagmi/core@2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - zustand: 5.0.0(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + zustand: 5.0.0(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: '@tanstack/query-core': 5.104.1 typescript: 5.9.3 @@ -35745,12 +38751,12 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - zustand: 5.0.0(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.0(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) optionalDependencies: '@tanstack/query-core': 5.104.1 typescript: 5.9.3 @@ -35760,12 +38766,12 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))': + '@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - zustand: 5.0.0(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + zustand: 5.0.0(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: '@tanstack/query-core': 5.104.1 typescript: 5.9.3 @@ -35775,12 +38781,12 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.0(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.0(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) optionalDependencies: '@tanstack/query-core': 5.104.1 typescript: 5.9.3 @@ -35790,12 +38796,12 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))': + '@wagmi/core@3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - zustand: 5.0.0(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.0(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) optionalDependencies: '@tanstack/query-core': 5.104.1 typescript: 5.9.3 @@ -36123,6 +39129,50 @@ snapshots: - utf-8-validate - zod + '@walletconnect/core@2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.39.3 + events: 3.3.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/core@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@walletconnect/heartbeat': 1.2.2 @@ -36343,6 +39393,50 @@ snapshots: - utf-8-validate - zod + '@walletconnect/core@2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/logger': 3.0.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.6.5) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.44.0 + events: 3.3.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/core@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@walletconnect/heartbeat': 1.2.2 @@ -36620,9 +39714,9 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: - '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) '@walletconnect/types': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) @@ -36666,9 +39760,9 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': + '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: - '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@19.3.0)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) '@walletconnect/types': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) @@ -36712,9 +39806,9 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': + '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5)': dependencies: - '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))) '@walletconnect/types': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))) @@ -36758,9 +39852,9 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.8.19(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(immer@10.1.1)(react@19.3.0)(supports-color@8.1.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10))) '@walletconnect/types': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@react-native/jest-preset@0.86.3(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@5.0.10))) @@ -37379,6 +40473,42 @@ snapshots: - utf-8-validate - zod + '@walletconnect/sign-client@2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@walletconnect/core': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/sign-client@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@walletconnect/core': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -37559,6 +40689,42 @@ snapshots: - utf-8-validate - zod + '@walletconnect/sign-client@2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@walletconnect/core': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 3.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.6.5) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/sign-client@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@walletconnect/core': 2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -37884,6 +41050,35 @@ snapshots: - ioredis - uploadthing + '@walletconnect/types@2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + '@walletconnect/types@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 @@ -38363,6 +41558,46 @@ snapshots: - utf-8-validate - zod + '@walletconnect/universal-provider@2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + '@walletconnect/types': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + es-toolkit: 1.39.3 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/universal-provider@2.23.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': dependencies: '@walletconnect/events': 1.0.1 @@ -38869,6 +42104,53 @@ snapshots: - utf-8-validate - zod + '@walletconnect/utils@2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)': + dependencies: + '@msgpack/msgpack': 3.1.2 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + blakejs: 1.2.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.1 + viem: 2.31.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/utils@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.6.5)': dependencies: '@msgpack/msgpack': 3.1.2 @@ -39091,6 +42373,50 @@ snapshots: - uploadthing - zod + '@walletconnect/utils@2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.6.5)': + dependencies: + '@msgpack/msgpack': 3.1.3 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/logger': 3.0.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))) + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + blakejs: 1.2.1 + detect-browser: 5.3.0 + ox: 0.9.3(typescript@5.9.3)(zod@4.6.5) + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - typescript + - uploadthing + - zod + '@walletconnect/utils@2.25.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.6.5)': dependencies: '@msgpack/msgpack': 3.1.3 @@ -39289,6 +42615,13 @@ snapshots: '@webext-core/match-patterns@2.0.0': {} + '@wharfkit/antelope@4.0.2': + dependencies: + '@noble/curves': 2.4.0 + '@noble/hashes': 2.4.0 + bn.js: 5.2.5 + pako: 3.0.2 + '@wxt-dev/browser@0.3.4': dependencies: '@types/filesystem': 0.0.36 @@ -39323,6 +42656,25 @@ snapshots: '@xmldom/xmldom@0.9.12': {} + '@xrplf/isomorphic@1.0.2(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + dependencies: + '@noble/hashes': 2.4.0 + eventemitter3: 5.0.1 + ws: 8.22.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@xrplf/mpt-crypto@0.1.1': {} + + '@xrplf/secret-numbers@3.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + dependencies: + '@xrplf/isomorphic': 1.0.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ripple-keypairs: 3.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@xstate/react@1.6.3(@types/react@19.3.0)(react@18.3.1)(xstate@4.38.3)': dependencies: react: 18.3.1 @@ -39731,6 +43083,10 @@ snapshots: dependencies: tslib: 2.8.1 + async@2.6.4: + dependencies: + lodash: 4.18.1 + async@3.2.6: {} asynckit@0.4.0: {} @@ -39781,6 +43137,16 @@ snapshots: - debug - supports-color + axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2): + dependencies: + follow-redirects: 1.16.1(debug@4.4.3(supports-color@10.2.2)) + form-data: 4.0.6 + https-proxy-agent: 5.0.1(supports-color@10.2.2) + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + axios@1.20.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1): dependencies: follow-redirects: 1.16.1(debug@4.4.3(supports-color@8.1.1)) @@ -40459,10 +43825,14 @@ snapshots: base58-js@3.0.3: {} + base64-arraybuffer@0.2.0: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.11.27: {} + bech32@1.1.4: {} + bech32@2.0.0: {} better-path-resolve@1.0.0: @@ -40485,6 +43855,8 @@ snapshots: bignumber.js@9.1.1: {} + bignumber.js@9.1.2: {} + bignumber.js@9.3.1: {} binary-extensions@2.3.0: {} @@ -40503,6 +43875,17 @@ snapshots: bip32-path@0.4.2: {} + bip322-js@2.0.0: + dependencies: + '@bitcoinerlab/secp256k1': 1.2.0 + bitcoinjs-lib: 6.1.8 + bitcoinjs-message: 2.2.0 + ecpair: 2.1.0 + elliptic: 6.6.1 + fast-sha256: 1.3.0 + secp256k1: 5.0.1 + optional: true + bip322-js@4.0.0(typescript@5.9.3): dependencies: '@bitcoinerlab/secp256k1': 1.2.0 @@ -40537,6 +43920,11 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + bip66@1.1.5: + dependencies: + safe-buffer: 5.2.1 + optional: true + bip68@1.0.4: {} birpc@2.9.0: {} @@ -40568,6 +43956,20 @@ snapshots: transitivePeerDependencies: - typescript + bitcoinjs-message@2.2.0: + dependencies: + bech32: 1.1.4 + bs58check: 2.1.2 + buffer-equals: 1.0.4 + create-hash: 1.2.0 + secp256k1: 3.8.1 + varuint-bitcoin: 1.1.2 + optional: true + + blake2b-wasm@1.1.7: + dependencies: + nanoassert: 1.1.0 + blake2b-wasm@2.4.0: dependencies: b4a: 1.9.0 @@ -40575,6 +43977,11 @@ snapshots: transitivePeerDependencies: - react-native-b4a + blake2b@2.1.3: + dependencies: + blake2b-wasm: 1.1.7 + nanoassert: 1.1.0 + blake2b@2.1.4: dependencies: blake2b-wasm: 2.4.0 @@ -40607,6 +44014,16 @@ snapshots: boolean@3.2.0: optional: true + borc@2.1.2: + dependencies: + bignumber.js: 9.3.1 + buffer: 5.7.1 + commander: 2.20.3 + ieee754: 1.2.1 + iso-url: 0.4.7 + json-text-sequence: 0.1.1 + readable-stream: 3.6.2 + borsh@0.7.0: dependencies: bn.js: 5.2.5 @@ -40663,6 +44080,16 @@ snapshots: dependencies: base64-js: 1.5.1 + browserify-aes@1.2.0: + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.7 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + optional: true + browserslist@4.29.3: dependencies: baseline-browser-mapping: 2.11.27 @@ -40703,12 +44130,18 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equals@1.0.4: + optional: true + buffer-from@1.1.2: {} buffer-image-size@0.6.4: dependencies: '@types/node': 22.20.5 + buffer-xor@1.0.3: + optional: true + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -40760,6 +44193,11 @@ snapshots: dependencies: run-applescript: 7.1.0 + bundle-require@5.1.0(esbuild@0.25.3): + dependencies: + esbuild: 0.25.3 + load-tsconfig: 0.2.5 + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -40788,6 +44226,11 @@ snapshots: optionalDependencies: magicast: 0.5.5 + c32check@2.0.0: + dependencies: + '@noble/hashes': 1.8.0 + base-x: 4.0.1 + cac@6.7.14: {} cac@7.0.0: {} @@ -41043,6 +44486,10 @@ snapshots: cli-spinners@3.4.0: {} + cli-table@0.3.11: + dependencies: + colors: 1.0.3 + cliui@6.0.0: dependencies: string-width: 4.2.3 @@ -41067,6 +44514,9 @@ snapshots: co@4.6.0: {} + code-block-writer@13.0.3: + optional: true + collect-v8-coverage@1.0.3: {} color-convert@1.9.3: @@ -41102,6 +44552,8 @@ snapshots: colorette@2.0.20: {} + colors@1.0.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -41214,9 +44666,9 @@ snapshots: transitivePeerDependencies: - supports-color - connectkit@1.9.2(@babel/core@7.29.7(supports-color@5.5.0))(@tanstack/react-query@5.104.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@19.3.0)(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(128dd19e588211bd4f16254762e0e3f1)): + connectkit@1.9.2(@babel/core@7.29.7(supports-color@5.5.0))(@tanstack/react-query@5.104.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@19.3.0)(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(6830db7ba68b15a013c6582bffdd96ea)): dependencies: - '@aave/account': 0.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(128dd19e588211bd4f16254762e0e3f1)) + '@aave/account': 0.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5))(wagmi@2.19.5(6830db7ba68b15a013c6582bffdd96ea)) '@tanstack/react-query': 5.104.1(react@18.3.1) buffer: 6.0.3 detect-browser: 5.3.0 @@ -41229,7 +44681,7 @@ snapshots: resize-observer-polyfill: 1.5.1 styled-components: 5.3.11(@babel/core@7.29.7(supports-color@5.5.0))(react-dom@18.3.1(react@18.3.1))(react-is@19.3.0)(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) - wagmi: 2.19.5(128dd19e588211bd4f16254762e0e3f1) + wagmi: 2.19.5(6830db7ba68b15a013c6582bffdd96ea) transitivePeerDependencies: - '@babel/core' - react-is @@ -41274,6 +44726,8 @@ snapshots: path-type: 4.0.0 yaml: 1.10.3 + cosmjs-types@0.11.0: {} + crc-32@1.2.2: {} crc@3.8.0: @@ -41783,6 +45237,8 @@ snapshots: delayed-stream@1.0.0: {} + delimit-stream@0.1.0: {} + depd@1.1.2: {} depd@2.0.0: {} @@ -41951,6 +45407,13 @@ snapshots: dependencies: detect-libc: 1.0.3 + drbg.js@1.0.1: + dependencies: + browserify-aes: 1.2.0 + create-hash: 1.2.0 + create-hmac: 1.1.7 + optional: true + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -41968,6 +45431,8 @@ snapshots: readable-stream: 3.6.2 stream-shift: 1.0.3 + eastasianwidth@0.2.0: {} + eciesjs@0.4.18: dependencies: '@ecies/ciphers': 0.2.6(@noble/ciphers@1.3.0) @@ -41989,6 +45454,12 @@ snapshots: transitivePeerDependencies: - typescript + ed25519-hd-key@1.1.2: + dependencies: + bip39: 3.0.2 + create-hmac: 1.1.7 + tweetnacl: 1.0.3 + ed25519-hd-key@2.0.0: dependencies: '@noble/ed25519': 3.1.0 @@ -42155,6 +45626,8 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encode-utf8@1.0.3: {} encodeurl@1.0.2: {} @@ -42311,6 +45784,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.25.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.3 + '@esbuild/android-arm': 0.25.3 + '@esbuild/android-arm64': 0.25.3 + '@esbuild/android-x64': 0.25.3 + '@esbuild/darwin-arm64': 0.25.3 + '@esbuild/darwin-x64': 0.25.3 + '@esbuild/freebsd-arm64': 0.25.3 + '@esbuild/freebsd-x64': 0.25.3 + '@esbuild/linux-arm': 0.25.3 + '@esbuild/linux-arm64': 0.25.3 + '@esbuild/linux-ia32': 0.25.3 + '@esbuild/linux-loong64': 0.25.3 + '@esbuild/linux-mips64el': 0.25.3 + '@esbuild/linux-ppc64': 0.25.3 + '@esbuild/linux-riscv64': 0.25.3 + '@esbuild/linux-s390x': 0.25.3 + '@esbuild/linux-x64': 0.25.3 + '@esbuild/netbsd-arm64': 0.25.3 + '@esbuild/netbsd-x64': 0.25.3 + '@esbuild/openbsd-arm64': 0.25.3 + '@esbuild/openbsd-x64': 0.25.3 + '@esbuild/sunos-x64': 0.25.3 + '@esbuild/win32-arm64': 0.25.3 + '@esbuild/win32-ia32': 0.25.3 + '@esbuild/win32-x64': 0.25.3 + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -42502,6 +46003,32 @@ snapshots: - bufferutil - utf-8-validate + ethers@6.17.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ethers@6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + event-target-shim@5.0.1: {} eventemitter2@6.4.9: {} @@ -42528,6 +46055,12 @@ snapshots: dependencies: eventsource-parser: 3.1.1 + evp_bytestokey@1.0.3: + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + optional: true + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -43286,6 +46819,8 @@ snapshots: fetch-nodeshim@0.4.10: {} + fflate@0.8.2: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -43376,6 +46911,10 @@ snapshots: optionalDependencies: debug: 4.4.1(supports-color@5.5.0) + follow-redirects@1.16.1(debug@4.4.3(supports-color@10.2.2)): + optionalDependencies: + debug: 4.4.3(supports-color@10.2.2) + follow-redirects@1.16.1(debug@4.4.3(supports-color@8.1.1)): optionalDependencies: debug: 4.4.3(supports-color@8.1.1) @@ -43386,6 +46925,11 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + forge-light@1.1.4: {} form-data@4.0.6: @@ -43489,6 +47033,114 @@ snapshots: fsevents@2.3.3: optional: true + fuels@0.103.0(supports-color@10.2.2)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))): + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/abi-typegen': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/contract': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/recipes': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/script': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + '@fuels/vm-asm': 0.62.0 + bundle-require: 5.1.0(esbuild@0.25.3) + chalk: 4.1.2 + chokidar: 3.6.0 + commander: 13.1.0 + esbuild: 0.25.3 + glob: 10.4.5 + handlebars: 4.7.8 + joycon: 3.1.1 + lodash.camelcase: 4.3.0 + portfinder: 1.0.32(supports-color@10.2.2) + toml: 3.0.0 + uglify-js: 3.19.3 + yup: 1.6.1 + transitivePeerDependencies: + - encoding + - supports-color + - vitest + + fuels@0.103.0(supports-color@10.2.2)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))): + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/abi-typegen': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/contract': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/recipes': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/script': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + '@fuels/vm-asm': 0.62.0 + bundle-require: 5.1.0(esbuild@0.25.3) + chalk: 4.1.2 + chokidar: 3.6.0 + commander: 13.1.0 + esbuild: 0.25.3 + glob: 10.4.5 + handlebars: 4.7.8 + joycon: 3.1.1 + lodash.camelcase: 4.3.0 + portfinder: 1.0.32(supports-color@10.2.2) + toml: 3.0.0 + uglify-js: 3.19.3 + yup: 1.6.1 + transitivePeerDependencies: + - encoding + - supports-color + - vitest + + fuels@0.103.0(supports-color@8.1.1)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))): + dependencies: + '@fuel-ts/abi-coder': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/abi-typegen': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/account': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/address': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/contract': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/crypto': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/errors': 0.103.0 + '@fuel-ts/hasher': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/math': 0.103.0 + '@fuel-ts/program': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/recipes': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/script': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/transactions': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/utils': 0.103.0(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@fuel-ts/versions': 0.103.0 + '@fuels/vm-asm': 0.62.0 + bundle-require: 5.1.0(esbuild@0.25.3) + chalk: 4.1.2 + chokidar: 3.6.0 + commander: 13.1.0 + esbuild: 0.25.3 + glob: 10.4.5 + handlebars: 4.7.8 + joycon: 3.1.1 + lodash.camelcase: 4.3.0 + portfinder: 1.0.32(supports-color@8.1.1) + toml: 3.0.0 + uglify-js: 3.19.3 + yup: 1.6.1 + transitivePeerDependencies: + - encoding + - supports-color + - vitest + function-bind@1.1.2: {} generate-function@2.3.1: @@ -43560,6 +47212,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@13.0.6: dependencies: minimatch: 10.2.6 @@ -43594,7 +47255,6 @@ snapshots: dependencies: define-properties: 1.2.1 gopd: 1.2.0 - optional: true globby@11.1.0: dependencies: @@ -43605,6 +47265,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + google-protobuf@3.21.4: {} + gopd@1.2.0: {} got@11.8.6: @@ -43650,10 +47312,25 @@ snapshots: graceful-fs@4.2.11: {} + graphql-request@6.1.0(graphql@16.10.0): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + cross-fetch: 3.2.0 + graphql: 16.10.0 + transitivePeerDependencies: + - encoding + + graphql-tag@2.12.6(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + tslib: 2.8.1 + graphql-ws@5.16.2(graphql@16.14.2): dependencies: graphql: 16.14.2 + graphql@16.10.0: {} + graphql@16.14.2: {} graphql@17.0.2: {} @@ -43672,6 +47349,15 @@ snapshots: hachure-fill@0.5.2: {} + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: '@types/node': 22.20.5 @@ -43825,6 +47511,12 @@ snapshots: htm@3.1.1: {} + html-dom-parser@5.1.8: + dependencies: + domhandler: 5.0.3 + htmlparser2: 10.1.0 + optional: true + html-encoding-sniffer@3.0.0: dependencies: whatwg-encoding: 2.0.0 @@ -43843,6 +47535,17 @@ snapshots: dependencies: void-elements: 3.1.0 + html-react-parser@5.2.17(@types/react@19.3.0)(react@19.3.0): + dependencies: + domhandler: 5.0.3 + html-dom-parser: 5.1.8 + react: 19.3.0 + react-property: 2.0.2 + style-to-js: 1.1.21 + optionalDependencies: + '@types/react': 19.3.0 + optional: true + html-void-elements@3.0.0: {} htmlparser2@10.1.0: @@ -43971,6 +47674,8 @@ snapshots: ignore@5.3.2: {} + immer@10.1.1: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -44005,6 +47710,9 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.7: + optional: true + internmap@1.0.1: {} internmap@2.0.3: {} @@ -44153,6 +47861,8 @@ snapshots: isexe@4.0.0: {} + iso-url@0.4.7: {} + iso-url@1.2.1: {} isomorphic-ws@4.0.1(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@5.0.10)): @@ -44167,6 +47877,10 @@ snapshots: dependencies: ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + isows@1.0.7(ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10) + isows@1.0.7(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -44283,6 +47997,12 @@ snapshots: transitivePeerDependencies: - '@types/react' + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jake@10.9.4: dependencies: async: 3.2.6 @@ -44984,6 +48704,10 @@ snapshots: json-stringify-safe@5.0.1: {} + json-text-sequence@0.1.1: + dependencies: + delimit-stream: 0.1.0 + json2mq@0.2.0: dependencies: string-convert: 0.2.1 @@ -45018,6 +48742,12 @@ snapshots: dependencies: commander: 8.3.0 + keccak@3.0.2: + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.8.4 + readable-stream: 3.6.2 + keccak@3.0.4: dependencies: node-addon-api: 2.0.2 @@ -45166,6 +48896,8 @@ snapshots: dependencies: uc.micro: 2.1.0 + linkifyjs@4.3.3: {} + lit-element@3.3.3: dependencies: '@lit-labs/ssr-dom-shim': 1.6.0 @@ -45220,20 +48952,39 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.capitalize@4.2.1: + optional: true + + lodash.clonedeep@4.5.0: {} + lodash.debounce@4.0.8: {} lodash.escaperegexp@4.1.2: {} + lodash.inrange@3.3.6: + optional: true + + lodash.isempty@4.4.0: {} + lodash.isequal@4.5.0: {} + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} + lodash.range@3.2.0: + optional: true + lodash.startcase@4.4.0: {} lodash.throttle@4.1.1: {} + lodash.trimend@4.18.0: {} + + lodash.uniq@4.5.0: {} + lodash@4.18.1: {} log-symbols@2.2.0: @@ -46395,6 +50146,8 @@ snapshots: mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + mlly@1.8.2: dependencies: acorn: 8.18.0 @@ -46448,6 +50201,8 @@ snapshots: nan@2.29.0: optional: true + nanoassert@1.1.0: {} + nanoassert@2.0.0: {} nanoid@3.3.19: {} @@ -46503,6 +50258,8 @@ snapshots: dependencies: content-type: 2.1.0 + neo-async@2.6.2: {} + nice-grpc-client-middleware-retry@3.1.16: dependencies: abort-controller-x: 0.5.0 @@ -46672,6 +50429,8 @@ snapshots: object-hash@3.0.0: {} + object-inspect@1.13.4: {} + object-is@1.1.6: dependencies: call-bind: 1.0.9 @@ -46927,6 +50686,37 @@ snapshots: transitivePeerDependencies: - zod + ox@0.7.1(typescript@5.9.3)(zod@4.6.5): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.3.0(typescript@5.9.3)(zod@4.6.5) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + + ox@0.8.1(typescript@5.9.3)(zod@4.6.5): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.3.0(typescript@5.9.3)(zod@4.6.5) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + optional: true + ox@0.9.17(typescript@5.9.3)(zod@4.6.5): dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -47042,6 +50832,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 @@ -47050,6 +50842,8 @@ snapshots: pako@2.2.0: {} + pako@3.0.2: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -47083,6 +50877,9 @@ snapshots: parseurl@1.3.3: {} + path-browserify@1.0.1: + optional: true + path-data-parser@0.1.0: {} path-exists@4.0.0: {} @@ -47095,6 +50892,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-scurry@2.0.2: dependencies: lru-cache: 11.5.3 @@ -47240,6 +51042,8 @@ snapshots: pvutils: 1.2.0 tslib: 2.8.1 + platform@1.3.6: {} + playwright-core@1.63.0: {} playwright@1.63.0: @@ -47313,47 +51117,63 @@ snapshots: style-value-types: 5.1.2 tslib: 2.4.0 - porto@0.2.35(7042b70557cb9848dbae83cba6f51b7c): + portfinder@1.0.32(supports-color@10.2.2): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + async: 2.6.4 + debug: 3.2.7(supports-color@10.2.2) + mkdirp: 0.5.6 + transitivePeerDependencies: + - supports-color + + portfinder@1.0.32(supports-color@8.1.1): + dependencies: + async: 2.6.4 + debug: 3.2.7(supports-color@8.1.1) + mkdirp: 0.5.6 + transitivePeerDependencies: + - supports-color + + porto@0.2.35(3196dd928d628e279521689dd6a3f17e): + dependencies: + '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) hono: 4.13.13 idb-keyval: 6.3.0 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.17(typescript@5.9.3)(zod@4.6.5) - viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) zod: 4.6.5 - zustand: 5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) + zustand: 5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: - '@tanstack/react-query': 5.104.1(react@19.3.0) + '@tanstack/react-query': 5.104.1(react@18.3.1) expo-crypto: 57.0.3(expo@57.0.26) - expo-web-browser: 57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)) - react: 19.3.0 - react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + expo-web-browser: 57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)) + react: 18.3.1 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10) typescript: 5.9.3 - wagmi: 2.19.5(36cbd6af1fba239fc5835c3ea32e5981) + wagmi: 2.19.5(6830db7ba68b15a013c6582bffdd96ea) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(ae15c160b8a2c0f4aa0d1c8501d5a081): + porto@0.2.35(3fb600e36ebb746f6a0395f199e75848): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) hono: 4.13.13 idb-keyval: 6.3.0 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.17(typescript@5.9.3)(zod@4.6.5) - viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) + viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) zod: 4.6.5 - zustand: 5.0.15(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + zustand: 5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)) optionalDependencies: - '@tanstack/react-query': 5.104.1(react@18.3.1) + '@tanstack/react-query': 5.104.1(react@19.3.0) expo-crypto: 57.0.3(expo@57.0.26) - expo-web-browser: 57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)) - react: 18.3.1 - react-native: 0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10) + expo-web-browser: 57.0.3(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)) + react: 19.3.0 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) typescript: 5.9.3 - wagmi: 2.19.5(128dd19e588211bd4f16254762e0e3f1) + wagmi: 2.19.5(f6fa2c3037d24512664382b670f973d0) transitivePeerDependencies: - '@types/react' - immer @@ -47486,6 +51306,8 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 + property-expr@2.0.6: {} + property-information@7.2.0: {} protobufjs@7.5.4: @@ -47596,6 +51418,10 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qs@6.11.2: + dependencies: + side-channel: 1.1.1 + quansync@0.2.11: {} query-string@6.13.5: @@ -47621,6 +51447,8 @@ snapshots: radix3@1.1.2: {} + ramda@0.30.1: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -48430,6 +52258,9 @@ snapshots: - utf-8-validate optional: true + react-property@2.0.2: + optional: true + react-qrcode-logo@4.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: qrcode-generator: 2.0.4 @@ -48518,7 +52349,7 @@ snapshots: optionalDependencies: react-dom: 19.3.0(react@19.3.0) - react-spring-bottom-sheet@3.5.0-alpha.0(@react-three/fiber@9.8.1(1b8196e6ab68073e033beecf79b81774))(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3): + react-spring-bottom-sheet@3.5.0-alpha.0(@react-three/fiber@9.8.1(0d1a6e9022da08806fd1513b41507824))(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3): dependencies: '@juggle/resize-observer': 3.4.0 '@reach/portal': 0.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -48526,7 +52357,7 @@ snapshots: body-scroll-lock: 3.1.5 focus-trap: 6.9.4 react: 18.3.1 - react-spring: 9.7.5(@react-three/fiber@9.8.1(1b8196e6ab68073e033beecf79b81774))(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3) + react-spring: 9.7.5(@react-three/fiber@9.8.1(0d1a6e9022da08806fd1513b41507824))(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3) react-use-gesture: 8.0.1(react@18.3.1) xstate: 4.38.3 transitivePeerDependencies: @@ -48541,7 +52372,7 @@ snapshots: - three - zdog - react-spring-bottom-sheet@3.5.0-alpha.0(@react-three/fiber@9.8.1(43ebe24fc3ae5dca528188986d089789))(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3): + react-spring-bottom-sheet@3.5.0-alpha.0(@react-three/fiber@9.8.1(9de97b7269446073e8ab8cc6ac08c6e3))(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3): dependencies: '@juggle/resize-observer': 3.4.0 '@reach/portal': 0.13.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -48549,7 +52380,7 @@ snapshots: body-scroll-lock: 3.1.5 focus-trap: 6.9.4 react: 19.3.0 - react-spring: 9.7.5(@react-three/fiber@9.8.1(43ebe24fc3ae5dca528188986d089789))(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3) + react-spring: 9.7.5(@react-three/fiber@9.8.1(9de97b7269446073e8ab8cc6ac08c6e3))(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3) react-use-gesture: 8.0.1(react@19.3.0) xstate: 4.38.3 transitivePeerDependencies: @@ -48564,12 +52395,12 @@ snapshots: - three - zdog - react-spring@9.7.5(@react-three/fiber@9.8.1(1b8196e6ab68073e033beecf79b81774))(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3): + react-spring@9.7.5(@react-three/fiber@9.8.1(0d1a6e9022da08806fd1513b41507824))(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@18.3.1)(three@0.186.1)(zdog@1.1.3): dependencies: '@react-spring/core': 9.7.5(react@18.3.1) '@react-spring/konva': 9.7.5(konva@10.7.0)(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@react-spring/native': 9.7.5(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10))(react@18.3.1) - '@react-spring/three': 9.7.5(@react-three/fiber@9.8.1(1b8196e6ab68073e033beecf79b81774))(react@18.3.1)(three@0.186.1) + '@react-spring/three': 9.7.5(@react-three/fiber@9.8.1(0d1a6e9022da08806fd1513b41507824))(react@18.3.1)(three@0.186.1) '@react-spring/web': 9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-spring/zdog': 9.7.5(react-dom@18.3.1(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(zdog@1.1.3) react: 18.3.1 @@ -48583,12 +52414,12 @@ snapshots: - three - zdog - react-spring@9.7.5(@react-three/fiber@9.8.1(43ebe24fc3ae5dca528188986d089789))(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3): + react-spring@9.7.5(@react-three/fiber@9.8.1(9de97b7269446073e8ab8cc6ac08c6e3))(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react-zdog@1.2.2)(react@19.3.0)(three@0.186.1)(zdog@1.1.3): dependencies: '@react-spring/core': 9.7.5(react@19.3.0) '@react-spring/konva': 9.7.5(konva@10.7.0)(react-konva@18.2.16(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0) '@react-spring/native': 9.7.5(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10))(react@19.3.0) - '@react-spring/three': 9.7.5(@react-three/fiber@9.8.1(43ebe24fc3ae5dca528188986d089789))(react@19.3.0)(three@0.186.1) + '@react-spring/three': 9.7.5(@react-three/fiber@9.8.1(9de97b7269446073e8ab8cc6ac08c6e3))(react@19.3.0)(three@0.186.1) '@react-spring/web': 9.7.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@react-spring/zdog': 9.7.5(react-dom@19.3.0(react@19.3.0))(react-zdog@1.2.2)(react@19.3.0)(zdog@1.1.3) react: 19.3.0 @@ -48628,6 +52459,11 @@ snapshots: optionalDependencies: '@types/react': 19.3.0 + react-style-stringify@1.2.0: + dependencies: + '@emotion/unitless': 0.10.0 + optional: true + react-test-renderer@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -48745,6 +52581,8 @@ snapshots: readdirp@5.1.1: {} + readonly-date-esm@2.0.0: {} + real-require@0.1.0: {} real-require@0.2.0: {} @@ -48874,6 +52712,12 @@ snapshots: dependencies: glob: 7.2.3 + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + ripemd160-min@0.0.6: {} + ripemd160@2.0.3: dependencies: hash-base: 3.1.2 @@ -48884,6 +52728,14 @@ snapshots: base-x: 3.0.11 create-hash: 1.2.0 + ripple-address-codec@5.0.1(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@scure/base': 2.4.0 + '@xrplf/isomorphic': 1.0.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + ripple-binary-codec@1.11.0: dependencies: assert: 2.1.0 @@ -48893,6 +52745,24 @@ snapshots: decimal.js: 10.6.0 ripple-address-codec: 4.3.1 + ripple-binary-codec@2.11.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@xrplf/isomorphic': 1.0.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + bignumber.js: 10.0.2 + ripple-address-codec: 5.0.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ripple-keypairs@3.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@noble/curves': 2.4.0 + '@xrplf/isomorphic': 1.0.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ripple-address-codec: 5.0.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -49054,16 +52924,34 @@ snapshots: scheduler@0.28.0: {} + schema-inspector@2.1.0: + dependencies: + async: 2.6.4 + scroll-into-view-if-needed@3.1.0: dependencies: compute-scroll-into-view: 3.1.1 + scryptsy@2.1.0: {} + scule@1.3.0: {} sdp@3.2.2: {} search-insights@2.17.3: {} + secp256k1@3.8.1: + dependencies: + bindings: 1.5.0 + bip66: 1.1.5 + bn.js: 4.12.5 + create-hash: 1.2.0 + drbg.js: 1.0.1 + elliptic: 6.6.1 + nan: 2.29.0 + safe-buffer: 5.2.1 + optional: true + secp256k1@4.0.5: dependencies: elliptic: 6.6.1 @@ -49278,6 +53166,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -49290,6 +53206,8 @@ snapshots: dependencies: signal-polyfill: 0.2.2 + simple-cbor@0.4.1: {} + simple-plist@1.3.1: dependencies: bplist-creator: 0.1.0 @@ -49327,6 +53245,17 @@ snapshots: - bufferutil - utf-8-validate + socket.io-client@4.8.3(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@10.2.2) + engine.io-client: 6.6.7(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.7(supports-color@10.2.2) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + socket.io-client@4.8.4(bufferutil@4.1.0)(supports-color@10.2.2)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 @@ -49486,10 +53415,10 @@ snapshots: pako: 2.2.0 ts-mixer: 6.0.4 - starknetkit@3.4.3(1c2478886b26ffb945b93bfbff4fa192): + starknetkit@3.4.3(9929b9e50ebc9ec98a319d4fa0d43aa0): dependencies: - '@argent/x-ui': 1.109.1(4e84717f2760c66a48b36c0f5fc6bcaa) - '@cartridge/controller': 0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) + '@argent/x-ui': 1.109.1(6faa20acdd5a370ff8c45a11746c495d) + '@cartridge/controller': 0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@5.5.0))(react@18.3.1)(supports-color@5.5.0))(@types/react@19.3.0)(bufferutil@4.1.0)(react@18.3.1)(supports-color@5.5.0)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@5.5.0))(immer@10.1.1)(react@18.3.1)(supports-color@5.5.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.0.5) '@starknet-io/get-starknet': 4.0.8 '@starknet-io/get-starknet-core': 4.0.8 '@starknet-io/types-js': 0.8.4 @@ -49573,10 +53502,10 @@ snapshots: - utf-8-validate - zod - starknetkit@3.4.3(634430694b27258fc46dd25a8218dcb9): + starknetkit@3.4.3(99f3bbb49cc003434109d2744f216011): dependencies: - '@argent/x-ui': 1.109.1(7b6fb343122df9d748def04b3b23f38c) - '@cartridge/controller': 0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) + '@argent/x-ui': 1.109.1(349d5131ce3221b5191115dc655d9f9b) + '@cartridge/controller': 0.10.7(@react-native-async-storage/async-storage@1.24.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0)(supports-color@10.2.2))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@10.2.2)(utf-8-validate@5.0.10)))(@types/react@19.3.0)(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(immer@10.1.1)(react@18.3.1)(supports-color@10.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5) '@starknet-io/get-starknet': 4.0.8 '@starknet-io/get-starknet-core': 4.0.8 '@starknet-io/types-js': 0.8.4 @@ -49710,6 +53639,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + string-width@8.3.0: dependencies: get-east-asian-width: 1.7.0 @@ -49758,6 +53693,16 @@ snapshots: structured-headers@0.4.1: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + optional: true + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + optional: true + style-value-types@5.0.0: dependencies: hey-listen: 1.0.8 @@ -49878,6 +53823,8 @@ snapshots: dependencies: react: 19.3.0 + symbol-observable@2.0.3: {} + symbol-tree@3.2.4: {} tabbable@5.3.3: {} @@ -50002,6 +53949,8 @@ snapshots: dependencies: semver: 5.7.2 + tiny-case@1.0.3: {} + tiny-open@1.3.0: {} tiny-typed-emitter@2.1.0: {} @@ -50049,6 +53998,10 @@ snapshots: toidentifier@1.0.1: {} + toml@3.0.0: {} + + toposort@2.0.2: {} + toqr@0.1.1: {} tough-cookie@4.1.4: @@ -50076,6 +54029,57 @@ snapshots: trim-lines@3.0.1: {} + tronweb@6.5.1(bufferutil@4.1.0)(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2)(utf-8-validate@5.0.10): + dependencies: + axios: 1.20.0(debug@4.4.1(supports-color@10.2.2))(supports-color@10.2.2) + bignumber.js: 9.1.2 + ethereum-cryptography: 2.2.1 + ethers: 6.17.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + eventemitter3: 5.0.1 + google-protobuf: 3.21.4 + semver: 7.7.1 + tslib: 2.7.0 + validator: 13.15.23 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + tronweb@6.5.1(bufferutil@4.1.0)(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)(utf-8-validate@6.0.6): + dependencies: + axios: 1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) + bignumber.js: 9.1.2 + ethereum-cryptography: 2.2.1 + ethers: 6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + eventemitter3: 5.0.1 + google-protobuf: 3.21.4 + semver: 7.7.1 + tslib: 2.7.0 + validator: 13.15.23 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + tronweb@6.5.1(bufferutil@4.1.0)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(utf-8-validate@6.0.6): + dependencies: + axios: 1.20.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) + bignumber.js: 9.1.2 + ethereum-cryptography: 2.2.1 + ethers: 6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + eventemitter3: 5.0.1 + google-protobuf: 3.21.4 + semver: 7.7.1 + tslib: 2.7.0 + validator: 13.15.23 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + trpc-browser@1.4.4(@trpc/client@10.45.4(@trpc/server@10.45.4))(@trpc/server@10.45.4): dependencies: '@trpc/client': 10.45.4(@trpc/server@10.45.4) @@ -50097,6 +54101,12 @@ snapshots: ts-mixer@6.0.4: {} + ts-morph@28.0.0: + dependencies: + '@ts-morph/common': 0.29.0 + code-block-writer: 13.0.3 + optional: true + ts-poet@6.12.0: dependencies: dprint-node: 1.0.8 @@ -50173,6 +54183,8 @@ snapshots: type-fest@2.19.0: {} + type-fest@4.34.1: {} + type-fest@4.41.0: {} type-fest@5.10.0: @@ -50231,6 +54243,8 @@ snapshots: ufo@1.6.4: {} + uglify-js@3.19.3: {} + uhyphen@0.2.0: {} uint8-varint@2.0.5: @@ -50279,6 +54293,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@5.26.5: {} + undici-types@6.19.8: {} undici-types@6.21.0: {} @@ -50654,6 +54670,8 @@ snapshots: validate-npm-package-name@5.0.1: {} + validator@13.15.23: {} + valtio@1.11.2(@types/react@19.3.0)(react@18.3.1): dependencies: proxy-compare: 2.5.1 @@ -50688,6 +54706,13 @@ snapshots: '@types/react': 19.3.0 react: 19.3.0 + valtio@2.1.5(@types/react@19.3.0)(react@19.3.0): + dependencies: + proxy-compare: 3.0.1 + optionalDependencies: + '@types/react': 19.3.0 + react: 19.3.0 + valtio@2.1.7(@types/react@19.3.0)(react@18.3.1): dependencies: proxy-compare: 3.0.1 @@ -50756,6 +54781,41 @@ snapshots: - utf-8-validate - zod + viem@2.31.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.0.8(typescript@5.9.3)(zod@4.6.5) + isows: 1.0.7(ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ox: 0.7.1(typescript@5.9.3)(zod@4.6.5) + ws: 8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.32.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5): + dependencies: + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.0.8(typescript@5.9.3)(zod@4.6.5) + isows: 1.0.7(ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ox: 0.8.1(typescript@5.9.3)(zod@4.6.5) + ws: 8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + optional: true + viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.1 @@ -51270,6 +55330,36 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(happy-dom@20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.2.0 + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 26.6.4 + happy-dom: 20.14.5(bufferutil@4.1.0)(utf-8-validate@6.0.6) + jsdom: 30.1.1(@noble/hashes@2.4.0) + transitivePeerDependencies: + - msw + vlq@1.0.1: {} vlq@2.0.4: {} @@ -51294,11 +55384,11 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wagmi@2.19.5(128dd19e588211bd4f16254762e0e3f1): + wagmi@2.19.5(6830db7ba68b15a013c6582bffdd96ea): dependencies: '@tanstack/react-query': 5.104.1(react@18.3.1) - '@wagmi/connectors': 6.2.0(428aff905596a60eebcce6893d38b837) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) + '@wagmi/connectors': 6.2.0(2a6925cfdca15d9c0c10695e8004f135) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5)) react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.0.5) @@ -51344,11 +55434,11 @@ snapshots: - utf-8-validate - zod - wagmi@2.19.5(36cbd6af1fba239fc5835c3ea32e5981): + wagmi@2.19.5(f6fa2c3037d24512664382b670f973d0): dependencies: '@tanstack/react-query': 5.104.1(react@19.3.0) - '@wagmi/connectors': 6.2.0(d8fa758b0acd7fca8d227b76a9c6f906) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + '@wagmi/connectors': 6.2.0(6df5f7c1f725c65a94ed8b4dd6e309e9) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) react: 19.3.0 use-sync-external-store: 1.4.0(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -51394,11 +55484,11 @@ snapshots: - utf-8-validate - zod - wagmi@3.7.7(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@tanstack/query-core@5.104.1)(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)): + wagmi@3.7.7(@coinbase/wallet-sdk@4.3.6(@types/react@19.3.0)(bufferutil@4.1.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5))(@tanstack/query-core@5.104.1)(@tanstack/react-query@5.104.1(react@19.3.0))(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)): dependencies: '@tanstack/react-query': 5.104.1(react@19.3.0) - '@wagmi/connectors': 8.2.0(87153c2f809509eb916a8e14d6dfd1b7) - '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) + '@wagmi/connectors': 8.2.0(6c8dc751a4562f1028bdb7866d5ae208) + '@wagmi/core': 3.6.5(@tanstack/query-core@5.104.1)(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.3.0))(viem@2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5)) react: 19.3.0 use-sync-external-store: 1.4.0(react@19.3.0) viem: 2.57.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) @@ -51547,6 +55637,8 @@ snapshots: dependencies: bs58check: 4.0.0 + wordwrap@1.0.0: {} + workerd@1.20260815.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260815.1 @@ -51584,6 +55676,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -51636,6 +55734,11 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 5.0.10 + ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 5.0.10 + ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 @@ -51823,8 +55926,30 @@ snapshots: xmlhttprequest-ssl@2.1.2: {} + xrpl@5.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@scure/bip32': 2.4.0 + '@scure/bip39': 2.4.0 + '@xrplf/isomorphic': 1.0.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@xrplf/mpt-crypto': 0.1.1 + '@xrplf/secret-numbers': 3.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + bignumber.js: 10.0.2 + eventemitter3: 5.0.4 + fast-json-stable-stringify: 2.1.0 + ripple-address-codec: 5.0.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ripple-binary-codec: 2.11.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ripple-keypairs: 3.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + xstate@4.38.3: {} + xstream@11.14.0: + dependencies: + globalthis: 1.0.4 + symbol-observable: 2.0.3 + xtend@4.0.2: {} y18n@4.0.3: {} @@ -51889,6 +56014,13 @@ snapshots: cookie: 1.1.1 youch-core: 0.3.3 + yup@1.6.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + zdog@1.1.3: {} zen-observable-ts@1.1.0: @@ -51908,58 +56040,77 @@ snapshots: zod@4.6.5: {} - zustand@5.0.0(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + zone-file@2.0.0-beta.3: {} + + zustand@4.4.7(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0): + dependencies: + use-sync-external-store: 1.2.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + immer: 10.1.1 + react: 19.3.0 + + zustand@5.0.0(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - zustand@5.0.0(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)): + zustand@5.0.0(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 19.3.0 use-sync-external-store: 1.4.0(react@19.3.0) - zustand@5.0.15(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + zustand@5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - zustand@5.0.15(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.7.0(react@18.3.1)): + zustand@5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.7.0(react@18.3.1)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 18.3.1 use-sync-external-store: 1.7.0(react@18.3.1) - zustand@5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)): + zustand@5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 19.3.0 use-sync-external-store: 1.4.0(react@19.3.0) - zustand@5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)): + zustand@5.0.15(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 19.3.0 use-sync-external-store: 1.7.0(react@19.3.0) - zustand@5.0.3(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + zustand@5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - zustand@5.0.3(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@19.3.0)): + zustand@5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.4.0(react@19.3.0)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 18.3.1 use-sync-external-store: 1.4.0(react@19.3.0) optional: true - zustand@5.0.3(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)): + zustand@5.0.3(@types/react@19.3.0)(immer@10.1.1)(react@19.3.0)(use-sync-external-store@1.4.0(react@19.3.0)): optionalDependencies: '@types/react': 19.3.0 + immer: 10.1.1 react: 19.3.0 use-sync-external-store: 1.4.0(react@19.3.0) diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index d89e8b1d..85a83dff 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -58,6 +58,10 @@ export const EXTRA = { "@clip-wallet/link": { description: "Linked devices for Clip Wallet: phone or desktop as signer, end-to-end encrypted settings sync, handoffs and native messaging" }, "@clip-wallet/media-client": { description: "Decides what an untrusted NFT media URL may become; shared by the wallet UI and the media proxy" }, "@clip-wallet/kit-modules": { description: "Clip Wallet modules for ecosystem wallet pickers: NEAR Wallet Selector, Stellar Wallets Kit, Beacon, use-wallet" }, + "@clip-wallet/chains-tron": { + description: "TRON ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + keywords: ["clip-wallet", "wallet", "tron", "chain-module"], + }, "create-clip-wallet": { description: "Scaffold your own wallet on the Clip Wallet kit, on every platform from one clip.config.ts: browser extension, desktop app (Electron) and phone app (Expo), with its own name, ids and icons; testnet by default", @@ -79,6 +83,10 @@ const FAMILY = { stellar: "Stellar", tezos: "Tezos", algorand: "Algorand", + stacks: "Stacks", + bitcoincash: "Bitcoin Cash", + xrpl: "XRP Ledger", + antelope: "Antelope (Vaulta, Telos, XPR Network)", }; for (const [f, label] of Object.entries(FAMILY)) { EXTRA[`@clip-wallet/chains-${f}`] = { @@ -86,6 +94,10 @@ for (const [f, label] of Object.entries(FAMILY)) { keywords: ["clip-wallet", "wallet", f, "chain-module"], }; } +EXTRA["@clip-wallet/chains-fuel"] = { + description: "Fuel ChainModule for Clip Wallet: builds and decodes transactions in plain words, dry-runs contract calls; never touches keys", + keywords: ["clip-wallet", "wallet", "fuel", "chain-module"], +}; /** Third-party code a package ships, with the licence text its NOTICE must carry (read from the vendored LICENSE). */ const THIRD_PARTY = { @@ -108,6 +120,21 @@ const THIRD_PARTY = { const read = (file) => (existsSync(file) ? readFileSync(file, "utf8") : undefined); const rootLicense = readFileSync(join(root, "LICENSE"), "utf8"); +EXTRA["@clip-wallet/chains-multiversx"] = { + description: "MultiversX ChainModule for Clip Wallet: builds and decodes transactions in plain words; never touches keys", + keywords: ["clip-wallet", "wallet", "multiversx", "egld", "chain-module"], +}; +EXTRA["@clip-wallet/chains-icp"] = { + description: "Internet Computer (ICP) ChainModule for Clip Wallet: builds and decodes ledger transfers in plain words; never touches keys", + keywords: ["clip-wallet", "wallet", "icp", "internet-computer", "chain-module"], +}; + +EXTRA["@clip-wallet/chains-cosmos"] = { + description: + "Cosmos SDK ChainModule for Clip Wallet (Osmosis, dYdX, ZIGChain, Provenance, THORChain, Initia): builds and decodes transactions in plain words; never touches keys", + keywords: ["clip-wallet", "wallet", "cosmos", "osmosis", "dydx", "provenance", "thorchain", "initia", "chain-module"], +}; + let bad = 0; for (const { dir, path, pkg } of publishablePackages(root)) { const next = normalize(pkg, dir, EXTRA[pkg.name] ?? {});