From 182de2027bf545403cba91909ce700a7d86ce773 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:26:48 +0800 Subject: [PATCH 01/36] feat(vault,core): twelve networks87 families: derivation, addresses and canonical Antelope signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLPR networks Clip didn't cover yet need their own keys. New Family members (core, additive; config NETWORK_FAMILIES and 1Mask's wire list follow): cosmos (coin 118: Osmosis, dYdX, ZIGChain), provenance (505), thorchain (931), initia (60, ethsecp256k1), tron, xrpl, antelope (Vaulta, Telos, XPR Network), multiversx, icp, stacks, fuel and bitcoincash. - vault: each family at its ecosystem wallet's standard path and curve (Keplr/Leap per chain-registry slip44, TronLink, xrpl.js/Ledger Live, TokenPocket, xPortal, Plug/dfx, Leather/Xverse, Fuel Wallet, Electron Cash), address encodings in src/encodings87.ts (bech32, base58check, Ripple base58, PUB_K1, principal + account identifier, c32check, Fuel b256 with the fuels-ts checksum, CashAddr). - vault: Antelope accepts only canonical K1 signatures; signEcdsaCanonical retries with an RFC 6979 extra-entropy counter (as WharfKit bumps its personalisation), still deterministic. - test/families87.test.ts: "abandon … about" cross-checked against cosmjs, xrpl.js, TronWeb, WharfKit (verify + recover), MultiversX sdk-core, @dfinity/identity-secp256k1, @stacks/wallet-sdk, fuels-ts and libauth, plus the published cosmos1… and CashAddr spec vectors. - core: optional ChainModule.receiveAddress(ctx) for families whose address is spelled per network or lives on chain. - engine/extension-kit: ETAs and fixture curves for the new families; engine wiring skips a family without a module. Co-Authored-By: Claude Opus 5.5 --- packages/1mask/src/shared/protocol.ts | 1 + packages/config/src/index.ts | 12 + packages/config/test/config.test.ts | 6 +- packages/core/src/index.ts | 28 +- packages/engine/src/adapters.ts | 13 + packages/engine/src/wiring.ts | 7 +- .../src/background/mocks/mock-chains.ts | 12 + packages/extension-kit/src/background/real.ts | 13 + packages/vault/package.json | 16 +- packages/vault/src/address.ts | 41 + packages/vault/src/derive.ts | 37 + packages/vault/src/encodings87.ts | 246 +++ packages/vault/src/index.ts | 25 + packages/vault/src/sign.ts | 25 + packages/vault/src/vault.ts | 16 +- packages/vault/test/families87.test.ts | 275 +++ packages/vault/test/vault-phase2.test.ts | 4 +- packages/vault/test/vault.test.ts | 2 +- pnpm-lock.yaml | 1516 ++++++++++++++++- 19 files changed, 2274 insertions(+), 21 deletions(-) create mode 100644 packages/vault/src/encodings87.ts create mode 100644 packages/vault/test/families87.test.ts diff --git a/packages/1mask/src/shared/protocol.ts b/packages/1mask/src/shared/protocol.ts index e11f91b..e59d548 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/config/src/index.ts b/packages/config/src/index.ts index 9a2fc09..fcd950a 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 7885099..828b12a 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 8dc2a01..2cc6722 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/engine/src/adapters.ts b/packages/engine/src/adapters.ts index 41f0e99..becd840 100644 --- a/packages/engine/src/adapters.ts +++ b/packages/engine/src/adapters.ts @@ -215,6 +215,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/wiring.ts b/packages/engine/src/wiring.ts index 2f3adeb..702fbd4 100644 --- a/packages/engine/src/wiring.ts +++ b/packages/engine/src/wiring.ts @@ -56,7 +56,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, @@ -74,7 +74,10 @@ export function createEngineDependencies(o: EngineWiringOptions): Dependencies & }; // 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/extension-kit/src/background/mocks/mock-chains.ts b/packages/extension-kit/src/background/mocks/mock-chains.ts index 80b7fa1..7124b4c 100644 --- a/packages/extension-kit/src/background/mocks/mock-chains.ts +++ b/packages/extension-kit/src/background/mocks/mock-chains.ts @@ -129,6 +129,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 036f30a..da51509 100644 --- a/packages/extension-kit/src/background/real.ts +++ b/packages/extension-kit/src/background/real.ts @@ -233,6 +233,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/vault/package.json b/packages/vault/package.json index 6406f5e..562364d 100644 --- a/packages/vault/package.json +++ b/packages/vault/package.json @@ -69,25 +69,39 @@ "devDependencies": { "@algorandfoundation/xhd-wallet-api": "^1.0.3", "@aptos-labs/ts-sdk": "^7.3.0", + "@bitauth/libauth": "^3.0.0", "@clip-wallet/chains-bitcoin": "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 779857f..d3e9cf0 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 0c21e10..dff8915 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 0000000..3eb61d0 --- /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 d2a9879..49a0de0 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 04b5637..0ecb63d 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 7a746d6..57f482b 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 0000000..b07f8ee --- /dev/null +++ b/packages/vault/test/families87.test.ts @@ -0,0 +1,275 @@ +/** + * 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(); + }); +}); diff --git a/packages/vault/test/vault-phase2.test.ts b/packages/vault/test/vault-phase2.test.ts index 5921916..c3643f5 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 10db3d5..61be62e 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 9149c89..f6bda0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,7 +366,7 @@ importers: 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) '@hashgraph/hedera-wallet-connect': specifier: 2.1.3 - version: 2.1.3(a31850274c23b83333f7aa05d3b0718f) + version: 2.1.3(902de281d5a371348f6b5f944abdc922) '@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) @@ -586,7 +586,7 @@ importers: version: link:../../packages/ui '@hashgraph/hedera-wallet-connect': specifier: 2.1.3 - version: 2.1.3(d1a5b987f8d9e1b2cd9533a0ab02adfc) + version: 2.1.3(f7f7eff1cae7b83564107d7da257a70e) '@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) @@ -736,7 +736,7 @@ importers: 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) '@hashgraph/hedera-wallet-connect': specifier: 2.1.3 - version: 2.1.3(5dedda258f06b094f7e7ac4ac90bcdfc) + version: 2.1.3(4e08597bdbcd476a3dac274ca280fa40) '@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) @@ -2391,15 +2391,39 @@ 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-bitcoin': specifier: workspace:* version: link:../chains-bitcoin '@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 +2439,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 +2463,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 +2475,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.25.3)(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 +2487,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 +3351,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 +3801,21 @@ 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/math@0.39.0': + resolution: {integrity: sha512-FSLy/oDF+BtOP/J60RsjL5W4MCKiCfBjSoeV5xj6qg2g8N884Ue853iuWanjqGkQJwkXCp+JbeK8Mv9j6AaYHw==} + + '@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 +3863,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 +4060,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 +4096,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 +4132,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 +4168,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 +4204,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 +4240,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 +4276,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 +4312,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 +4348,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 +4384,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 +4420,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 +4456,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 +4492,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 +4528,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 +4564,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 +4600,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 +4636,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 +4666,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 +4702,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 +4732,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 +4768,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 +4828,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 +4864,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 +4900,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 +4936,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 +5246,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 +5716,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'} @@ -5906,6 +6213,19 @@ 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@16.0.0': + resolution: {integrity: sha512-qSsm85+3dfLrPY0uMBrLPcqfPNI/sCy7D4idF0QvvRbvauRIGS80N7tfFAolucQflK+rXgRpsR5fUyVmMhSpoQ==} + peerDependencies: + bignumber.js: ^9.0.1 + protobufjs: ^7.5.6 + + '@multiversx/sdk-transaction-decoder@1.0.2': + resolution: {integrity: sha512-j43QsKquu8N51WLmVlJ7dV2P3A1448R7/ktvl8r3i6wRMpfdtzDPNofTdHmMRT7DdQdvs4+RNgz8hVKL11Etsw==} + '@mysten/bcs@2.1.2': resolution: {integrity: sha512-fv6lyuZMTHN0xRlu4GYiV/UXUrdJDOHbXm1KfZYEU0sy/t+22yTrTVtWnAQv1Km7eyB3qHhwIrAG2aem4JMocw==} @@ -6170,9 +6490,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 +6546,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 +6667,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'} @@ -7672,6 +8008,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 +8026,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 +9474,30 @@ 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@7.6.0': + resolution: {integrity: sha512-VUW8WDmZP4wXUNWVvpFerV+7LQ0PqIn8YOs8AjSUMA7ZOXIRLxJHoKxjfiHzJqazy7KlyryPW4kSNBCr7tLqhQ==} + + '@stacks/encryption@7.6.0': + resolution: {integrity: sha512-1D2yazEcC5blc/xZkCPXQRbiAozXiVisLdxdFF3bVvNocUZ7yWkW7SOhhNve8RJ0Yf0XBkG6IbnBa3F1vyOyXA==} + + '@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@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==} @@ -9614,6 +9980,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==} @@ -10436,6 +10805,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 +10829,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 +11131,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 +11437,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 +11449,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 +11481,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==} @@ -11143,9 +11540,15 @@ packages: resolution: {integrity: sha512-ugLk9SR+2gvhpNA7kqQL1eVDKyBNo/rbr+df72nrZj4OOdZvQRHDVtyogPGXAU2MLQimLL7EbUdz95QGh6Th5Q==} engines: {node: '>=18.0.0'} + 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 +11590,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==} @@ -11317,6 +11724,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 +11942,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==} @@ -11598,6 +12013,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'} @@ -12147,6 +12566,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'} @@ -12334,6 +12756,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 +12771,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 +12864,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 +12981,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 +13073,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'} @@ -12991,6 +13431,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 +13499,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 +13583,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 +13664,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 +13692,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 +13716,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 +13751,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'} @@ -13731,6 +14211,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 +14273,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 +14566,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 +14604,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'} @@ -14324,6 +14818,9 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -14761,6 +15258,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 +15317,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 +15360,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==} @@ -15289,6 +15797,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 +15809,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'} @@ -15350,6 +15864,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} @@ -15503,6 +16021,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 +16208,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==} @@ -15801,6 +16326,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==} @@ -16341,6 +16869,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 +17006,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 +17022,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 +17135,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==} @@ -16734,6 +17291,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==} @@ -16946,6 +17506,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'} @@ -17200,6 +17764,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 +17827,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 +17862,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: @@ -17398,6 +17974,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 +18050,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==} @@ -17882,6 +18467,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'} @@ -18296,6 +18885,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 +18911,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==} @@ -18462,6 +19058,10 @@ 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==} @@ -18525,6 +19125,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 +19152,10 @@ 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@5.0.0: resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} engines: {node: '>=12.20.0'} @@ -20991,6 +21598,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 @@ -22129,6 +22738,33 @@ snapshots: dependencies: commander: 15.0.0 + '@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/math@0.39.0': {} + + '@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))(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 @@ -22287,6 +22923,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 +23268,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 +23286,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 +23304,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 +23322,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 +23340,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 +23358,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 +23376,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 +23394,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 +23412,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 +23430,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 +23448,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 +23466,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 +23484,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 +23502,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 +23520,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 +23538,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 +23556,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 +23571,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 +23589,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 +23604,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 +23622,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 +23652,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 +23670,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 +23688,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 +23706,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 +25008,182 @@ 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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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 +25221,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,7 +25314,7 @@ 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)': + '@hashgraph/hedera-wallet-connect@2.1.3(4e08597bdbcd476a3dac274ca280fa40)': 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) @@ -24399,9 +25322,9 @@ snapshots: '@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) + ethers: 6.17.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(902de281d5a371348f6b5f944abdc922)': 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) @@ -24409,9 +25332,9 @@ snapshots: '@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(f7f7eff1cae7b83564107d7da257a70e)': 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/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) @@ -24419,7 +25342,7 @@ snapshots: '@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) '@headlessui/react@2.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -24913,6 +25836,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 @@ -26308,6 +27240,38 @@ snapshots: uint8-varint: 3.0.0 uint8arrays: 6.1.1 + '@multiversx/sdk-bls-wasm@0.3.5': + optional: true + + '@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-transaction-decoder@1.0.2': + dependencies: + bech32: 2.0.0 + '@mysten/bcs@2.1.2': dependencies: '@mysten/utils': 0.4.1 @@ -26787,8 +27751,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 +27785,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 +27934,9 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@playwright/test@1.63.0': dependencies: playwright: 1.63.0 @@ -32153,6 +33128,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 +33164,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 +35175,88 @@ 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@7.6.0': {} + + '@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@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@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': @@ -34896,6 +35964,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 @@ -35417,6 +36489,14 @@ snapshots: optionalDependencies: vite: 8.3.2(@types/node@22.20.5)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) + '@vitest/mocker@4.1.11(vite@8.3.2(@types/node@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.3.2(@types/node@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) + '@vitest/mocker@4.1.11(vite@8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))': dependencies: '@vitest/spy': 4.1.11 @@ -39289,6 +40369,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 +40410,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 +40837,10 @@ snapshots: dependencies: tslib: 2.8.1 + async@2.6.4: + dependencies: + lodash: 4.18.1 + async@3.2.6: {} asynckit@0.4.0: {} @@ -40459,10 +41569,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 +41599,8 @@ snapshots: bignumber.js@9.1.1: {} + bignumber.js@9.1.2: {} + bignumber.js@9.3.1: {} binary-extensions@2.3.0: {} @@ -40568,6 +41684,10 @@ snapshots: transitivePeerDependencies: - typescript + blake2b-wasm@1.1.7: + dependencies: + nanoassert: 1.1.0 + blake2b-wasm@2.4.0: dependencies: b4a: 1.9.0 @@ -40575,6 +41695,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 +41732,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 @@ -40760,6 +41895,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 +41928,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 +42188,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 @@ -41102,6 +42251,8 @@ snapshots: colorette@2.0.20: {} + colors@1.0.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -41783,6 +42934,8 @@ snapshots: delayed-stream@1.0.0: {} + delimit-stream@0.1.0: {} + depd@1.1.2: {} depd@2.0.0: {} @@ -41968,6 +43121,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 +43144,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 +43316,8 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encode-utf8@1.0.3: {} encodeurl@1.0.2: {} @@ -42311,6 +43474,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 +43693,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: {} @@ -43286,6 +44503,8 @@ snapshots: fetch-nodeshim@0.4.10: {} + fflate@0.8.2: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -43386,6 +44605,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 +44713,42 @@ snapshots: fsevents@2.3.3: optional: true + 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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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 +44820,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 @@ -43605,6 +44874,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 +44921,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 +44958,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 @@ -44153,6 +45448,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)): @@ -44283,6 +45580,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 +46287,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 +46325,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 @@ -45220,6 +46533,8 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.clonedeep@4.5.0: {} + lodash.debounce@4.0.8: {} lodash.escaperegexp@4.1.2: {} @@ -46395,6 +47710,8 @@ snapshots: mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + mlly@1.8.2: dependencies: acorn: 8.18.0 @@ -46448,6 +47765,8 @@ snapshots: nan@2.29.0: optional: true + nanoassert@1.1.0: {} + nanoassert@2.0.0: {} nanoid@3.3.19: {} @@ -46503,6 +47822,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 @@ -47042,6 +48363,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 +48373,8 @@ snapshots: pako@2.2.0: {} + pako@3.0.2: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -47095,6 +48420,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 @@ -47313,6 +48643,14 @@ snapshots: style-value-types: 5.1.2 tslib: 2.4.0 + 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(7042b70557cb9848dbae83cba6f51b7c): 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)) @@ -47486,6 +48824,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: @@ -47621,6 +48961,8 @@ snapshots: radix3@1.1.2: {} + ramda@0.30.1: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -48745,6 +50087,8 @@ snapshots: readdirp@5.1.1: {} + readonly-date-esm@2.0.0: {} + real-require@0.1.0: {} real-require@0.2.0: {} @@ -48874,6 +50218,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 +50234,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 +50251,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,10 +50430,16 @@ 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: {} @@ -49290,6 +50672,8 @@ snapshots: dependencies: signal-polyfill: 0.2.2 + simple-cbor@0.4.1: {} + simple-plist@1.3.1: dependencies: bplist-creator: 0.1.0 @@ -49710,6 +51094,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 @@ -50002,6 +51392,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 +51441,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 +51472,23 @@ snapshots: trim-lines@3.0.1: {} + 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) @@ -50173,6 +51586,8 @@ snapshots: type-fest@2.19.0: {} + type-fest@4.34.1: {} + type-fest@4.41.0: {} type-fest@5.10.0: @@ -50231,6 +51646,8 @@ snapshots: ufo@1.6.4: {} + uglify-js@3.19.3: {} + uhyphen@0.2.0: {} uint8-varint@2.0.5: @@ -50654,6 +52071,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 @@ -50883,6 +52302,21 @@ snapshots: terser: 5.51.2 yaml: 2.9.1 + vite@8.3.2(@types/node@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.12 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.5 + esbuild: 0.25.3 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.51.2 + yaml: 2.9.1 + vite@8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 @@ -51090,6 +52524,36 @@ snapshots: transitivePeerDependencies: - msw + 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.25.3)(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@22.20.5)(esbuild@0.25.3)(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@22.20.5)(esbuild@0.25.3)(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': 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) + transitivePeerDependencies: + - msw + 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.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)): dependencies: '@vitest/expect': 4.1.11 @@ -51547,6 +53011,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 +53050,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: @@ -51823,6 +53295,23 @@ 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: {} xtend@4.0.2: {} @@ -51889,6 +53378,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,6 +53404,8 @@ snapshots: zod@4.6.5: {} + zone-file@2.0.0-beta.3: {} + zustand@5.0.0(@types/react@19.3.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): optionalDependencies: '@types/react': 19.3.0 From d3499ee27a5bd357b203d31cb7fc15c10688b171 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:26:48 +0800 Subject: [PATCH 02/36] chore(core): message sections for the networks87 chain modules (English + 11 languages) One section per new package in en/n87.ts and locales/n87/.ts, spread into the catalog and each locale, so the chain-module streams add their ids and translations without touching the same lines. Co-Authored-By: Claude Opus 5.5 --- packages/core/src/messages/en/index.ts | 5 +++- packages/core/src/messages/en/n87.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/ar.ts | 2 ++ packages/core/src/messages/locales/de.ts | 2 ++ packages/core/src/messages/locales/es.ts | 2 ++ packages/core/src/messages/locales/fr.ts | 2 ++ packages/core/src/messages/locales/hi.ts | 2 ++ packages/core/src/messages/locales/it.ts | 2 ++ packages/core/src/messages/locales/ja.ts | 2 ++ packages/core/src/messages/locales/ko.ts | 2 ++ packages/core/src/messages/locales/n87/ar.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/de.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/es.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/fr.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/hi.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/it.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/ja.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/ko.ts | 26 +++++++++++++++++++ .../core/src/messages/locales/n87/pt-BR.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/n87/tr.ts | 26 +++++++++++++++++++ .../core/src/messages/locales/n87/zh-Hans.ts | 26 +++++++++++++++++++ packages/core/src/messages/locales/pt-BR.ts | 2 ++ packages/core/src/messages/locales/tr.ts | 2 ++ packages/core/src/messages/locales/zh-Hans.ts | 2 ++ 24 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/messages/en/n87.ts create mode 100644 packages/core/src/messages/locales/n87/ar.ts create mode 100644 packages/core/src/messages/locales/n87/de.ts create mode 100644 packages/core/src/messages/locales/n87/es.ts create mode 100644 packages/core/src/messages/locales/n87/fr.ts create mode 100644 packages/core/src/messages/locales/n87/hi.ts create mode 100644 packages/core/src/messages/locales/n87/it.ts create mode 100644 packages/core/src/messages/locales/n87/ja.ts create mode 100644 packages/core/src/messages/locales/n87/ko.ts create mode 100644 packages/core/src/messages/locales/n87/pt-BR.ts create mode 100644 packages/core/src/messages/locales/n87/tr.ts create mode 100644 packages/core/src/messages/locales/n87/zh-Hans.ts diff --git a/packages/core/src/messages/en/index.ts b/packages/core/src/messages/en/index.ts index 301659a..dc4c7c0 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 0000000..899c430 --- /dev/null +++ b/packages/core/src/messages/en/n87.ts @@ -0,0 +1,26 @@ +/** + * 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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +} as const; diff --git a/packages/core/src/messages/locales/ar.ts b/packages/core/src/messages/locales/ar.ts index c5cbc78..feeabfd 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 ff5aa5f..1cbaed6 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 e26f51b..da66b21 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 f807e01..ca3d119 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 cbe8783..ca1265d 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 0fb9915..2152ccc 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 f162ca7..2434954 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 357caf5..a76cb8f 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 0000000..09064dd --- /dev/null +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..af073ba --- /dev/null +++ b/packages/core/src/messages/locales/n87/de.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..bf9c792 --- /dev/null +++ b/packages/core/src/messages/locales/n87/es.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..dc50ac6 --- /dev/null +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..48dd514 --- /dev/null +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..d862c6e --- /dev/null +++ b/packages/core/src/messages/locales/n87/it.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..5771db2 --- /dev/null +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..079b80e --- /dev/null +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..799d022 --- /dev/null +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..646fab2 --- /dev/null +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +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 0000000..b51d270 --- /dev/null +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -0,0 +1,26 @@ +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) + // ---- end chains-cosmos + // ---- chains-tron (tron) + // ---- end chains-tron + // ---- chains-xrpl (xrpl) + // ---- end chains-xrpl + // ---- chains-antelope (antelope) + // ---- end chains-antelope + // ---- chains-multiversx (multiversx) + // ---- end chains-multiversx + // ---- chains-icp (icp) + // ---- end chains-icp + // ---- chains-stacks (stacks) + // ---- end chains-stacks + // ---- chains-fuel (fuel) + // ---- end chains-fuel + // ---- chains-bitcoincash (bitcoincash) + // ---- end chains-bitcoincash + // ---- 1mask (networks87 providers) (1mask) + // ---- end 1mask (networks87 providers) +}; +export default messages; diff --git a/packages/core/src/messages/locales/pt-BR.ts b/packages/core/src/messages/locales/pt-BR.ts index 84e7248..cd4ecc4 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 c929745..4eb39b3 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 cd853d7..f4cda76 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; From 804d221da70f22467326bde14cd82604f722167b Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:26:48 +0800 Subject: [PATCH 03/36] feat(chains-evm): STRATO mainnet and Helium testnet STRATO runs SolidVM, but its nodes take signed legacy Ethereum transactions (EIP-155) through eth_sendRawTransaction: a value transfer moves the native USDST and a call's selector is matched to the SolidVM function (strato-platform techdocs transactions-and-fees.md and json-rpc.md; EthereumTX in blockapps-data Transaction.hs). So a self-custodial EVM key holds and sends USDST there. Checked live on 2026-10-06: eth_chainId 0x7030addddcf2 (mainnet) and 0xb165855668ca (Helium), eth_gasPrice 0x0, USDST decimals 18. - networks: both as legacy-gas specs with a new `flatFee` (the flat 0.01 USDST, or a voucher, the network charges per transaction outside gas, kept even when the transaction fails). - decode: the flat charge is added to the fee and shown as its own line (bg.label.networkCharge, bg.evm.flatFee in English and the 11 languages). Co-Authored-By: Claude Opus 5.5 --- packages/chains-evm/src/decode.ts | 9 +++++++++ packages/chains-evm/src/networks.ts | 20 ++++++++++++++++--- packages/chains-evm/test/decode.test.ts | 11 ++++++++++ packages/chains-evm/test/registry.test.ts | 18 +++++++++++++---- packages/core/src/messages/en/n87.ts | 4 ++++ packages/core/src/messages/locales/n87/ar.ts | 4 ++++ packages/core/src/messages/locales/n87/de.ts | 4 ++++ packages/core/src/messages/locales/n87/es.ts | 4 ++++ packages/core/src/messages/locales/n87/fr.ts | 4 ++++ packages/core/src/messages/locales/n87/hi.ts | 4 ++++ packages/core/src/messages/locales/n87/it.ts | 4 ++++ packages/core/src/messages/locales/n87/ja.ts | 4 ++++ packages/core/src/messages/locales/n87/ko.ts | 4 ++++ .../core/src/messages/locales/n87/pt-BR.ts | 4 ++++ packages/core/src/messages/locales/n87/tr.ts | 4 ++++ .../core/src/messages/locales/n87/zh-Hans.ts | 4 ++++ 16 files changed, 99 insertions(+), 7 deletions(-) diff --git a/packages/chains-evm/src/decode.ts b/packages/chains-evm/src/decode.ts index 784405a..3b1a9ec 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 ef8a555..653bd03 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" }; @@ -97,9 +107,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 111485c..e8a2615 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 2f06318..c6472e8 100644 --- a/packages/chains-evm/test/registry.test.ts +++ b/packages/chains-evm/test/registry.test.ts @@ -15,9 +15,10 @@ 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 60 CLPR EVM networks plus the Sepolia, Arc and STRATO testnets", () => { + expect(EVM_NETWORKS).toHaveLength(65); + expect(EVM_NETWORKS.filter((n) => !n.testnet)).toHaveLength(59); + 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", () => { @@ -29,7 +30,16 @@ describe("network registry", () => { 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/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index 899c430..aab8b80 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -23,4 +23,8 @@ export default { // ---- 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 } as const; diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index 09064dd..aedc0ec 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- end chains-bitcoincash // ---- 1mask (networks87 providers) (1mask) // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "رسم ثابت للشبكة", + "bg.evm.flatFee": "⁨{amount}⁩ لكل معاملة، حتى إن فشلت", + // ---- end chains-evm }; export default messages; diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index af073ba..83aaad2 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- 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 }; export default messages; diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index bf9c792..aa71381 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- 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 }; export default messages; diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index dc50ac6..8d04321 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- 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 }; export default messages; diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index 48dd514..5e3d629 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- end chains-bitcoincash // ---- 1mask (networks87 providers) (1mask) // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "नेटवर्क का तय शुल्क", + "bg.evm.flatFee": "हर लेन-देन पर {amount}, विफल होने पर भी", + // ---- end chains-evm }; export default messages; diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index d862c6e..bc0576b 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- 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 }; export default messages; diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index 5771db2..6101219 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- end chains-bitcoincash // ---- 1mask (networks87 providers) (1mask) // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "ネットワークの定額料金", + "bg.evm.flatFee": "1件の取引ごとに{amount}(失敗しても請求)", + // ---- end chains-evm }; export default messages; diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index 079b80e..84c78cd 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- end chains-bitcoincash // ---- 1mask (networks87 providers) (1mask) // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "네트워크 정액 요금", + "bg.evm.flatFee": "거래당 {amount}, 실패해도 부과됨", + // ---- end chains-evm }; 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 index 799d022..55f91b7 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- 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 }; export default messages; diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 646fab2..6662161 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- 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 }; 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 index b51d270..032dfeb 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -22,5 +22,9 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- end chains-bitcoincash // ---- 1mask (networks87 providers) (1mask) // ---- end 1mask (networks87 providers) + // ---- chains-evm + "bg.label.networkCharge": "网络定额收费", + "bg.evm.flatFee": "每笔交易 {amount},失败也收取", + // ---- end chains-evm }; export default messages; From 051bf72fa7a2450a18cbef4d7ed3d7ce7a797736 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:26:49 +0800 Subject: [PATCH 04/36] feat(engine,extension-kit,ui): receive on the address each network spells Receive used the vault's one address per family. The networks87 families spell it per network (Cosmos bech32 prefixes, Stacks SP/ST, CashAddr bitcoincash:/bchtest:) or keep it on chain (Antelope account names), so the extension background and the engine ask the module's optional receiveAddress(ctx) and group targets by what it returns; lazily loaded modules forward it. Family labels for the twelve new families (bare network names). Co-Authored-By: Claude Opus 5.5 --- packages/engine/src/engine.ts | 8 ++++++-- packages/engine/test/engine.test.ts | 13 +++++++++++++ packages/extension-kit/src/background/service.ts | 8 ++++++-- packages/extension-kit/src/background/wiring.ts | 4 ++++ packages/ui/src/platform/client.ts | 13 +++++++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/engine/src/engine.ts b/packages/engine/src/engine.ts index c6c9544..87e6601 100644 --- a/packages/engine/src/engine.ts +++ b/packages/engine/src/engine.ts @@ -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()]; } diff --git a/packages/engine/test/engine.test.ts b/packages/engine/test/engine.test.ts index 34a8a74..13d4330 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/extension-kit/src/background/service.ts b/packages/extension-kit/src/background/service.ts index 31ea12b..4029374 100644 --- a/packages/extension-kit/src/background/service.ts +++ b/packages/extension-kit/src/background/service.ts @@ -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()]; } diff --git a/packages/extension-kit/src/background/wiring.ts b/packages/extension-kit/src/background/wiring.ts index d36548e..b8824fc 100644 --- a/packages/extension-kit/src/background/wiring.ts +++ b/packages/extension-kit/src/background/wiring.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; + }, }; } diff --git a/packages/ui/src/platform/client.ts b/packages/ui/src/platform/client.ts index 676b1b7..201f37f 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. */ From c902d72594ff420a5e5219a4bbdbeed83103263a Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:26:49 +0800 Subject: [PATCH 05/36] feat(chains-evm): Arc mainnet; fixture chains know the networks87 families - Arc mainnet went live on 2026-09-16: chain 5042, USDC as native gas (18 decimals), rpc.mainnet.arc.io and explorer.arc.io per docs.arc.io "Connect to Arc" (eth_chainId 0x13b2 checked 2026-10-06). The testnet entry moves to the documented rpc.testnet.arc.io / explorer.testnet.arc.io (the old RPC host still answers and stays as fallback). - extension-kit fixture mode: address patterns, schemes and paths for the twelve new families (resolveRecipient asks every family's isAddress). Co-Authored-By: Claude Opus 5.5 --- packages/chains-evm/src/networks.ts | 6 ++- packages/chains-evm/test/registry.test.ts | 8 ++-- .../src/background/mocks/mock-chains.ts | 37 +++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/chains-evm/src/networks.ts b/packages/chains-evm/src/networks.ts index 653bd03..1b15c41 100644 --- a/packages/chains-evm/src/networks.ts +++ b/packages/chains-evm/src/networks.ts @@ -52,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 }, diff --git a/packages/chains-evm/test/registry.test.ts b/packages/chains-evm/test/registry.test.ts index c6472e8..dbcc787 100644 --- a/packages/chains-evm/test/registry.test.ts +++ b/packages/chains-evm/test/registry.test.ts @@ -15,15 +15,15 @@ describe("network registry", () => { } }); - it("covers 60 CLPR EVM networks plus the Sepolia, Arc and STRATO testnets", () => { - expect(EVM_NETWORKS).toHaveLength(65); - expect(EVM_NETWORKS.filter((n) => !n.testnet)).toHaveLength(59); + 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", () => { diff --git a/packages/extension-kit/src/background/mocks/mock-chains.ts b/packages/extension-kit/src/background/mocks/mock-chains.ts index 7124b4c..562a650 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 = { From e985ee9fc99db538370aa32ac93ea32329df3c68 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:26:50 +0800 Subject: [PATCH 06/36] docs(vault): derivation table for the networks87 families Co-Authored-By: Claude Opus 5.5 --- packages/vault/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/vault/README.md b/packages/vault/README.md index ee4ae88..b4aae63 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 From 36d008c9d38cd310fa9ec3eb7bcdda7baf03b4fa Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:00 +0800 Subject: [PATCH 07/36] feat(chains-fuel,1mask): Fuel module and the Fuel connector @clip-wallet/chains-fuel: Fuel Ignition (fuel:9889) and the testnet (fuel:0), ids from the connector standard's numeric chain ids (no CAIP-2 namespace exists), both GraphQL endpoints checked with chain { consensusParameters }. - m/44'/1179993420'/i'/0/0, secp256k1; address = SHA-256 of the uncompressed key with the fuels-ts checksum. - Script transactions serialized by hand from the specs (tx-format, transaction-id); bytes, ids, min/max gas and message hashes compared with fuels-ts (devDependency only) for transfers, token sends, contract calls with all policies, and message coins. - buildTransfer: coinsToSpend, return-zero script, gas measured by dry run, max fee from the spec formula at estimateGasPrice(10). A testnet transfer built and signed this way passed dryRun(utxoValidation) and one went through fuels-ts Fuel and the connector to submitAndAwaitStatus. - decode: plain transfers from inputs/outputs; contract-calling scripts dry-run, flows from CALL/TRANSFER/ TRANSFER_OUT receipts (simulated); predicates, non-script transactions and failed dry runs are blind; change to someone else or no change is a danger warning. Messages per fuels-ts hashMessage; unreadable plain strings are blind. - finalize: verify, check the recovery id recovers the key, r || s with the recovery bit in s, then submit. - Balances: ETH plus the bridged USDC/USDT/FUEL from Fuel's verified-assets list. 1Mask (new files, wiring left to router/index): a dependency-free FuelConnector that announces itself with the FuelConnector event under the wallet's identity and sits at window.clipwallet.fuel, and its background dispatcher; tested alone and under the real fuels-ts Fuel manager. Matrix page e2e/matrix/dapps/fuel.ts (testnet). Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix/dapps/fuel.ts | 72 ++ apps/extension/package.json | 1 + packages/1mask/src/background/fuel.ts | 108 +++ packages/1mask/src/inpage/fuel.ts | 326 +++++++++ packages/1mask/src/shared/fuel.ts | 40 ++ packages/1mask/test/fuel.test.ts | 208 ++++++ packages/chains-fuel/LICENSE | 202 ++++++ packages/chains-fuel/NOTICE | 8 + packages/chains-fuel/README.md | 125 ++++ packages/chains-fuel/package.json | 77 +++ packages/chains-fuel/src/address.ts | 56 ++ packages/chains-fuel/src/describe.ts | 202 ++++++ packages/chains-fuel/src/fee.ts | 57 ++ packages/chains-fuel/src/gql.ts | 230 +++++++ packages/chains-fuel/src/index.ts | 37 + packages/chains-fuel/src/message.ts | 41 ++ packages/chains-fuel/src/module.ts | 508 ++++++++++++++ packages/chains-fuel/src/networks.ts | 153 +++++ packages/chains-fuel/src/tx.ts | 507 ++++++++++++++ packages/chains-fuel/src/util.ts | 78 +++ packages/chains-fuel/test/connector.test.ts | 77 +++ packages/chains-fuel/test/fixtures.ts | 98 +++ packages/chains-fuel/test/fuel.test.ts | 318 +++++++++ packages/chains-fuel/test/helpers.ts | 48 ++ packages/chains-fuel/test/tx.test.ts | 224 ++++++ packages/chains-fuel/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 6 + packages/core/src/messages/locales/n87/ar.ts | 6 + packages/core/src/messages/locales/n87/de.ts | 6 + packages/core/src/messages/locales/n87/es.ts | 6 + packages/core/src/messages/locales/n87/fr.ts | 6 + packages/core/src/messages/locales/n87/hi.ts | 6 + packages/core/src/messages/locales/n87/it.ts | 6 + packages/core/src/messages/locales/n87/ja.ts | 6 + packages/core/src/messages/locales/n87/ko.ts | 6 + .../core/src/messages/locales/n87/pt-BR.ts | 6 + packages/core/src/messages/locales/n87/tr.ts | 6 + .../core/src/messages/locales/n87/zh-Hans.ts | 6 + pnpm-lock.yaml | 640 ++++++++++++++---- tools/release/normalize-manifests.mjs | 4 + 40 files changed, 4395 insertions(+), 123 deletions(-) create mode 100644 apps/extension/e2e/matrix/dapps/fuel.ts create mode 100644 packages/1mask/src/background/fuel.ts create mode 100644 packages/1mask/src/inpage/fuel.ts create mode 100644 packages/1mask/src/shared/fuel.ts create mode 100644 packages/1mask/test/fuel.test.ts create mode 100644 packages/chains-fuel/LICENSE create mode 100644 packages/chains-fuel/NOTICE create mode 100644 packages/chains-fuel/README.md create mode 100644 packages/chains-fuel/package.json create mode 100644 packages/chains-fuel/src/address.ts create mode 100644 packages/chains-fuel/src/describe.ts create mode 100644 packages/chains-fuel/src/fee.ts create mode 100644 packages/chains-fuel/src/gql.ts create mode 100644 packages/chains-fuel/src/index.ts create mode 100644 packages/chains-fuel/src/message.ts create mode 100644 packages/chains-fuel/src/module.ts create mode 100644 packages/chains-fuel/src/networks.ts create mode 100644 packages/chains-fuel/src/tx.ts create mode 100644 packages/chains-fuel/src/util.ts create mode 100644 packages/chains-fuel/test/connector.test.ts create mode 100644 packages/chains-fuel/test/fixtures.ts create mode 100644 packages/chains-fuel/test/fuel.test.ts create mode 100644 packages/chains-fuel/test/helpers.ts create mode 100644 packages/chains-fuel/test/tx.test.ts create mode 100644 packages/chains-fuel/tsconfig.json diff --git a/apps/extension/e2e/matrix/dapps/fuel.ts b/apps/extension/e2e/matrix/dapps/fuel.ts new file mode 100644 index 0000000..04443f7 --- /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/package.json b/apps/extension/package.json index c174be2..6210287 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -97,6 +97,7 @@ "cborg": "^6.1.3", "esbuild": "^0.28.2", "events": "3.3.0", + "fuels": "0.103.0", "starknet": "^10.8.0", "viem": "^2.57.1", "wagmi": "^3.7.7", diff --git a/packages/1mask/src/background/fuel.ts b/packages/1mask/src/background/fuel.ts new file mode 100644 index 0000000..53bc375 --- /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/inpage/fuel.ts b/packages/1mask/src/inpage/fuel.ts new file mode 100644 index 0000000..1afb561 --- /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/shared/fuel.ts b/packages/1mask/src/shared/fuel.ts new file mode 100644 index 0000000..17ad832 --- /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/test/fuel.test.ts b/packages/1mask/test/fuel.test.ts new file mode 100644 index 0000000..58a9bc3 --- /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/chains-fuel/LICENSE b/packages/chains-fuel/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..dafb938 --- /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 0000000..29ce604 --- /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 0000000..b69ddb1 --- /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 0000000..267dd98 --- /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 0000000..c6b7e57 --- /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 0000000..083c9c0 --- /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 0000000..0eb4d9a --- /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 0000000..05e2e68 --- /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 0000000..ab42cd3 --- /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 0000000..b86519e --- /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 0000000..b25fc20 --- /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 0000000..084d3a5 --- /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 0000000..11aa7fb --- /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 0000000..e5f55cb --- /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 0000000..164712d --- /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 0000000..d43ea58 --- /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 0000000..95ecf5e --- /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 0000000..ccfeb42 --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-fuel/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index aab8b80..33eb161 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -18,6 +18,12 @@ export default { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index aedc0ec..207a965 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index 83aaad2..29e9385 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index aa71381..d8ce770 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index 8d04321..ae3f7ef 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index 5e3d629..78035ed 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index bc0576b..01a23e0 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index 6101219..93c9e96 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index 84c78cd..e21b681 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 55f91b7..12beb28 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 6662161..7288842 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index 032dfeb..1737794 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -17,6 +17,12 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-stacks (stacks) // ---- 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) // ---- end chains-bitcoincash diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6bda0d..7ebac62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -701,6 +701,9 @@ 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))) starknet: specifier: ^10.8.0 version: 10.8.0(typescript@5.9.3)(zod@4.6.5) @@ -1129,6 +1132,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': @@ -2477,7 +2496,7 @@ importers: 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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + 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 @@ -25008,21 +25027,71 @@ 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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + '@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@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.25.3)(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@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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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@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.25.3)(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))) 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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + '@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@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.25.3)(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 commander: 13.1.0 glob: 10.4.5 @@ -25033,17 +25102,63 @@ snapshots: 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.25.3)(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)))': + 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@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.25.3)(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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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@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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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@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.25.3)(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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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 @@ -25056,37 +25171,107 @@ snapshots: - 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.25.3)(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)))': 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.25.3)(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/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.25.3)(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/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.25.3)(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)))': 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.25.3)(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.25.3)(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.25.3)(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@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.25.3)(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/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@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.25.3)(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.25.3)(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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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/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.25.3)(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)))': + 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@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.25.3)(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 @@ -25095,10 +25280,26 @@ snapshots: 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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)))': + '@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@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.25.3)(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.25.3)(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/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 @@ -25109,73 +25310,209 @@ snapshots: '@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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.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@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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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.25.3)(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)))': 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.25.3)(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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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.25.3)(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.25.3)(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/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.25.3)(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)))': 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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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/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.25.3)(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)))': 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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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/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.25.3)(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.25.3)(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.25.3)(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/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/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.25.3)(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)))': 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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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/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.25.3)(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.25.3)(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/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.25.3)(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)))': + 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@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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + 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: @@ -36489,14 +36826,6 @@ snapshots: optionalDependencies: vite: 8.3.2(@types/node@22.20.5)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) - '@vitest/mocker@4.1.11(vite@8.3.2(@types/node@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))': - dependencies: - '@vitest/spy': 4.1.11 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.3.2(@types/node@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) - '@vitest/mocker@4.1.11(vite@8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))': dependencies: '@vitest/spy': 4.1.11 @@ -44713,22 +45042,94 @@ snapshots: fsevents@2.3.3: optional: true - 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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))): + 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@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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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@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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1))) + '@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@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.25.3)(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.25.3)(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.25.3)(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.25.3)(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.25.3)(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/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) @@ -48643,6 +49044,14 @@ snapshots: style-value-types: 5.1.2 tslib: 2.4.0 + portfinder@1.0.32(supports-color@10.2.2): + dependencies: + 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 @@ -52302,21 +52711,6 @@ snapshots: terser: 5.51.2 yaml: 2.9.1 - vite@8.3.2(@types/node@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.2.12 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 22.20.5 - esbuild: 0.25.3 - fsevents: 2.3.3 - jiti: 2.7.0 - terser: 5.51.2 - yaml: 2.9.1 - vite@8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 @@ -52524,10 +52918,10 @@ snapshots: transitivePeerDependencies: - msw - 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.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.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.27.7)(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@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + '@vitest/mocker': 4.1.11(vite@8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(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 @@ -52544,7 +52938,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.2.0 - vite: 8.3.2(@types/node@22.20.5)(esbuild@0.25.3)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) + vite: 8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(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 @@ -52554,10 +52948,10 @@ snapshots: transitivePeerDependencies: - msw - 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.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.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.1)(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@22.20.5)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + '@vitest/mocker': 4.1.11(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(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 @@ -52574,7 +52968,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.2.0 - vite: 8.3.2(@types/node@22.20.5)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) + vite: 8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(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 @@ -52584,10 +52978,10 @@ snapshots: transitivePeerDependencies: - msw - 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.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.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: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + '@vitest/mocker': 4.1.11(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)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -52604,7 +52998,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.2.0 - vite: 8.3.2(@types/node@22.20.5)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) + 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) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -52614,10 +53008,10 @@ snapshots: transitivePeerDependencies: - msw - 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)): + vitest@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)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(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)) + '@vitest/mocker': 4.1.11(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)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -52634,20 +53028,20 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.2.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) + 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) why-is-node-running: 2.3.0 optionalDependencies: '@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) + '@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) transitivePeerDependencies: - msw - vitest@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)): + 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: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(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)) + '@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 @@ -52664,20 +53058,20 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.2.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) + 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': 24.19.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) 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@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)): + 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.27.7)(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/mocker': 4.1.11(vite@8.3.2(@types/node@26.6.4)(esbuild@0.27.7)(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 @@ -52694,20 +53088,20 @@ snapshots: 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) + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.27.7)(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@5.0.10) + 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 - 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.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)): + 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.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1)) + '@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 @@ -52724,7 +53118,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.2.0 - vite: 8.3.2(@types/node@26.6.4)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.1) + 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 diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index d89e8b1..0479f6e 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -86,6 +86,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 = { From 6f1e4897fdd7be6ecfacbdd9186d3438f20517a6 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:02 +0800 Subject: [PATCH 08/36] feat(chains-stacks): Stacks module and SIP-030 provider (WBIP-004 discovery) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacks mainnet (stacks:1) and testnet (stacks:2147483648) on Hiro's keyless API, at m/44'/5757'/0'/0/i (Leather, Xverse), addresses spelled per network (SP/ST, receiveAddress). - Hand-written SIP-005: c32check, Clarity values (hex and SIP-030 JSON), post-conditions (incl. SIP-040 originator mode and SIP-045 staking/PoX conditions), payloads, sighash chain (initial sighash = txid of the cleared tx; presign = SHA-512/256(sighash ‖ 0x04 ‖ fee ‖ nonce)), VRS signatures; bytes, txids and digests cross-checked against @stacks/transactions 7.6 and signed transactions against its verifyOrigin(). - Methods are SIP-030's: stx_transferStx, stx_transferSip10Ft, stx_transferSip9Nft, stx_callContract, stx_deployContract, stx_signTransaction, stx_signMessage ("\x17Stacks Signed Message:\n", RSV) and stx_signStructuredMessage (SIP-018). Post-conditions become lines and balance changes; allow mode is a danger. - Balances: STX, curated sBTC / USDCx / aeUSDC (bridged, own keys), other SIP-010 tokens spam-checked. - 1Mask: window.clipwallet.stacks (request → JSON-RPC response, SIP-030 error codes) registered on window.wbip_providers as "clipwallet.stacks" with the wallet's own identity (what @stacks/connect v8 lists and resolves); never window.StacksProvider/LeatherProvider/XverseProviders. createStacksDispatcher for the router. - Matrix page dapps/stacks.ts: @stacks/connect request() + verifyMessageSignatureRsv, testnet. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix/dapps/stacks.ts | 59 + apps/extension/package.json | 3 + packages/1mask/src/background/stacks.ts | 92 ++ packages/1mask/src/inpage/stacks.ts | 170 +++ packages/1mask/src/shared/stacks.ts | 115 ++ packages/1mask/test/stacks-provider.test.ts | 149 +++ packages/chains-stacks/LICENSE | 202 ++++ packages/chains-stacks/NOTICE | 8 + packages/chains-stacks/README.md | 115 ++ packages/chains-stacks/package.json | 79 ++ packages/chains-stacks/src/api.ts | 192 +++ packages/chains-stacks/src/c32.ts | 112 ++ packages/chains-stacks/src/clarity.ts | 340 ++++++ packages/chains-stacks/src/describe.ts | 204 ++++ packages/chains-stacks/src/index.ts | 37 + packages/chains-stacks/src/message.ts | 38 + packages/chains-stacks/src/module.ts | 658 ++++++++++ packages/chains-stacks/src/networks.ts | 123 ++ packages/chains-stacks/src/tx.ts | 420 +++++++ packages/chains-stacks/src/util.ts | 133 +++ packages/chains-stacks/test/helpers.ts | 67 ++ packages/chains-stacks/test/signatures.ts | 28 + packages/chains-stacks/test/stacks.test.ts | 380 ++++++ packages/chains-stacks/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 37 + packages/core/src/messages/locales/n87/ar.ts | 37 + packages/core/src/messages/locales/n87/de.ts | 37 + packages/core/src/messages/locales/n87/es.ts | 37 + packages/core/src/messages/locales/n87/fr.ts | 37 + packages/core/src/messages/locales/n87/hi.ts | 37 + packages/core/src/messages/locales/n87/it.ts | 37 + packages/core/src/messages/locales/n87/ja.ts | 37 + packages/core/src/messages/locales/n87/ko.ts | 37 + .../core/src/messages/locales/n87/pt-BR.ts | 37 + packages/core/src/messages/locales/n87/tr.ts | 37 + .../core/src/messages/locales/n87/zh-Hans.ts | 37 + pnpm-lock.yaml | 1057 ++++++++++++++++- tools/release/normalize-manifests.mjs | 1 + 38 files changed, 5222 insertions(+), 5 deletions(-) create mode 100644 apps/extension/e2e/matrix/dapps/stacks.ts create mode 100644 packages/1mask/src/background/stacks.ts create mode 100644 packages/1mask/src/inpage/stacks.ts create mode 100644 packages/1mask/src/shared/stacks.ts create mode 100644 packages/1mask/test/stacks-provider.test.ts create mode 100644 packages/chains-stacks/LICENSE create mode 100644 packages/chains-stacks/NOTICE create mode 100644 packages/chains-stacks/README.md create mode 100644 packages/chains-stacks/package.json create mode 100644 packages/chains-stacks/src/api.ts create mode 100644 packages/chains-stacks/src/c32.ts create mode 100644 packages/chains-stacks/src/clarity.ts create mode 100644 packages/chains-stacks/src/describe.ts create mode 100644 packages/chains-stacks/src/index.ts create mode 100644 packages/chains-stacks/src/message.ts create mode 100644 packages/chains-stacks/src/module.ts create mode 100644 packages/chains-stacks/src/networks.ts create mode 100644 packages/chains-stacks/src/tx.ts create mode 100644 packages/chains-stacks/src/util.ts create mode 100644 packages/chains-stacks/test/helpers.ts create mode 100644 packages/chains-stacks/test/signatures.ts create mode 100644 packages/chains-stacks/test/stacks.test.ts create mode 100644 packages/chains-stacks/tsconfig.json diff --git a/apps/extension/e2e/matrix/dapps/stacks.ts b/apps/extension/e2e/matrix/dapps/stacks.ts new file mode 100644 index 0000000..1863ba6 --- /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/package.json b/apps/extension/package.json index 6210287..ba2dbfa 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -78,6 +78,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", diff --git a/packages/1mask/src/background/stacks.ts b/packages/1mask/src/background/stacks.ts new file mode 100644 index 0000000..b677d92 --- /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/inpage/stacks.ts b/packages/1mask/src/inpage/stacks.ts new file mode 100644 index 0000000..1468775 --- /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/shared/stacks.ts b/packages/1mask/src/shared/stacks.ts new file mode 100644 index 0000000..c164d8f --- /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]{39,41}$/.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/test/stacks-provider.test.ts b/packages/1mask/test/stacks-provider.test.ts new file mode 100644 index 0000000..7e900a0 --- /dev/null +++ b/packages/1mask/test/stacks-provider.test.ts @@ -0,0 +1,149 @@ +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(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/chains-stacks/LICENSE b/packages/chains-stacks/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..a45ce80 --- /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 0000000..ce2a83a --- /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 0000000..1d86491 --- /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 0000000..be70860 --- /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 0000000..a02bf02 --- /dev/null +++ b/packages/chains-stacks/src/c32.ts @@ -0,0 +1,112 @@ +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(); + if (!/^S[0-9A-HJKMNP-TV-Z][0-9A-HJKMNP-TV-Z]{38,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 0000000..b965caa --- /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 0000000..844eca1 --- /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 0000000..e617344 --- /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 0000000..cdb5c8b --- /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 0000000..a9ae783 --- /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 0000000..3fa2b5b --- /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 0000000..900cf92 --- /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 0000000..e0fef34 --- /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 0000000..73a9e2c --- /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 0000000..0f56cf8 --- /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 0000000..c144985 --- /dev/null +++ b/packages/chains-stacks/test/stacks.test.ts @@ -0,0 +1,380 @@ +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); + 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 0000000..9d04942 --- /dev/null +++ b/packages/chains-stacks/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index 33eb161..8b81361 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -16,6 +16,43 @@ export default { // ---- chains-icp (icp) // ---- 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.", diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index 207a965..5320d2d 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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، ولا يعود إليك.", diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index 29e9385..255fec8 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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.", diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index d8ce770..cb5dae2 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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.", diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index ae3f7ef..4fafa46 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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.", diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index 78035ed..3115f18 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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} को जाएगा।", diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index 01a23e0..fedcdcf 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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.", diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index 93c9e96..cdafd26 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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}に送られます。", diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index e21b681..65bfe0f 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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}(으)로 가요.", diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 12beb28..586a9f8 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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ê.", diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 7288842..9813127 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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.", diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index 1737794..00f54b8 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -15,6 +15,43 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-icp (icp) // ---- 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},而不会退回给你。", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ebac62..deb6c75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -644,6 +644,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 @@ -1209,6 +1218,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': @@ -6497,6 +6525,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} @@ -7620,6 +7652,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==} @@ -7632,6 +7667,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==} @@ -7644,6 +7682,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==} @@ -7656,6 +7697,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==} @@ -7668,6 +7712,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==} @@ -7680,6 +7727,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==} @@ -7692,6 +7747,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: @@ -7712,6 +7775,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==} @@ -7724,6 +7790,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==} @@ -7851,11 +7920,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] @@ -7883,12 +7962,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] @@ -7937,12 +8028,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] @@ -7959,6 +8062,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] @@ -7974,6 +8082,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] @@ -9496,12 +9609,24 @@ packages: '@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==} @@ -9511,6 +9636,9 @@ packages: '@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==} @@ -9574,6 +9702,11 @@ 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 + '@stricahq/bip32ed25519@1.1.2': resolution: {integrity: sha512-YfjVRvJzTWodEbrl4TQlL9Isxg1mrZr9hoPxY75JpgFAS0l6MT8R6Lev21TbQtIJy5LfHMUoK25UICKXFmYvKQ==} @@ -10203,6 +10336,9 @@ packages: '@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==} @@ -10602,6 +10738,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'} @@ -10728,6 +10868,10 @@ 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==} @@ -10756,6 +10900,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==} @@ -10776,6 +10923,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==} @@ -10794,6 +10945,9 @@ 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==} @@ -11524,6 +11678,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'} @@ -11541,6 +11698,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'} @@ -11559,6 +11719,10 @@ 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==} @@ -11662,6 +11826,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} @@ -11688,6 +11855,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==} @@ -11695,6 +11866,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==} @@ -12765,6 +12939,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'} @@ -13131,6 +13309,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'} @@ -15744,6 +15925,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: @@ -17172,6 +17369,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'} @@ -18124,6 +18325,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==} @@ -18514,6 +18718,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'} @@ -18550,6 +18766,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: @@ -19001,6 +19233,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'} @@ -28076,6 +28320,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 @@ -30328,6 +30576,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 @@ -30471,6 +30741,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) @@ -30821,6 +31126,42 @@ snapshots: - utf-8-validate - zod + '@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.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' + - '@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-pay@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) @@ -31253,6 +31594,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 @@ -31269,6 +31614,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) @@ -31719,13 +32101,90 @@ snapshots: - valtio - 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)': + '@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.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.6.5) - '@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) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@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 - qrcode: 1.5.3 + 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) + '@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) + '@reown/appkit-wallet': 1.7.8(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' @@ -32077,6 +32536,81 @@ snapshots: - utf-8-validate - zod + '@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: + '@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' + - '@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-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: + '@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' + - '@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-utils@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) @@ -32573,6 +33107,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) @@ -32617,6 +33162,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) @@ -33298,9 +33890,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 @@ -33316,9 +33914,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 @@ -33343,9 +33947,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 @@ -33355,6 +33965,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 @@ -33364,6 +33977,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 @@ -35524,8 +36140,59 @@ snapshots: 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 @@ -35537,6 +36204,13 @@ snapshots: 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 @@ -35566,6 +36240,17 @@ snapshots: 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 @@ -35694,6 +36379,17 @@ 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 + '@stricahq/bip32ed25519@1.1.2': dependencies: blakejs: 1.2.1 @@ -36546,6 +37242,10 @@ snapshots: '@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 @@ -37532,6 +38232,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 @@ -38788,6 +39532,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) @@ -39293,6 +40073,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 @@ -39772,6 +40581,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 @@ -40278,6 +41127,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 @@ -41948,6 +42844,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 @@ -41982,6 +42889,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: {} @@ -42013,6 +42925,16 @@ 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 @@ -42127,6 +43049,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 @@ -42167,12 +43099,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 @@ -43433,6 +44371,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 @@ -44074,6 +45019,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 @@ -45865,6 +46816,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) @@ -48649,6 +49604,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 @@ -50855,6 +51841,18 @@ snapshots: 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 @@ -52105,6 +53103,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@5.26.5: {} + undici-types@6.19.8: {} undici-types@6.21.0: {} @@ -52516,6 +53516,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 @@ -52584,6 +53591,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 @@ -53502,6 +54544,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 diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index 0479f6e..c3b47d2 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -79,6 +79,7 @@ const FAMILY = { stellar: "Stellar", tezos: "Tezos", algorand: "Algorand", + stacks: "Stacks", }; for (const [f, label] of Object.entries(FAMILY)) { EXTRA[`@clip-wallet/chains-${f}`] = { From 2018bf69f0ddf83d1e7f4379f6620d1d8d0270e2 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:04 +0800 Subject: [PATCH 09/36] feat(chains-tron): TRON module, 1Mask TIP-1193/TIP-6963 provider and the Nile matrix page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TRON mainnet, Nile and Shasta as a ChainModule with hand-written protobuf (no TronWeb at runtime; @noble/curves, @noble/hashes, @scure/base only). - Networks: Reown's CAIP-2 ids tron:0xcd8690dc (Nile, default), tron:0x94a9059e (Shasta), tron:0x2b6653dc (mainnet); fromChainId also takes the ChainAgnostic draft's decimal form. Keyless TronGrid plus api.nileex.io, api.tronstack.io and tron-rpc.publicnode.com, with fallback. - Accounts: m/44'/195'/0'/0/i, base58check T… addresses (TronWeb known answers); signing is refused when the account's owner/active permission doesn't give this key the threshold (the public test account on Nile was taken over exactly that way). - Transactions: TransferContract and TRC-20 transfer built locally (ref block = latest, 10 min expiration, fee limit from the energy estimate); txID = sha256(raw_data), r||s||v with v = 27 + recovery, verified and recovered before broadcasthex, confirmed with gettransactioninfobyid. - decode trusts only the hex: txID and raw_data JSON must match it, the reference block must be on this network. TRX/TRC-10/TRC-20 transfers, transferFrom, approve (unlimited-approval warning), Stake 2.0 freeze/unfreeze/ withdraw/cancel, delegate/undelegate, votes, reward claims and AccountPermissionUpdate (account-takeover) in plain words; other calldata shows contract + selector, blind. The fee is the most TRX it can burn now: bandwidth, account opening (1.1 TRX), memo fee and energy, capped by the fee limit. - Messages: TIP-104 signMessageV2 (keccak of "\x19TRON Signed Message:\n" + length), checked against TronWeb's verifyMessageV2. - 1Mask: window.clipwallet.tron implements TIP-1193 with eth_requestAccounts (TIP-1102), eth_chainId and wallet_switchEthereumChain (TIP-3326), announced via TIP-6963 with the wallet's own identity, plus the TronWeb signing subset TIP-1193 delegates to (trx.sign, trx.signMessageV2). Background dispatcher createTronDispatcher (not wired yet). Never sets isTronLink, window.tronLink or window.tronWeb. - Matrix page dapps/tron.ts (Nile): TIP-6963 connect, signMessageV2 verified with Trx.verifyMessageV2, a 1-sun TronWeb-built send to the black-hole address (TRON refuses self-transfers). - i18n: 19 bg.tron.* ids in English and the 11 languages. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix/dapps/tron.ts | 74 +++ apps/extension/package.json | 1 + packages/1mask/src/background/tron.ts | 62 ++ packages/1mask/src/inpage/tron.ts | 314 ++++++++++ packages/1mask/src/shared/tron-methods.ts | 38 ++ packages/1mask/test/tron.test.ts | 210 +++++++ packages/chains-tron/LICENSE | 202 +++++++ packages/chains-tron/NOTICE | 8 + packages/chains-tron/README.md | 139 +++++ packages/chains-tron/package.json | 79 +++ packages/chains-tron/src/abi.ts | 81 +++ packages/chains-tron/src/address.ts | 70 +++ packages/chains-tron/src/describe.ts | 309 ++++++++++ packages/chains-tron/src/index.ts | 28 + packages/chains-tron/src/message.ts | 16 + packages/chains-tron/src/module.ts | 535 ++++++++++++++++++ packages/chains-tron/src/networks.ts | 129 +++++ packages/chains-tron/src/proto.ts | 134 +++++ packages/chains-tron/src/rpc.ts | 171 ++++++ packages/chains-tron/src/tx.ts | 416 ++++++++++++++ packages/chains-tron/src/util.ts | 85 +++ packages/chains-tron/test/fixtures.ts | 255 +++++++++ packages/chains-tron/test/helpers.ts | 81 +++ packages/chains-tron/test/signatures.ts | 22 + packages/chains-tron/test/tron.test.ts | 489 ++++++++++++++++ packages/chains-tron/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 19 + packages/core/src/messages/locales/n87/ar.ts | 19 + packages/core/src/messages/locales/n87/de.ts | 19 + packages/core/src/messages/locales/n87/es.ts | 19 + packages/core/src/messages/locales/n87/fr.ts | 19 + packages/core/src/messages/locales/n87/hi.ts | 19 + packages/core/src/messages/locales/n87/it.ts | 19 + packages/core/src/messages/locales/n87/ja.ts | 19 + packages/core/src/messages/locales/n87/ko.ts | 19 + .../core/src/messages/locales/n87/pt-BR.ts | 19 + packages/core/src/messages/locales/n87/tr.ts | 19 + .../core/src/messages/locales/n87/zh-Hans.ts | 19 + pnpm-lock.yaml | 70 +++ tools/release/normalize-manifests.mjs | 4 + 40 files changed, 4251 insertions(+) create mode 100644 apps/extension/e2e/matrix/dapps/tron.ts create mode 100644 packages/1mask/src/background/tron.ts create mode 100644 packages/1mask/src/inpage/tron.ts create mode 100644 packages/1mask/src/shared/tron-methods.ts create mode 100644 packages/1mask/test/tron.test.ts create mode 100644 packages/chains-tron/LICENSE create mode 100644 packages/chains-tron/NOTICE create mode 100644 packages/chains-tron/README.md create mode 100644 packages/chains-tron/package.json create mode 100644 packages/chains-tron/src/abi.ts create mode 100644 packages/chains-tron/src/address.ts create mode 100644 packages/chains-tron/src/describe.ts create mode 100644 packages/chains-tron/src/index.ts create mode 100644 packages/chains-tron/src/message.ts create mode 100644 packages/chains-tron/src/module.ts create mode 100644 packages/chains-tron/src/networks.ts create mode 100644 packages/chains-tron/src/proto.ts create mode 100644 packages/chains-tron/src/rpc.ts create mode 100644 packages/chains-tron/src/tx.ts create mode 100644 packages/chains-tron/src/util.ts create mode 100644 packages/chains-tron/test/fixtures.ts create mode 100644 packages/chains-tron/test/helpers.ts create mode 100644 packages/chains-tron/test/signatures.ts create mode 100644 packages/chains-tron/test/tron.test.ts create mode 100644 packages/chains-tron/tsconfig.json diff --git a/apps/extension/e2e/matrix/dapps/tron.ts b/apps/extension/e2e/matrix/dapps/tron.ts new file mode 100644 index 0000000..0a5d08c --- /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/package.json b/apps/extension/package.json index ba2dbfa..de60ee5 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -102,6 +102,7 @@ "events": "3.3.0", "fuels": "0.103.0", "starknet": "^10.8.0", + "tronweb": "^6.5.1", "viem": "^2.57.1", "wagmi": "^3.7.7", "wxt": "^0.21.4" diff --git a/packages/1mask/src/background/tron.ts b/packages/1mask/src/background/tron.ts new file mode 100644 index 0000000..06e80db --- /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/inpage/tron.ts b/packages/1mask/src/inpage/tron.ts new file mode 100644 index 0000000..f0c2a6f --- /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/shared/tron-methods.ts b/packages/1mask/src/shared/tron-methods.ts new file mode 100644 index 0000000..6b070ab --- /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/test/tron.test.ts b/packages/1mask/test/tron.test.ts new file mode 100644 index 0000000..ebb4855 --- /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/chains-tron/LICENSE b/packages/chains-tron/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..a052f54 --- /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 0000000..94213f4 --- /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 0000000..cb74296 --- /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 0000000..10b5bba --- /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 0000000..2f24bfa --- /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 0000000..11798d0 --- /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 0000000..bfff656 --- /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 0000000..a7bcb27 --- /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 0000000..8f72906 --- /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 0000000..25eab47 --- /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 0000000..b2be1f9 --- /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 0000000..1c2730b --- /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 0000000..eb294d4 --- /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 0000000..530c9df --- /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 0000000..3724adf --- /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 0000000..87aae88 --- /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 0000000..35db38e --- /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 0000000..4450d4e --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-tron/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index 8b81361..ec00399 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -6,6 +6,25 @@ export default { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index 5320d2d..a26e8a1 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index 255fec8..6693dc7 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index cb5dae2..ca83b7c 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index 4fafa46..7c95b41 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index 3115f18..736733c 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index fedcdcf..47d285a 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index cdafd26..535b64f 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index 65bfe0f..d8fd464 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 586a9f8..3f6c819 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 9813127..f2a3829 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index 00f54b8..859cec6 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -5,6 +5,25 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-cosmos (cosmos, provenance, thorchain, initia) // ---- 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) // ---- end chains-xrpl diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index deb6c75..f4036a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -716,6 +716,9 @@ 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.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) @@ -1354,6 +1357,25 @@ 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/config: dependencies: zod: @@ -42116,6 +42138,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)) @@ -45875,6 +45907,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) @@ -52879,6 +52915,40 @@ 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) diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index c3b47d2..7bc003f 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", From 2cc067a36b27092b0c8412ebf0981f7a8f9997a5 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:05 +0800 Subject: [PATCH 10/36] feat(chains-multiversx): MultiversX module (EGLD, ESDT, staking calls in plain words) @clip-wallet/chains-multiversx: devnet (mvx:D), testnet (mvx:T) and mainnet (mvx:1), the ChainAgnostic / WalletConnect `mvx` ids; public gateway + API endpoints, no key. Accounts at m/44'/508'/0'/0'/i' (xPortal, sdk-core), erd1 bech32. - Transactions serialized exactly as sdk-core's TransactionComputer signs them (field order, omitted empty fields, base64 data, keccak256 for the hash-signing option, guardian/relayer); messages as MessageComputer ("\x17Elrond Signed Message:\n" + length, keccak256). Tests check the bytes against @multiversx/sdk-core 16, and the devnet gateway accepted the signatures (simulate with checkSignature). - decode: EGLD sends and notes, ESDTTransfer / ESDTNFTTransfer / MultiESDTNFTTransfer, staking-provider delegate / unDelegate / claimRewards / withdraw / reDelegateRewards, SetGuardian / GuardAccount / ChangeOwnerAddress as account-takeover dangers, other contract calls named with an unknown-call caution, the rest blind; fee from the network config, relayed fees as sponsored, a gateway dry run (simulate) for single transactions. - WalletConnect methods mvx_signTransaction(s) / mvx_signMessage; buildTransfer sends EGLD or ESDTs with sdk-core's gas, confirms through process-status, and every gateway rejection comes back in plain words. - Balances: EGLD plus registered fungible ESDTs; USDC (USDC-c76f1f / devnet USDC-350c4e) as bridged "usdc.mvx", unregistered and look-alike tokens marked spam. - No injected provider: sdk-dapp has no wallet discovery a third-party extension can use under its own identity (README has the sources); WalletConnect is the route. - core: bg.multiversx.* messages in English and 11 languages. Co-Authored-By: Claude Opus 5.5 --- packages/chains-multiversx/LICENSE | 202 +++++++++ packages/chains-multiversx/NOTICE | 8 + packages/chains-multiversx/README.md | 211 ++++++++++ packages/chains-multiversx/package.json | 80 ++++ packages/chains-multiversx/src/api.ts | 198 +++++++++ packages/chains-multiversx/src/describe.ts | 223 ++++++++++ packages/chains-multiversx/src/index.ts | 10 + packages/chains-multiversx/src/module.ts | 391 ++++++++++++++++++ packages/chains-multiversx/src/networks.ts | 153 +++++++ packages/chains-multiversx/src/tx.ts | 210 ++++++++++ packages/chains-multiversx/src/util.ts | 131 ++++++ packages/chains-multiversx/test/helpers.ts | 79 ++++ .../chains-multiversx/test/multiversx.test.ts | 345 ++++++++++++++++ packages/chains-multiversx/test/signatures.ts | 21 + packages/chains-multiversx/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 12 + packages/core/src/messages/locales/n87/ar.ts | 12 + packages/core/src/messages/locales/n87/de.ts | 12 + packages/core/src/messages/locales/n87/es.ts | 12 + packages/core/src/messages/locales/n87/fr.ts | 12 + packages/core/src/messages/locales/n87/hi.ts | 12 + packages/core/src/messages/locales/n87/it.ts | 12 + packages/core/src/messages/locales/n87/ja.ts | 12 + packages/core/src/messages/locales/n87/ko.ts | 12 + .../core/src/messages/locales/n87/pt-BR.ts | 12 + packages/core/src/messages/locales/n87/tr.ts | 12 + .../core/src/messages/locales/n87/zh-Hans.ts | 12 + pnpm-lock.yaml | 44 ++ tools/release/normalize-manifests.mjs | 5 + 29 files changed, 2456 insertions(+) create mode 100644 packages/chains-multiversx/LICENSE create mode 100644 packages/chains-multiversx/NOTICE create mode 100644 packages/chains-multiversx/README.md create mode 100644 packages/chains-multiversx/package.json create mode 100644 packages/chains-multiversx/src/api.ts create mode 100644 packages/chains-multiversx/src/describe.ts create mode 100644 packages/chains-multiversx/src/index.ts create mode 100644 packages/chains-multiversx/src/module.ts create mode 100644 packages/chains-multiversx/src/networks.ts create mode 100644 packages/chains-multiversx/src/tx.ts create mode 100644 packages/chains-multiversx/src/util.ts create mode 100644 packages/chains-multiversx/test/helpers.ts create mode 100644 packages/chains-multiversx/test/multiversx.test.ts create mode 100644 packages/chains-multiversx/test/signatures.ts create mode 100644 packages/chains-multiversx/tsconfig.json diff --git a/packages/chains-multiversx/LICENSE b/packages/chains-multiversx/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..40a8b25 --- /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 0000000..0da15af --- /dev/null +++ b/packages/chains-multiversx/README.md @@ -0,0 +1,211 @@ +# @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: send and receive only (no injected provider) + +There's no documented way for a third-party browser-extension wallet to be discovered by MultiversX dapps under its own +identity without the dapp adding code for that wallet. 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 dapp-side code: `ICustomProvider` objects passed to `initApp({ customProviders })`. + - `initApp` also merges `window.multiversx.providers` (`src/methods/initApp/initApp.ts`). The README documents that as + something the dapp author sets ("add it to the window object"), and the template dapp overwrites `window.multiversx`. + There's no announce event or registry a wallet could use. We don't inject into it. +- **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 we don'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. +- **WalletConnect works across dapps without impersonation.** Namespace `mvx`, chains `mvx:1` / `mvx:D` / `mvx:T`, with + the methods above. + - This module decodes and signs those requests, so wiring it into the engine's WalletConnect host is enough. + - Stock sdk-dapp labels any WalletConnect wallet "xPortal App" in its unlock panel. The wallet's own WalletConnect + metadata still carries its own name. + - sdk-dapp always asks for `mvx_signLoginToken` too, so a WalletConnect login needs that method; it isn't + implemented yet. + +**Dapp matrix.** +- L0 (send and receive on devnet) works today. +- L1 (connect) and L2 (sign a message) would go through WalletConnect, using sdk-dapp's `verifyMessage` + (`MessageComputer.computeBytesForVerifying` + `UserVerifier`). There's no injected page. + +## 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 + +- No injected provider (see above). No login-token / native-auth signing yet. +- 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 0000000..114eece --- /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 0000000..3b1ad87 --- /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 0000000..b954d47 --- /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 0000000..bac1ee3 --- /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 0000000..1065038 --- /dev/null +++ b/packages/chains-multiversx/src/module.ts @@ -0,0 +1,391 @@ +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"); + } +} + +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 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 0000000..c888364 --- /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 0000000..73b768f --- /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 0000000..2ebae08 --- /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 0000000..486f4b1 --- /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 0000000..3fc3c0d --- /dev/null +++ b/packages/chains-multiversx/test/multiversx.test.ts @@ -0,0 +1,345 @@ +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("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 0000000..648b424 --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-multiversx/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index ec00399..f4ad48e 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -31,6 +31,18 @@ export default { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index a26e8a1..ed94341 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index 6693dc7..6b7ffb9 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index ca83b7c..54d2916 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index 7c95b41..d70a73d 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index 736733c..52dcc46 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index 47d285a..a414213 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index 535b64f..34a8a1b 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index d8fd464..3cc6afb 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 3f6c819..113196a 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index f2a3829..4e522ba 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index 859cec6..26c5f33 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -30,6 +30,18 @@ const messages: { readonly [K in keyof typeof N87]: string } = { // ---- chains-antelope (antelope) // ---- 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) // ---- end chains-icp diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4036a3..48da793 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1182,6 +1182,25 @@ 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-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': @@ -27846,6 +27865,31 @@ snapshots: '@multiversx/sdk-bls-wasm@0.3.5': optional: true + '@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 diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index 7bc003f..6f66ea3 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -117,6 +117,11 @@ 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"], +}; + let bad = 0; for (const { dir, path, pkg } of publishablePackages(root)) { const next = normalize(pkg, dir, EXTRA[pkg.name] ?? {}); From aee403f991654e7074f9f871d55f954f19f8983c Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:14 +0800 Subject: [PATCH 11/36] feat(chains-icp): Internet Computer module (ICP, ckBTC, ckUSDC, ckETH transfers over a hand-written IC agent) @clip-wallet/chains-icp: IC mainnet (icp:737ba355e855bd4b61279056603e0550, the proposed CAIP-2 id from the root key) and "icp:test", mainnet restricted to DFINITY's test ledgers (TESTICP with a no-login faucet, ckTESTBTC, ckSepoliaUSDC, ckSepoliaETH): there's no public IC test network for ledger transfers. Boundary node icp-api.io, no key. Accounts at m/44'/223'/0'/0/i (Plug, dfx), the self-authenticating principal. - A minimal IC HTTPS client: CBOR (incl. the indefinite maps boundary nodes answer with), representation-independent request ids, signed envelopes (DER key + 64-byte r||s over SHA-256("\x0Aic-request" || request id)), anonymous v3 queries, the synchronous v4 call with a pre-signed v3 read_state for 202s (two payloads, one approval), and the request status read out of the certificate. Certificates and query signatures are NOT verified (README and code say so); no reply is used for a security decision. - Candid for exactly the ledger types (Account, TransferArg, TransferResult and its errors, the ICP ledger's legacy transfer to an account identifier); decode re-encodes the arg and refuses anything that isn't byte-identical. - Recipients: principals, ICRC-1 account text (all TextualEncoding.md examples) or 64-hex account identifiers (ICP/TESTICP only, with a note that exchanges use them); fee read with icrc1_fee, created_at_time for ledger dedup, ledger errors in plain words. - Tests cross-check request ids, envelopes, certificate lookups, Candid bytes and principals against @dfinity/agent, candid, principal and identity-secp256k1 2.4.x. Live: a signed transfer from the unfunded test-vector account was accepted by icp-api.io and answered InsufficientFunds. - No injected provider: ICRC-94 (extension discovery) is still a working-group DRAFT; Plug's window.ic.plug is proprietary. README lists what the provider would use once ICRC-94 is final. - core: bg.icp.* messages in English and 11 languages. Co-Authored-By: Claude Opus 5.5 --- packages/chains-icp/LICENSE | 202 +++++++++ packages/chains-icp/NOTICE | 8 + packages/chains-icp/README.md | 209 +++++++++ packages/chains-icp/package.json | 83 ++++ packages/chains-icp/src/agent.ts | 195 ++++++++ packages/chains-icp/src/candid.ts | 377 ++++++++++++++++ packages/chains-icp/src/cbor.ts | 151 +++++++ packages/chains-icp/src/index.ts | 12 + packages/chains-icp/src/ledger.ts | 94 ++++ packages/chains-icp/src/module.ts | 415 ++++++++++++++++++ packages/chains-icp/src/networks.ts | 105 +++++ packages/chains-icp/src/principal.ts | 100 +++++ packages/chains-icp/src/util.ts | 80 ++++ packages/chains-icp/test/helpers.ts | 75 ++++ packages/chains-icp/test/icp.test.ts | 302 +++++++++++++ packages/chains-icp/test/signatures.ts | 29 ++ packages/chains-icp/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 2 + packages/core/src/messages/locales/n87/ar.ts | 2 + packages/core/src/messages/locales/n87/de.ts | 2 + packages/core/src/messages/locales/n87/es.ts | 2 + packages/core/src/messages/locales/n87/fr.ts | 2 + packages/core/src/messages/locales/n87/hi.ts | 2 + packages/core/src/messages/locales/n87/it.ts | 2 + packages/core/src/messages/locales/n87/ja.ts | 2 + packages/core/src/messages/locales/n87/ko.ts | 2 + .../core/src/messages/locales/n87/pt-BR.ts | 2 + packages/core/src/messages/locales/n87/tr.ts | 2 + .../core/src/messages/locales/n87/zh-Hans.ts | 2 + pnpm-lock.yaml | 28 ++ tools/release/normalize-manifests.mjs | 4 + 31 files changed, 2494 insertions(+) create mode 100644 packages/chains-icp/LICENSE create mode 100644 packages/chains-icp/NOTICE create mode 100644 packages/chains-icp/README.md create mode 100644 packages/chains-icp/package.json create mode 100644 packages/chains-icp/src/agent.ts create mode 100644 packages/chains-icp/src/candid.ts create mode 100644 packages/chains-icp/src/cbor.ts create mode 100644 packages/chains-icp/src/index.ts create mode 100644 packages/chains-icp/src/ledger.ts create mode 100644 packages/chains-icp/src/module.ts create mode 100644 packages/chains-icp/src/networks.ts create mode 100644 packages/chains-icp/src/principal.ts create mode 100644 packages/chains-icp/src/util.ts create mode 100644 packages/chains-icp/test/helpers.ts create mode 100644 packages/chains-icp/test/icp.test.ts create mode 100644 packages/chains-icp/test/signatures.ts create mode 100644 packages/chains-icp/tsconfig.json diff --git a/packages/chains-icp/LICENSE b/packages/chains-icp/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..1900fee --- /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 0000000..7ca1761 --- /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 0000000..74a879e --- /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 0000000..a8844ee --- /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 0000000..145258b --- /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 0000000..f18f529 --- /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 0000000..3b42c2a --- /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 0000000..9356bc4 --- /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 0000000..d567089 --- /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 0000000..bbf13e0 --- /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 0000000..33070e7 --- /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 0000000..fd5ffd0 --- /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 0000000..abbc6b9 --- /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 0000000..83b5ddb --- /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 0000000..ab78f8c --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-icp/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index f4ad48e..9ae4c44 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -45,6 +45,8 @@ export default { "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", diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index ed94341..f2d6bd2 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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": "شرط لاحق", diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index 6b7ffb9..964ef55 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index 54d2916..269493b 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index d70a73d..87fbaec 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index 52dcc46..d08616b 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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": "पोस्ट-कंडीशन", diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index a414213..0a6c4b4 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index 34a8a1b..10f188f 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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": "ポストコンディション", diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index 3cc6afb..5d85e83 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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": "사후 조건", diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 113196a..6911906 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 4e522ba..91357b3 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index 26c5f33..0e5b1ec 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -44,6 +44,8 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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": "后置条件", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48da793..bef893a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1182,6 +1182,34 @@ 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': diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index 6f66ea3..d7fa18b 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -121,6 +121,10 @@ 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"], +}; let bad = 0; for (const { dir, path, pkg } of publishablePackages(root)) { From a1140650972d4293057b0b1e1dd62f28986d3895 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:15 +0800 Subject: [PATCH 12/36] feat(chains-substrate): Chainflip State Chain and its Perseverance testnet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chainflip is a Substrate solo chain (ss58 2112 "cF…", FLIP 18) that the existing sr25519 account signs for, and lp.chainflip.io finds 1Mask's injectedWeb3 provider (it lists Object.keys(window.injectedWeb3)). Read live on 2026-10-06: genesis 0x8b8c140b… (mainnet, chainflip-node 20216) and 0x7a5d4db8… (Perseverance, 20302), both transactionVersion 13, 6-second blocks. - networks: Chainflip (mainnet-rpc.chainflip.io, rpc.chainflip.io) and Chainflip Perseverance (archive.perseverance.chainflip.io, perseverance.chainflip.xyz), keys flip / flip-testnet, scan.chainflip.io /extrinsics/ links. - balances: the runtime has no Balances pallet, so FLIP is read from Flip.Account (balance, bond included, which is what pays fees); getFlipAccount() splits it into bond, redeemable (balance - bond) and a pending redemption. - buildTransfer refuses with substrate/no-transfers: FLIP only enters by funding from Ethereum and leaves by Funding.redeem. - decode: plain words for Funding.redeem / rebalance and the permanent bind_redeem_address / bind_executor_address (danger), LiquidityProvider, Swapping deposit channels, LendingPools (Boost, lending) and AccountRoles.as_sub_account, with cross-chain addresses in each chain's own format. LP order calls stay Pallet.call(args) with a caution about the 1 FLIP order escrow. - signing: AuthorizeCall / WeightReclaim are empty and add nothing; signing bytes and the signed extrinsic match @polkadot/types 17 byte for byte, and a vault-signed register_lp_account was validated read-only by both networks (TaggedTransactionQueue → Payment). - fixed: two-byte SS58 prefixes (polkadot-api 0.21 reads 2112 as 20488) and a malformed sr25519 signature that made finalize throw instead of substrate/bad-signature. - messages: bg.chainflip.* in English and the 11 languages. Co-Authored-By: Claude Opus 5.5 --- packages/chains-substrate/README.md | 68 +++- packages/chains-substrate/src/chainflip.ts | 310 ++++++++++++++++++ packages/chains-substrate/src/defi.ts | 3 + packages/chains-substrate/src/describe.ts | 13 + packages/chains-substrate/src/index.ts | 1 + packages/chains-substrate/src/module.ts | 44 ++- packages/chains-substrate/src/networks.ts | 52 ++- packages/chains-substrate/src/util.ts | 16 + .../test/chainflip-fixtures.ts | 85 +++++ .../chains-substrate/test/chainflip.test.ts | 282 ++++++++++++++++ .../fixtures/chainflip-metadata-v15.scale.gz | Bin 0 -> 132897 bytes .../chains-substrate/test/substrate.test.ts | 2 +- packages/core/src/messages/en/n87.ts | 37 +++ packages/core/src/messages/locales/n87/ar.ts | 37 +++ packages/core/src/messages/locales/n87/de.ts | 37 +++ packages/core/src/messages/locales/n87/es.ts | 37 +++ packages/core/src/messages/locales/n87/fr.ts | 37 +++ packages/core/src/messages/locales/n87/hi.ts | 37 +++ packages/core/src/messages/locales/n87/it.ts | 37 +++ packages/core/src/messages/locales/n87/ja.ts | 37 +++ packages/core/src/messages/locales/n87/ko.ts | 37 +++ .../core/src/messages/locales/n87/pt-BR.ts | 37 +++ packages/core/src/messages/locales/n87/tr.ts | 37 +++ .../core/src/messages/locales/n87/zh-Hans.ts | 37 +++ 24 files changed, 1305 insertions(+), 15 deletions(-) create mode 100644 packages/chains-substrate/src/chainflip.ts create mode 100644 packages/chains-substrate/test/chainflip-fixtures.ts create mode 100644 packages/chains-substrate/test/chainflip.test.ts create mode 100644 packages/chains-substrate/test/fixtures/chainflip-metadata-v15.scale.gz diff --git a/packages/chains-substrate/README.md b/packages/chains-substrate/README.md index ba049d0..96cea55 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 0000000..53ddcae --- /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 6a27af2..5e52ad7 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 b68c313..952e6b3 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 2dab034..19dc1b7 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 ce78a0c..334a980 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 754d2e0..8245d1b 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 e4d5465..055354b 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 0000000..38b24c9 --- /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 0000000..dd74e4d --- /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 0000000000000000000000000000000000000000..d8590694fc3242adc92b74f9bae87f99cdbb210d GIT binary patch literal 132897 zcmV)cK&ZbTiwFP!000026YRYWXk=NnFuHfuNl*1yt?7F+-^+Y2^S(3J%ro7ux=i<^ z=S$~inxs3KNuKHMbfC@99XyPsV-XP>>-UVH7e*IIl1G~Lj7 z*At_xI2N?4oo*}in(lbB+Xy|UUhlX;Fn$UDm=%8%MrUg^A6hNexizkSx9aXTALK)Y z#k$2T9<^bN4bfYR4e?lW##!;2bJt?SeB?Q|Hr<^(&vI;34)f$0ixs&2LU;0C?I+Ky zvb=tCX53;U^39p?Rc0|}6-NN{n%{AUYn`2T=$G{PPo9C-v7T6gvAhj%72*G}fjk?; z7Ho*IBZF+1v0RajK#L)2VHcKy&*WFgtYtd#r0O4o_egK5cw#8=nn}<2N9?1ZJv(kL)j@R*9O}7`&=8E_n7qtAO`3s{5OvDilrc^&jn1_T8*`kd47 z>Q3kb1Rp(Q2=q_}1V`D(ic9oiv2PtRCCGt{(H~|b&vu+vz3o`+JBK{F9LN~`oowt` z|Bl;fIjx#&vEOq3jY9=Mk7dB{E>@}dx82s3->ADC6P$q@|IMMY<*Z>1$OhTiHMiY` zLyU;yc=>>;A|Ay&!vB%K-_%zGU4OIiMF@|H3AtrS|;> zK=c!f|KR?R7LI4Y@@`g{?W}vyb4tD(INa6C9M*vB2{xgcNYUW*z~Ra$vEd9z{%^LV zUd34C*@465+ZlrzP`xKwxdeGXeVv0p7&SEq4)=6P4QW91|F9+TIzdU_IAjXX7}S8O#AJi{hFc4Tt?(BE z{6~imksi>1=9I!wf=_<@$o;@kFD_XIE)WE6xYb=x!1d`Phs*`ChBY7?2mURaNIvkT zBZtht&EX74P6PjzuVOpyD~G&t8G{;7orzX1DV`MW&&u`McTciuSWh%ruY?^ZbT@Y_ z_JhLV?z_Z*1~e0Fe90vPx|EfETR2<`fjOE1$5}QbE@cilF!-Xd~q5;qUW^+naFaUbv9fy0WrG_*hnq+fIR52iW;+=;JQEEs7qH}Dn z((#jK$+zBhxDcg=G$5K{qxf2K=-<5SkS{{jPzD75%II;7GynEohs|yL9+rn`u|f%s z{O9|yLcO^0(`*7?8x;K5;NhO5vEd9z&NDVz9N*|TO}84*h5B$CK5@fA{L->2jF*Nx zZqw=9wz&LZcqp*q)+Oh*YX^SQwf)v9@(ZvMyH8CpZvAn^4Q=2DcB|W5cRRMfVN(Me zhQY_ryX$!LHP;)^b#x#8p1{UV%Qkjj^Xsm~2k{@%xx%XJzTLaFM5>Lg%D2>K)w^D} z<#*waEw^?%NcWN-e3bX<#+D!3bzt|Kw$s|NMK?GL{9x0&bm4^) z>TbI2O*lkhx_7-s!*2QX#`f@Y4{#9JHC(JZjbuC6;1p0p$8`c6F=)Fr8}NI1NTDRfQngTJn-sV;) zmL3*vquZj*Se_HRg($5GyQ39mZM281&-UDCKv{L$ZF-?ipO@#HF3?@ad%s(i^keZ+ zqUJ3p*qY`em8q!(_{EM6I($~Og*J;moDDZrd%uK`+~n@I(`+~7#|Rhg)fTW}T2!wkh%mqhxV*ZZ z+|G(Tl+sx=4D zvs2Yw)Fbjrvc-3dUgy%{ZOi@QHE|1%DU`2!EvK_{LUKO>Hi=VMI9L@}$7sYdTjYy^ zd8&tWyRHDP!`dqh5xz@ex6!0%joeny?YME2|5~9O_T-k++Qe>5oSS@OFyDH&A*LqrSY~UpFlVuT0r4t8gnt(Rw~q0Pek-JD1J0a| z=d?n*4J4(c2oxXS=ngy3#H%T60MtT2Ktzk**IfXVR#snJC+*;L7fc@ zu<62dyW;_Cg8f(xciL_+xNzp|Qz<9Jnc|G-wHhkFx|AGZ72yQ4c(LW*o#Ugx2QK+| z)HAkJQ7164-VQvnX@|4xJR;}-c4MQCo7`*N@o&Ql!ww{D`z_e6IB!B~**ZnputeQmYOzKw&al~jkE;DHL{ zW;Z~9?doM1ri$_hp-X>MYd8VP7}ZO#8_>gQr;3CocMu#mn=YKYTDX%0D$#fWK)wqE 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 literal 0 HcmV?d00001 diff --git a/packages/chains-substrate/test/substrate.test.ts b/packages/chains-substrate/test/substrate.test.ts index d99be07..d3644d9 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/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index 9ae4c44..098a858 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -103,4 +103,41 @@ export default { "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/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index f2d6bd2..933e490 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 964ef55..94c8402 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 269493b..6feeb6d 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 87fbaec..490f6b3 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index d08616b..a158292 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 0a6c4b4..ff83e88 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 10f188f..8f0b5d2 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 5d85e83..4dd3fed 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 6911906..7ee4be7 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 91357b3..8819c74 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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 index 0e5b1ec..d556acb 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -102,5 +102,42 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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; From b96d0e3f552b0471e9f435f9c7d4daa424d978dd Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:19 +0800 Subject: [PATCH 13/36] feat(chains-cosmos,1mask): Cosmos SDK module for Osmosis, dYdX, ZIGChain, Provenance, THORChain and Initia One package, four key families (cosmos 118, provenance 505, thorchain 931, initia 60 ethsecp256k1), each its own createCosmosModule({ family }) answering only its networks (cosmos:). Hand-written protobuf and Amino JSON (checked byte for byte against cosmjs-types / @cosmjs/amino), SIGN_MODE_DIRECT, REST with failover, simulate-based gas, per-chain fee models (Osmosis txfees base fee, Initia dynamicfee, Provenance flat fees, THORChain fixed native fee with /types.MsgSend), sync broadcast and polling. decode explains bank, IBC, staking, distribution, CosmWasm execute and authz/feegrant grants (danger), ADR-36; anything else is blind. 1Mask: a Keplr-compatible provider at window.clipwallet.cosmos (never window.keplr) with its background dispatcher (signer ownership per chain prefix) and tests; matrix dapp page for osmo-test-5 with cosmjs. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix/dapps/cosmos.ts | 60 ++ apps/extension/package.json | 4 + packages/1mask/src/background/cosmos.ts | 160 +++++ packages/1mask/src/inpage/cosmos.ts | 323 +++++++++ packages/1mask/src/shared/cosmos-bech32.ts | 80 +++ packages/1mask/src/shared/cosmos.ts | 151 +++++ packages/1mask/test/cosmos.test.ts | 233 +++++++ packages/chains-cosmos/LICENSE | 202 ++++++ packages/chains-cosmos/NOTICE | 8 + packages/chains-cosmos/README.md | 196 ++++++ packages/chains-cosmos/package.json | 87 +++ packages/chains-cosmos/src/address.ts | 49 ++ packages/chains-cosmos/src/amino.ts | 101 +++ packages/chains-cosmos/src/describe.ts | 184 ++++++ packages/chains-cosmos/src/index.ts | 22 + packages/chains-cosmos/src/module.ts | 613 ++++++++++++++++++ packages/chains-cosmos/src/msgs.ts | 270 ++++++++ packages/chains-cosmos/src/networks.ts | 375 +++++++++++ packages/chains-cosmos/src/proto.ts | 506 +++++++++++++++ packages/chains-cosmos/src/rest.ts | 223 +++++++ packages/chains-cosmos/src/util.ts | 94 +++ packages/chains-cosmos/test/cosmos.test.ts | 461 +++++++++++++ packages/chains-cosmos/test/helpers.ts | 105 +++ packages/chains-cosmos/test/proto.test.ts | 177 +++++ packages/chains-cosmos/test/scenarios.ts | 104 +++ packages/chains-cosmos/test/signatures.ts | 15 + packages/chains-cosmos/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 17 + packages/core/src/messages/locales/n87/ar.ts | 17 + packages/core/src/messages/locales/n87/de.ts | 17 + packages/core/src/messages/locales/n87/es.ts | 17 + packages/core/src/messages/locales/n87/fr.ts | 17 + packages/core/src/messages/locales/n87/hi.ts | 17 + packages/core/src/messages/locales/n87/it.ts | 17 + packages/core/src/messages/locales/n87/ja.ts | 17 + packages/core/src/messages/locales/n87/ko.ts | 17 + .../core/src/messages/locales/n87/pt-BR.ts | 17 + packages/core/src/messages/locales/n87/tr.ts | 17 + .../core/src/messages/locales/n87/zh-Hans.ts | 17 + pnpm-lock.yaml | 136 +++- tools/release/normalize-manifests.mjs | 6 + 41 files changed, 5149 insertions(+), 1 deletion(-) create mode 100644 apps/extension/e2e/matrix/dapps/cosmos.ts create mode 100644 packages/1mask/src/background/cosmos.ts create mode 100644 packages/1mask/src/inpage/cosmos.ts create mode 100644 packages/1mask/src/shared/cosmos-bech32.ts create mode 100644 packages/1mask/src/shared/cosmos.ts create mode 100644 packages/1mask/test/cosmos.test.ts create mode 100644 packages/chains-cosmos/LICENSE create mode 100644 packages/chains-cosmos/NOTICE create mode 100644 packages/chains-cosmos/README.md create mode 100644 packages/chains-cosmos/package.json create mode 100644 packages/chains-cosmos/src/address.ts create mode 100644 packages/chains-cosmos/src/amino.ts create mode 100644 packages/chains-cosmos/src/describe.ts create mode 100644 packages/chains-cosmos/src/index.ts create mode 100644 packages/chains-cosmos/src/module.ts create mode 100644 packages/chains-cosmos/src/msgs.ts create mode 100644 packages/chains-cosmos/src/networks.ts create mode 100644 packages/chains-cosmos/src/proto.ts create mode 100644 packages/chains-cosmos/src/rest.ts create mode 100644 packages/chains-cosmos/src/util.ts create mode 100644 packages/chains-cosmos/test/cosmos.test.ts create mode 100644 packages/chains-cosmos/test/helpers.ts create mode 100644 packages/chains-cosmos/test/proto.test.ts create mode 100644 packages/chains-cosmos/test/scenarios.ts create mode 100644 packages/chains-cosmos/test/signatures.ts create mode 100644 packages/chains-cosmos/tsconfig.json diff --git a/apps/extension/e2e/matrix/dapps/cosmos.ts b/apps/extension/e2e/matrix/dapps/cosmos.ts new file mode 100644 index 0000000..d5131a8 --- /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/package.json b/apps/extension/package.json index de60ee5..756d1d7 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -58,6 +58,10 @@ "@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", "@mysten/sui": "^2.34.0", diff --git a/packages/1mask/src/background/cosmos.ts b/packages/1mask/src/background/cosmos.ts new file mode 100644 index 0000000..0dbc124 --- /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/inpage/cosmos.ts b/packages/1mask/src/inpage/cosmos.ts new file mode 100644 index 0000000..c0da98e --- /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/shared/cosmos-bech32.ts b/packages/1mask/src/shared/cosmos-bech32.ts new file mode 100644 index 0000000..a159ee7 --- /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 0000000..c361faf --- /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/test/cosmos.test.ts b/packages/1mask/test/cosmos.test.ts new file mode 100644 index 0000000..e38d6e0 --- /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/chains-cosmos/LICENSE b/packages/chains-cosmos/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..72782df --- /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 0000000..58466b0 --- /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 0000000..1cd4542 --- /dev/null +++ b/packages/chains-cosmos/package.json @@ -0,0 +1,87 @@ +{ + "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/proto-signing": "^0.39.0", + "cosmjs-types": "^0.11.0", + "@cosmjs/crypto": "^0.39.0" + } +} diff --git a/packages/chains-cosmos/src/address.ts b/packages/chains-cosmos/src/address.ts new file mode 100644 index 0000000..b299595 --- /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 0000000..a19f56e --- /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 0000000..0e7efdb --- /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 0000000..3f40c96 --- /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 0000000..1ba6644 --- /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 0000000..1785293 --- /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 0000000..9daf9cc --- /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 0000000..5860778 --- /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 0000000..0cbc7ae --- /dev/null +++ b/packages/chains-cosmos/src/rest.ts @@ -0,0 +1,223 @@ +import { ClipError, type ChainContext } from "@clip-wallet/core"; +import { b64encode, isObj } from "./util.js"; + +/** + * 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((u) => u.replace(/\/+$/, "")); + 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 0000000..fd5c0d8 --- /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 0000000..9cfbebb --- /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 0000000..edbd0c7 --- /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 0000000..a4f9e7c --- /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 0000000..1bb8671 --- /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 0000000..970836a --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-cosmos/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index 098a858..36c8d4c 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -4,6 +4,23 @@ */ 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.", diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index 933e490..68b0995 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -3,6 +3,23 @@ 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 التوقيع عنه.", diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index 94c8402..d62fbe9 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -3,6 +3,23 @@ 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.", diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index 6feeb6d..b358e3b 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -3,6 +3,23 @@ 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.", diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index 490f6b3..edad75a 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -3,6 +3,23 @@ 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.", diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index a158292..f015c98 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -3,6 +3,23 @@ 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 इसके लिए साइन नहीं कर सकता।", diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index ff83e88..1475a9e 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -3,6 +3,23 @@ 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.", diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index 8f0b5d2..bcc34d1 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -3,6 +3,23 @@ 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 は署名できません。", diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index 4dd3fed..f4b9a2c 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -3,6 +3,23 @@ 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이 서명할 수 없어요.", diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 7ee4be7..0761336 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -3,6 +3,23 @@ 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.", diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 8819c74..2215155 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -3,6 +3,23 @@ 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.", diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index d556acb..da5a237 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -3,6 +3,23 @@ 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 无法为其签名。", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bef893a..3057a0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,6 +584,18 @@ 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(f7f7eff1cae7b83564107d7da257a70e) @@ -1128,6 +1140,34 @@ 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 + cosmjs-types: + specifier: ^0.11.0 + version: 0.11.0 + packages/chains-evm: dependencies: '@clip-wallet/core': @@ -3926,9 +3966,27 @@ packages: '@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==} @@ -12415,6 +12473,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'} @@ -17958,6 +18020,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==} @@ -19397,6 +19463,9 @@ packages: 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'} @@ -23093,8 +23162,65 @@ snapshots: 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))(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)': @@ -43796,6 +43922,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: @@ -46323,7 +46451,6 @@ snapshots: dependencies: define-properties: 1.2.1 gopd: 1.2.0 - optional: true globby@11.1.0: dependencies: @@ -52783,6 +52910,8 @@ snapshots: dependencies: react: 19.3.0 + symbol-observable@2.0.3: {} + symbol-tree@3.2.4: {} tabbable@5.3.3: {} @@ -54897,6 +55026,11 @@ snapshots: 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: {} diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index d7fa18b..4ad972a 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -126,6 +126,12 @@ EXTRA["@clip-wallet/chains-icp"] = { 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] ?? {}); From 29448347c4340aa9e164a79068983ac3fc58f009 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:19 +0800 Subject: [PATCH 14/36] feat(1mask,engine,extension-kit): wire the networks87 providers - inpage/n87.ts installs the new families' providers under the wallet's own identity: Keplr-compatible API at window[globalKey].cosmos (Osmosis, dYdX, ZIGChain, Provenance, THORChain, Initia; never window.keplr), TRON TIP-1193 announced with TIP-6963, Stacks SIP-030 on window.wbip_providers (WBIP-004), and a Fuel connector announced with the FuelConnector event. `providers.{cosmos,tron,stacks,fuel}` turn each off. - router: the Cosmos, TRON, Stacks and Fuel dispatchers; methods.ts allowlists them. Families without a verified injected standard (XRPL, Antelope, MultiversX, ICP, Bitcoin Cash) stay send and receive only. - Hosts: Keplr enable, TIP-1193 accounts, SIP-030 connect and FuelConnector connect are connect approvals; cosmos_sendTx / cosmos_verifyArbitrary are answered by chains-cosmos read() without an approval. - 1mask content test: "cosmos" is a real family now; the invalid family is "dogecoin". Co-Authored-By: Claude Opus 5.5 --- packages/1mask/src/background/index.ts | 13 ++++ packages/1mask/src/background/methods.ts | 17 ++++- packages/1mask/src/background/router.ts | 36 ++++++++++ packages/1mask/src/inpage/index.ts | 10 +++ packages/1mask/src/inpage/n87.ts | 67 +++++++++++++++++++ packages/1mask/src/shared/config.ts | 5 ++ packages/1mask/test/content.test.ts | 2 +- packages/engine/src/adapters.ts | 5 +- packages/engine/src/engine.ts | 8 ++- packages/extension-kit/src/background/real.ts | 5 +- .../extension-kit/src/background/service.ts | 10 ++- 11 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 packages/1mask/src/inpage/n87.ts diff --git a/packages/1mask/src/background/index.ts b/packages/1mask/src/background/index.ts index 8d82450..5c878c1 100644 --- a/packages/1mask/src/background/index.ts +++ b/packages/1mask/src/background/index.ts @@ -39,3 +39,16 @@ 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"; +/** Methods the host treats as a connect approval (Keplr enable, TIP-1193 accounts, SIP-030 addresses, FuelConnector connect). */ +export const N87_CONNECT_METHODS: readonly string[] = [..._COSMOS_CONNECT, ..._TRON_CONNECT, _STACKS.connect, ..._FUEL_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 301f9b5..1dfc0eb 100644 --- a/packages/1mask/src/background/methods.ts +++ b/packages/1mask/src/background/methods.ts @@ -2,6 +2,10 @@ 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 { cardanoSubstrateAllowlist } from "./cardano-substrate.js"; import { starknetTonAllowlist } from "./starknet-ton.js"; import { HEDERA_INJECTED_METHODS } from "../shared/hedera.js"; @@ -126,8 +130,19 @@ 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(); default: - // Phase 2 families register their injected methods here as their connectors land. + // Families without an injected provider (xrpl, antelope, multiversx, icp, bitcoincash): send and receive only. return new Set(); } } diff --git a/packages/1mask/src/background/router.ts b/packages/1mask/src/background/router.ts index c72ed42..8bfff8f 100644 --- a/packages/1mask/src/background/router.ts +++ b/packages/1mask/src/background/router.ts @@ -27,6 +27,10 @@ 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 { createFuelDispatcher } from "./fuel.js"; import { isCallsMethod, type CallsHost } from "../shared/calls.js"; /** Background side of a runtime port (chrome.runtime.Port satisfies it). */ @@ -584,6 +588,34 @@ 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 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 +646,10 @@ 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 === "hedera" && method === HEDERA_WC_PAIR) { const uri = (params as { uri?: unknown } | undefined)?.uri; if (!opts.walletConnectPair) throw rpcError.unsupportedMethod(method); diff --git a/packages/1mask/src/inpage/index.ts b/packages/1mask/src/inpage/index.ts index 49548ac..f6ec142 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 }, + ...(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/n87.ts b/packages/1mask/src/inpage/n87.ts new file mode 100644 index 0000000..37d9be5 --- /dev/null +++ b/packages/1mask/src/inpage/n87.ts @@ -0,0 +1,67 @@ +/** + * 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) and Fuel (a FuelConnector announced with the FuelConnector event). + * Families without a verifiable third-party wallet standard (MultiversX, ICP, Bitcoin Cash, Antelope) 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 type { InpageTransport } from "./transport.js"; + +export interface InstalledN87 { + cosmos?: ClipCosmosProvider; + tron?: ClipTronProvider; + stacks?: ClipStacksProvider; + fuel?: ClipFuelConnector; + stop(): void; +} + +export type N87ProviderFamily = "cosmos" | "tron" | "stacks" | "fuel"; + +/** 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, ...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); + } + return out; +} + +export * from "./cosmos.js"; +export * from "./tron.js"; +export * from "./stacks.js"; +export * from "./fuel.js"; diff --git a/packages/1mask/src/shared/config.ts b/packages/1mask/src/shared/config.ts index f427dff..5c4ab79 100644 --- a/packages/1mask/src/shared/config.ts +++ b/packages/1mask/src/shared/config.ts @@ -46,6 +46,11 @@ 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; cardano?: boolean; substrate?: boolean; starknet?: boolean; diff --git a/packages/1mask/test/content.test.ts b/packages/1mask/test/content.test.ts index 1b6cf3b..0ca300a 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/engine/src/adapters.ts b/packages/engine/src/adapters.ts index becd840..95b0a08 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 */ diff --git a/packages/engine/src/engine.ts b/packages/engine/src/engine.ts index 87e6601..faabcbd 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"; @@ -1159,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/extension-kit/src/background/real.ts b/packages/extension-kit/src/background/real.ts index da51509..80092fd 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 */ diff --git a/packages/extension-kit/src/background/service.ts b/packages/extension-kit/src/background/service.ts index 4029374..d064b52 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"; @@ -1286,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"); From 8a466810ba6d889b96d16389726e1ffa622fc092 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:27:49 +0800 Subject: [PATCH 15/36] wip(engine,extension-kit): catalogue and lazy modules for cosmos, tron, multiversx, icp, stacks, fuel Co-Authored-By: Claude Opus 5.5 --- apps/desktop/clip.config.ts | 2 +- apps/extension/clip.config.ts | 2 +- apps/mobile/clip.config.ts | 2 +- packages/engine/package.json | 6 ++++ packages/engine/src/catalog.ts | 26 ++++++++++++++ packages/engine/src/wiring.ts | 15 ++++++++ packages/extension-kit/package.json | 6 ++++ .../extension-kit/src/background/wiring.ts | 12 ++++++- packages/extension-kit/test/clip.config.ts | 2 +- pnpm-lock.yaml | 36 +++++++++++++++++++ 10 files changed, 104 insertions(+), 5 deletions(-) diff --git a/apps/desktop/clip.config.ts b/apps/desktop/clip.config.ts index d9da217..60946c4 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/extension/clip.config.ts b/apps/extension/clip.config.ts index 95cce2c..efebfab 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/mobile/clip.config.ts b/apps/mobile/clip.config.ts index ff3bd72..896bc14 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/packages/engine/package.json b/packages/engine/package.json index 7bb5eae..9884101 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -171,16 +171,22 @@ "@clip-wallet/chains-aptos": "workspace:*", "@clip-wallet/chains-bitcoin": "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/config": "workspace:*", "@clip-wallet/core": "workspace:*", "@clip-wallet/features": "workspace:*", diff --git a/packages/engine/src/catalog.ts b/packages/engine/src/catalog.ts index 9420588..458f6df 100644 --- a/packages/engine/src/catalog.ts +++ b/packages/engine/src/catalog.ts @@ -23,6 +23,13 @@ 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 { 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 +62,12 @@ export function walletNetworks(config: Pick) ...STELLAR_NETWORKS, ...TEZOS_NETWORKS, ...ALGORAND_NETWORKS, + ...COSMOS_NETWORKS, + ...TRON_NETWORKS, + ...MULTIVERSX_NETWORKS, + ...ICP_NETWORKS, + ...STACKS_NETWORKS, + ...FUEL_NETWORKS, ]; return all.filter((n) => (families as readonly string[]).includes(n.family) && (mainnet || n.testnet)); } @@ -121,6 +134,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/wiring.ts b/packages/engine/src/wiring.ts index 702fbd4..99e04a5 100644 --- a/packages/engine/src/wiring.ts +++ b/packages/engine/src/wiring.ts @@ -16,6 +16,12 @@ 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 { createStarknetModule } from "@clip-wallet/chains-starknet"; import { createTonModule } from "@clip-wallet/chains-ton"; import { createNearModule } from "@clip-wallet/chains-near"; @@ -71,6 +77,15 @@ 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, }; // One instance per enabled family for the engine's lifetime (modules keep prepare→finalize state). const chains: Partial> = {}; diff --git a/packages/extension-kit/package.json b/packages/extension-kit/package.json index e7e8062..6551da7 100644 --- a/packages/extension-kit/package.json +++ b/packages/extension-kit/package.json @@ -138,16 +138,22 @@ "@clip-wallet/chains-aptos": "workspace:*", "@clip-wallet/chains-bitcoin": "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/config": "workspace:*", "@clip-wallet/core": "workspace:*", "@clip-wallet/engine": "workspace:*", diff --git a/packages/extension-kit/src/background/wiring.ts b/packages/extension-kit/src/background/wiring.ts index b8824fc..bba2c34 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 @@ -344,6 +344,16 @@ 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())), }; const prices = createPriceFeed(opts.kv, opts.features?.coingeckoDemoKey); const eager = { evm: createEvmModule(), hedera, solana: createSolanaModule(), bitcoin: createBitcoinModule() }; diff --git a/packages/extension-kit/test/clip.config.ts b/packages/extension-kit/test/clip.config.ts index ea579f7..6af478f 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/pnpm-lock.yaml b/pnpm-lock.yaml index 3057a0b..bbd01fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1666,18 +1666,33 @@ importers: '@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 @@ -1696,6 +1711,9 @@ importers: '@clip-wallet/chains-ton': specifier: workspace:* version: link:../chains-ton + '@clip-wallet/chains-tron': + specifier: workspace:* + version: link:../chains-tron '@clip-wallet/config': specifier: workspace:* version: link:../config @@ -1763,18 +1781,33 @@ importers: '@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 @@ -1793,6 +1826,9 @@ importers: '@clip-wallet/chains-ton': specifier: workspace:* version: link:../chains-ton + '@clip-wallet/chains-tron': + specifier: workspace:* + version: link:../chains-tron '@clip-wallet/config': specifier: workspace:* version: link:../config From 26354d96870466746bc339ff2a6449c1f4e05113 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:28:27 +0800 Subject: [PATCH 16/36] feat(chains-bitcoincash): Bitcoin Cash module over Fulcrum, wc2-bch-bcr requests Bitcoin Cash mainnet, chipnet (default test network) and testnet4 at m/44'/145'/0'/0/i (Electron Cash, Paytaca), CashAddr spelled per network (bitcoincash:/bchtest:, receiveAddress). Ids are bip122 + the first own block (mainnet 478559 as in the ChainAgnostic test case, chipnet 115252, testnet4 genesis), so none collides with Bitcoin. - Public Fulcrum servers over WebSocket (Electrum Cash 1.4), several per network with fallback. - Hand-written CashAddr (P2PKH, P2SH20/32, token-aware), transactions with CashTokens prefixes and the BCH signing serialization (SIGHASH_ALL|FORKID, spent token prefix); bytes and sighashes checked against libauth and every signed transaction run through libauth's BCH 2023 VM. - Sends: largest coins first, never a coin that carries CashTokens, dust 546, relay-fee rate, change back to the same address. Balances: BCH confirmed + unconfirmed, fungible CashTokens from listunspent. - Requests use the community WalletConnect spec (bch_signTransaction with libauth-stringified source outputs, bch_signMessage in Electron Cash format); CashScript placeholders are refused in plain words. - 1Mask: wc2-bch-bcr namespace, chain mapping and createBitcoinCashDispatcher (bch_getAddresses per network); there's no injected BCH standard, so the WalletConnect session layer is the integration step. Co-Authored-By: Claude Opus 5.5 --- packages/1mask/src/background/bitcoincash.ts | 46 ++ packages/1mask/src/shared/bitcoincash.ts | 83 +++ packages/1mask/test/bitcoincash-wc.test.ts | 46 ++ packages/chains-bitcoincash/LICENSE | 202 ++++++++ packages/chains-bitcoincash/NOTICE | 8 + packages/chains-bitcoincash/README.md | 100 ++++ packages/chains-bitcoincash/package.json | 78 +++ packages/chains-bitcoincash/src/cashaddr.ts | 140 ++++++ packages/chains-bitcoincash/src/electrum.ts | 173 +++++++ packages/chains-bitcoincash/src/index.ts | 12 + packages/chains-bitcoincash/src/message.ts | 23 + packages/chains-bitcoincash/src/module.ts | 476 ++++++++++++++++++ packages/chains-bitcoincash/src/networks.ts | 89 ++++ packages/chains-bitcoincash/src/tx.ts | 198 ++++++++ packages/chains-bitcoincash/src/util.ts | 152 ++++++ packages/chains-bitcoincash/src/wc.ts | 130 +++++ .../test/bitcoincash.test.ts | 338 +++++++++++++ packages/chains-bitcoincash/test/helpers.ts | 65 +++ .../chains-bitcoincash/test/signatures.ts | 24 + packages/chains-bitcoincash/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 11 + packages/core/src/messages/locales/n87/ar.ts | 11 + packages/core/src/messages/locales/n87/de.ts | 11 + packages/core/src/messages/locales/n87/es.ts | 11 + packages/core/src/messages/locales/n87/fr.ts | 11 + packages/core/src/messages/locales/n87/hi.ts | 11 + packages/core/src/messages/locales/n87/it.ts | 11 + packages/core/src/messages/locales/n87/ja.ts | 11 + packages/core/src/messages/locales/n87/ko.ts | 11 + .../core/src/messages/locales/n87/pt-BR.ts | 11 + packages/core/src/messages/locales/n87/tr.ts | 11 + .../core/src/messages/locales/n87/zh-Hans.ts | 11 + pnpm-lock.yaml | 16 + tools/release/normalize-manifests.mjs | 1 + 34 files changed, 2533 insertions(+) create mode 100644 packages/1mask/src/background/bitcoincash.ts create mode 100644 packages/1mask/src/shared/bitcoincash.ts create mode 100644 packages/1mask/test/bitcoincash-wc.test.ts create mode 100644 packages/chains-bitcoincash/LICENSE create mode 100644 packages/chains-bitcoincash/NOTICE create mode 100644 packages/chains-bitcoincash/README.md create mode 100644 packages/chains-bitcoincash/package.json create mode 100644 packages/chains-bitcoincash/src/cashaddr.ts create mode 100644 packages/chains-bitcoincash/src/electrum.ts create mode 100644 packages/chains-bitcoincash/src/index.ts create mode 100644 packages/chains-bitcoincash/src/message.ts create mode 100644 packages/chains-bitcoincash/src/module.ts create mode 100644 packages/chains-bitcoincash/src/networks.ts create mode 100644 packages/chains-bitcoincash/src/tx.ts create mode 100644 packages/chains-bitcoincash/src/util.ts create mode 100644 packages/chains-bitcoincash/src/wc.ts create mode 100644 packages/chains-bitcoincash/test/bitcoincash.test.ts create mode 100644 packages/chains-bitcoincash/test/helpers.ts create mode 100644 packages/chains-bitcoincash/test/signatures.ts create mode 100644 packages/chains-bitcoincash/tsconfig.json diff --git a/packages/1mask/src/background/bitcoincash.ts b/packages/1mask/src/background/bitcoincash.ts new file mode 100644 index 0000000..4d5c22f --- /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` (integration + * step). This dispatcher 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/shared/bitcoincash.ts b/packages/1mask/src/shared/bitcoincash.ts new file mode 100644 index 0000000..30d13c8 --- /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/test/bitcoincash-wc.test.ts b/packages/1mask/test/bitcoincash-wc.test.ts new file mode 100644 index 0000000..b2bd74f --- /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/chains-bitcoincash/LICENSE b/packages/chains-bitcoincash/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..1a5e832 --- /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 0000000..1f91e5f --- /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. `@clip-wallet/1mask` has the pieces: `shared/bitcoincash.ts` (namespace, methods, chain +mapping, CashAddr re-spelling) and `background/bitcoincash.ts` (`createBitcoinCashDispatcher`); the WalletConnect +session layer still has to accept the `bch` namespace and map its chains (integration step). Until then the family +is send/receive in the wallet itself. + +## 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 0000000..ef4bd6a --- /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 0000000..fabe3c0 --- /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 0000000..a209af0 --- /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 0000000..e7f0498 --- /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 0000000..20d6130 --- /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 0000000..e2e1d65 --- /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 0000000..b3cc2a1 --- /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 0000000..14af8f3 --- /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 0000000..8dfc161 --- /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 0000000..41cbc04 --- /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 0000000..57cbdb0 --- /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 0000000..d5a3e15 --- /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 0000000..0867344 --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-bitcoincash/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index 36c8d4c..7f93dbf 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -113,6 +113,17 @@ export default { "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) diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index 68b0995..ece218f 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index d62fbe9..4e9a5a6 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index b358e3b..0d11d88 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index edad75a..c06b903 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index f015c98..e163716 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index 1475a9e..a1bc743 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index bcc34d1..ef153c5 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index f4b9a2c..2326cd4 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 0761336..bb9ceb5 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 2215155..28fc014 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index da5a237..6ca33e9 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -112,6 +112,17 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbd01fd..2445232 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1125,6 +1125,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': diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index 4ad972a..5661b53 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -84,6 +84,7 @@ const FAMILY = { tezos: "Tezos", algorand: "Algorand", stacks: "Stacks", + bitcoincash: "Bitcoin Cash", }; for (const [f, label] of Object.entries(FAMILY)) { EXTRA[`@clip-wallet/chains-${f}`] = { From a7fdc408ef7d6d907b7376e7cbdf689240575ad0 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:28:27 +0800 Subject: [PATCH 17/36] fix(chains-stacks): accept c32 addresses whose hash starts with zero bytes Each leading zero byte is a single "0", so such addresses are shorter than the usual 41 characters: the boot addresses SP000000000000000000002Q6VF78 / ST000000000000000000002AMW42H were refused (found with a live read-only decode against api.testnet.hiro.so). The checksum and canonical-spelling checks still decide. Co-Authored-By: Claude Opus 5.5 --- packages/1mask/src/shared/stacks.ts | 2 +- packages/1mask/test/stacks-provider.test.ts | 1 + packages/chains-stacks/src/c32.ts | 3 ++- packages/chains-stacks/test/stacks.test.ts | 4 ++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/1mask/src/shared/stacks.ts b/packages/1mask/src/shared/stacks.ts index c164d8f..2e32db1 100644 --- a/packages/1mask/src/shared/stacks.ts +++ b/packages/1mask/src/shared/stacks.ts @@ -101,7 +101,7 @@ async function checksum(version: number, h: Uint8Array): Promise { export async function stacksAddressOn(address: string, networkId: NetworkId): Promise { const net = stacksNetworkName(networkId); const a = address.trim().toUpperCase(); - if (!net || !/^S[0-9A-Z]{39,41}$/.test(a)) return undefined; + 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; diff --git a/packages/1mask/test/stacks-provider.test.ts b/packages/1mask/test/stacks-provider.test.ts index 7e900a0..9274ee3 100644 --- a/packages/1mask/test/stacks-provider.test.ts +++ b/packages/1mask/test/stacks-provider.test.ts @@ -62,6 +62,7 @@ describe("Stacks addresses per network", () => { 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(); }); diff --git a/packages/chains-stacks/src/c32.ts b/packages/chains-stacks/src/c32.ts index a02bf02..4d6d8d6 100644 --- a/packages/chains-stacks/src/c32.ts +++ b/packages/chains-stacks/src/c32.ts @@ -60,7 +60,8 @@ export function c32address(version: number, h: Uint8Array): string { /** 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(); - if (!/^S[0-9A-HJKMNP-TV-Z][0-9A-HJKMNP-TV-Z]{38,41}$/i.test(v)) return null; + // 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); diff --git a/packages/chains-stacks/test/stacks.test.ts b/packages/chains-stacks/test/stacks.test.ts index c144985..e4dd512 100644 --- a/packages/chains-stacks/test/stacks.test.ts +++ b/packages/chains-stacks/test/stacks.test.ts @@ -95,6 +95,10 @@ describe("addresses and networks", () => { 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"]); }); From 7a077be0d9afd3db3ff65e53f932facb25f8a0b3 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:28:52 +0800 Subject: [PATCH 18/36] feat(engine,extension-kit,e2e): Bitcoin Cash in the catalogue; matrix targets for the networks87 families - engine/extension-kit: chains-bitcoincash (chipnet, testnet4, mainnet), lazily loaded in the extension. - Matrix: targets cosmos (osmo-test-5), tron (Nile), stacks, fuel and chainflip (Perseverance), plus balance-only targets for Provenance, Initia, MultiversX, ICP and Bitcoin Cash (no injected dapp standard to test). Addresses are per target where the network spells them differently (vaultAddress keeps the vault's form). A Chainflip page (polkadot extension-dapp, as lp.chainflip.io) signs a message and registers an LP account; Spec.call checks a call's approval title where there is no transfer. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix.spec.ts | 26 ++++-- apps/extension/e2e/matrix/addresses.json | 74 ++++++++++++++++- apps/extension/e2e/matrix/chain.ts | 81 ++++++++++++++++--- apps/extension/e2e/matrix/dapps/chainflip.ts | 58 +++++++++++++ apps/extension/package.json | 7 ++ packages/engine/package.json | 1 + packages/engine/src/catalog.ts | 2 + packages/engine/src/wiring.ts | 2 + packages/extension-kit/package.json | 1 + .../extension-kit/src/background/wiring.ts | 1 + pnpm-lock.yaml | 27 +++++++ 11 files changed, 264 insertions(+), 16 deletions(-) create mode 100644 apps/extension/e2e/matrix/dapps/chainflip.ts diff --git a/apps/extension/e2e/matrix.spec.ts b/apps/extension/e2e/matrix.spec.ts index de793bc..d410f19 100644 --- a/apps/extension/e2e/matrix.spec.ts +++ b/apps/extension/e2e/matrix.spec.ts @@ -216,8 +216,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 +237,14 @@ 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/ }, }; /** Hedera over WalletConnect answers with the 0.0.x account id: it must be the account behind our key's EVM alias. */ @@ -267,7 +277,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 +343,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 de11b2c..6151ed8 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 1ba2863..d9b1ce9 100644 --- a/apps/extension/e2e/matrix/chain.ts +++ b/apps/extension/e2e/matrix/chain.ts @@ -21,19 +21,38 @@ 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 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). Provenance, Initia, MultiversX, ICP and Bitcoin Cash are funding/balance targets only: no injected dapp + * standard Clip can answer under its own identity (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" + | "provenance" | "initia" | "multiversx" | "icp" | "bitcoincash"; 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 +74,43 @@ 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)" }, + 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)" }, }; -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", "chainflip", "provenance", "initia", "multiversx", "icp", "bitcoincash", +]; 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 +213,16 @@ 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}`; default: return `${n.explorerUrl}/tx/${id}`; } @@ -254,6 +297,26 @@ 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 "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 0000000..b19b45c --- /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/package.json b/apps/extension/package.json index 756d1d7..b76ab86 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -43,17 +43,24 @@ "@clip-wallet/chains-algorand": "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/connect": "workspace:*", "@clip-wallet/core": "workspace:*", "@clip-wallet/kit-modules": "workspace:*", diff --git a/packages/engine/package.json b/packages/engine/package.json index 9884101..cb1e3bb 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -170,6 +170,7 @@ "@clip-wallet/chains-algorand": "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:*", diff --git a/packages/engine/src/catalog.ts b/packages/engine/src/catalog.ts index 458f6df..1251cdb 100644 --- a/packages/engine/src/catalog.ts +++ b/packages/engine/src/catalog.ts @@ -29,6 +29,7 @@ import { TRON_NETWORKS, usdtAsset as tronUsdt } from "@clip-wallet/chains-tron/n 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 { FUEL_NETWORKS, assetFor as fuelAssetFor, specFor as fuelSpecFor } from "@clip-wallet/chains-fuel/networks"; /** Same mapping as chains-evm's (unexported) curatedAsset(). */ @@ -68,6 +69,7 @@ export function walletNetworks(config: Pick) ...ICP_NETWORKS, ...STACKS_NETWORKS, ...FUEL_NETWORKS, + ...BCH_NETWORKS, ]; return all.filter((n) => (families as readonly string[]).includes(n.family) && (mainnet || n.testnet)); } diff --git a/packages/engine/src/wiring.ts b/packages/engine/src/wiring.ts index 99e04a5..05dfc16 100644 --- a/packages/engine/src/wiring.ts +++ b/packages/engine/src/wiring.ts @@ -22,6 +22,7 @@ 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 { createStarknetModule } from "@clip-wallet/chains-starknet"; import { createTonModule } from "@clip-wallet/chains-ton"; import { createNearModule } from "@clip-wallet/chains-near"; @@ -86,6 +87,7 @@ export function createEngineDependencies(o: EngineWiringOptions): Dependencies & icp: createIcpModule, stacks: createStacksModule, fuel: createFuelModule, + bitcoincash: createBitcoinCashModule, }; // One instance per enabled family for the engine's lifetime (modules keep prepare→finalize state). const chains: Partial> = {}; diff --git a/packages/extension-kit/package.json b/packages/extension-kit/package.json index 6551da7..5c97282 100644 --- a/packages/extension-kit/package.json +++ b/packages/extension-kit/package.json @@ -137,6 +137,7 @@ "@clip-wallet/chains-algorand": "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:*", diff --git a/packages/extension-kit/src/background/wiring.ts b/packages/extension-kit/src/background/wiring.ts index bba2c34..2dddc96 100644 --- a/packages/extension-kit/src/background/wiring.ts +++ b/packages/extension-kit/src/background/wiring.ts @@ -354,6 +354,7 @@ export function createDependencies(opts: WiringOptions): Dependencies { 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())), + 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/pnpm-lock.yaml b/pnpm-lock.yaml index 2445232..b4820bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -539,21 +539,39 @@ importers: '@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 +590,9 @@ 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/connect': specifier: workspace:* version: link:../../packages/connect @@ -1679,6 +1700,9 @@ importers: '@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 @@ -1794,6 +1818,9 @@ importers: '@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 From ec1e3db2d4d2b265f1449480d53197ffabfaecca Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:28:53 +0800 Subject: [PATCH 19/36] feat(1mask): per-network accountsChanged for Stacks/BCH; Bitcoin Cash over WalletConnect (wc2-bch-bcr) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - router: accountsChanged events (connect, sign-in grant, notifyAccountsChanged) carry Stacks and Bitcoin Cash accounts spelled for the site's selected network (ST… / bchtest:…), as their dispatchers answer `accounts`; a Stacks/BCH connect also selects the network it connected on. - walletconnect: the "bch" namespace (bch_getAddresses, bch_signTransaction, bch_signMessage; addressesChanged). A small chain-alias layer maps "bch:bitcoincash" / "bch:bchtest" to the wallet's bip122 BCH networks for the registry check, the connect screen, request routing (family bitcoincash) and session accounts, which are "bch:" spelled for the chain as Cashonize builds them (src/stores/walletconnectStore.ts). bch_getAddresses is answered from the session; the bip122 (Bitcoin) namespace is unchanged. Co-Authored-By: Claude Opus 5.5 --- packages/1mask/src/background/bitcoincash.ts | 4 +- packages/1mask/src/background/router.ts | 30 +++- .../1mask/src/walletconnect/namespaces.ts | 34 ++++- packages/1mask/src/walletconnect/wallet.ts | 27 +++- .../1mask/test/n87-account-events.test.ts | 68 +++++++++ packages/1mask/test/walletconnect-bch.test.ts | 140 ++++++++++++++++++ packages/chains-bitcoincash/README.md | 8 +- 7 files changed, 289 insertions(+), 22 deletions(-) create mode 100644 packages/1mask/test/n87-account-events.test.ts create mode 100644 packages/1mask/test/walletconnect-bch.test.ts diff --git a/packages/1mask/src/background/bitcoincash.ts b/packages/1mask/src/background/bitcoincash.ts index 4d5c22f..dab098b 100644 --- a/packages/1mask/src/background/bitcoincash.ts +++ b/packages/1mask/src/background/bitcoincash.ts @@ -9,8 +9,8 @@ import type { P2RouterInternals } from "./p2-families.js"; * 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` (integration - * step). This dispatcher answers bch_getAddresses with the account's CashAddr spelled for the session's network and + * "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; diff --git a/packages/1mask/src/background/router.ts b/packages/1mask/src/background/router.ts index 8bfff8f..e7790b0 100644 --- a/packages/1mask/src/background/router.ts +++ b/packages/1mask/src/background/router.ts @@ -30,6 +30,8 @@ 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 { isCallsMethod, type CallsHost } from "../shared/calls.js"; @@ -303,10 +305,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) ?? []) { @@ -514,7 +537,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; } @@ -716,8 +739,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/walletconnect/namespaces.ts b/packages/1mask/src/walletconnect/namespaces.ts index 7ea02bc..724bca8 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 7c12156..f1064d9 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/n87-account-events.test.ts b/packages/1mask/test/n87-account-events.test.ts new file mode 100644 index 0000000..c741603 --- /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/walletconnect-bch.test.ts b/packages/1mask/test/walletconnect-bch.test.ts new file mode 100644 index 0000000..5f9e2fb --- /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/chains-bitcoincash/README.md b/packages/chains-bitcoincash/README.md index 1f91e5f..5d19180 100644 --- a/packages/chains-bitcoincash/README.md +++ b/packages/chains-bitcoincash/README.md @@ -90,10 +90,10 @@ the wallet get a caution. A fee above 20 sat/byte (and 10,000 sats) is a "high f 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. `@clip-wallet/1mask` has the pieces: `shared/bitcoincash.ts` (namespace, methods, chain -mapping, CashAddr re-spelling) and `background/bitcoincash.ts` (`createBitcoinCashDispatcher`); the WalletConnect -session layer still has to accept the `bch` namespace and map its chains (integration step). Until then the family -is send/receive in the wallet itself. +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) From 1fb0d0f6ce12beeec2764035960fdba226889570 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:40:54 +0800 Subject: [PATCH 20/36] feat(chains-xrpl): XRP Ledger module: hand-written binary codec, XLS-72d requests, plain-words decode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @clip-wallet/chains-xrpl (family xrpl): mainnet xrpl:0, testnet xrpl:1, devnet xrpl:2 (ChainAgnostic CAIP-2), public JSON-RPC checked with server_info. No SDK at runtime: the binary codec subset, Ripple base58 / X-addresses and the JSON-RPC client are written on @noble/@scure; tests encode the same JSON with ripple-binary-codec and require identical bytes, verify signatures with ripple-keypairs and hash with xrpl.js. - m/44'/144'/i'/0/0, secp256k1; digest = SHA-512Half(STX\0 ‖ signing fields); the vault's low-S r‖s is verified before it is DER-encoded into TxnSignature; submit, then tx until validated. - xrpl:signTransaction / xrpl:signAndSubmitTransaction (XLS-72d params); autofill (Sequence, Fee, LastLedgerSequence) runs once in decode and is cached per request, so the signature covers what was shown. Other network, other account, multisig, already signed and master-key-disabled accounts are refused. - Balances: XRP with the live reserve (spendable()), trust lines; RLUSD and Circle USDC known by issuer, look-alikes spam. buildTransfer (XRP, tokens, X-address tags) and buildTrustLine. - decode: Payment (partial-payment danger, destination tag, unfunded recipient below the reserve, RequireDestTag → memo-required, no trust line), TrustSet, offers, AccountSet flags in words, SetRegularKey / SignerListSet / DisableMaster → account-takeover, AccountDelete → account-closure, NFTokens (accepted offers read from the ledger), escrows, AMM deposit/withdraw; anything else, or any field the codec doesn't know, is blind and can't be signed. - Messages in the chains-xrpl section (English + 11 languages). Co-Authored-By: Claude Opus 5.5 --- packages/chains-xrpl/LICENSE | 202 ++++++++ packages/chains-xrpl/NOTICE | 8 + packages/chains-xrpl/README.md | 111 ++++ packages/chains-xrpl/package.json | 81 +++ packages/chains-xrpl/src/address.ts | 92 ++++ packages/chains-xrpl/src/codec.ts | 416 +++++++++++++++ packages/chains-xrpl/src/describe.ts | 471 +++++++++++++++++ packages/chains-xrpl/src/index.ts | 28 + packages/chains-xrpl/src/module.ts | 485 ++++++++++++++++++ packages/chains-xrpl/src/networks.ts | 158 ++++++ packages/chains-xrpl/src/rpc.ts | 199 +++++++ packages/chains-xrpl/src/util.ts | 89 ++++ packages/chains-xrpl/test/codec.test.ts | 90 ++++ packages/chains-xrpl/test/fixtures.ts | 29 ++ packages/chains-xrpl/test/helpers.ts | 61 +++ packages/chains-xrpl/test/signatures.ts | 11 + packages/chains-xrpl/test/xrpl.test.ts | 350 +++++++++++++ packages/chains-xrpl/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 110 ++++ packages/core/src/messages/locales/n87/ar.ts | 110 ++++ packages/core/src/messages/locales/n87/de.ts | 110 ++++ packages/core/src/messages/locales/n87/es.ts | 110 ++++ packages/core/src/messages/locales/n87/fr.ts | 110 ++++ packages/core/src/messages/locales/n87/hi.ts | 110 ++++ packages/core/src/messages/locales/n87/it.ts | 110 ++++ packages/core/src/messages/locales/n87/ja.ts | 110 ++++ packages/core/src/messages/locales/n87/ko.ts | 110 ++++ .../core/src/messages/locales/n87/pt-BR.ts | 110 ++++ packages/core/src/messages/locales/n87/tr.ts | 110 ++++ .../core/src/messages/locales/n87/zh-Hans.ts | 110 ++++ pnpm-lock.yaml | 25 + tools/release/normalize-manifests.mjs | 1 + 32 files changed, 4228 insertions(+) create mode 100644 packages/chains-xrpl/LICENSE create mode 100644 packages/chains-xrpl/NOTICE create mode 100644 packages/chains-xrpl/README.md create mode 100644 packages/chains-xrpl/package.json create mode 100644 packages/chains-xrpl/src/address.ts create mode 100644 packages/chains-xrpl/src/codec.ts create mode 100644 packages/chains-xrpl/src/describe.ts create mode 100644 packages/chains-xrpl/src/index.ts create mode 100644 packages/chains-xrpl/src/module.ts create mode 100644 packages/chains-xrpl/src/networks.ts create mode 100644 packages/chains-xrpl/src/rpc.ts create mode 100644 packages/chains-xrpl/src/util.ts create mode 100644 packages/chains-xrpl/test/codec.test.ts create mode 100644 packages/chains-xrpl/test/fixtures.ts create mode 100644 packages/chains-xrpl/test/helpers.ts create mode 100644 packages/chains-xrpl/test/signatures.ts create mode 100644 packages/chains-xrpl/test/xrpl.test.ts create mode 100644 packages/chains-xrpl/tsconfig.json diff --git a/packages/chains-xrpl/LICENSE b/packages/chains-xrpl/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..d8e118d --- /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 0000000..083e897 --- /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 0000000..b334d7f --- /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 0000000..3b7baa3 --- /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 0000000..5edfe0d --- /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 0000000..a4b1c05 --- /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 0000000..b022b31 --- /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 0000000..baec361 --- /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 0000000..971aedb --- /dev/null +++ b/packages/chains-xrpl/src/networks.ts @@ -0,0 +1,158 @@ +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, + rpc: ["https://s.altnet.rippletest.net:51234/", "https://testnet.xrpl-labs.com/"], + 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 0000000..ef8804e --- /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 0000000..d618fdf --- /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 0000000..1d3f6ad --- /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 0000000..1e55c33 --- /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 0000000..bd9dd6d --- /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 0000000..59dbbd6 --- /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 0000000..14680be --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-xrpl/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index 7f93dbf..cf04773 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -44,6 +44,116 @@ export default { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index ece218f..e94fc5d 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index 4e9a5a6..b035ca0 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index 0d11d88..0b43870 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index c06b903..be3f06f 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index e163716..c737f37 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index a1bc743..3c97ac9 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index ef153c5..29a8d2a 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index 2326cd4..ff2eefe 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index bb9ceb5..035c9dd 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 28fc014..7f63043 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index 6ca33e9..d05aef7 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -43,6 +43,116 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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) // ---- end chains-antelope diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4820bc..10bfbfa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1500,6 +1500,31 @@ importers: 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: diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index 5661b53..4e0b1ea 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -85,6 +85,7 @@ const FAMILY = { algorand: "Algorand", stacks: "Stacks", bitcoincash: "Bitcoin Cash", + xrpl: "XRP Ledger", }; for (const [f, label] of Object.entries(FAMILY)) { EXTRA[`@clip-wallet/chains-${f}`] = { From 8a17cfa32f75af5bbd77e015aacbe0af0795d0a9 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:40:54 +0800 Subject: [PATCH 21/36] feat(1mask): XLS-72d XRPL wallet (Wallet Standard) and its background dispatcher XLS-72d "Browser Wallet Standard" (XRPLF/XRPL-Standards discussion 206; reference types @xrpl-wallet-standard/core) is Wallet Standard with xrpl:signTransaction and xrpl:signAndSubmitTransaction on chains xrpl:. The wallet registers itself with registerWallet under the kit wallet's own name and icon, and is also at window.clipwallet.xrpl. It has no message signing, so neither does 1Mask for the XRPL. - inpage/xrpl.ts: ClipXrplWallet (connect, disconnect, events, both signing features; network aliases such as xrpl:testnet map to xrpl:1; only connected accounts and registry networks), installXrpl(). - background/xrpl.ts: createXrplDispatcher (allowlist, permission, the account and tx_json.Account must be connected, one network per request, approve(makeReq(...))). Not wired yet: router.ts/index.ts are the coordinator's (wiring notes in the file header). - e2e matrix page dapps/xrpl.ts: discovery as @xrpl-wallet-standard/app does it, L1 connect, L3 a no-op AccountSet with a memo via signAndSubmitTransaction (rippled refuses an XRP payment to yourself); no L2. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix/dapps/xrpl.ts | 49 ++++++++ packages/1mask/src/background/xrpl.ts | 59 ++++++++++ packages/1mask/src/inpage/xrpl.ts | 134 ++++++++++++++++++++++ packages/1mask/src/shared/xrpl.ts | 51 +++++++++ packages/1mask/test/xrpl.test.ts | 146 ++++++++++++++++++++++++ 5 files changed, 439 insertions(+) create mode 100644 apps/extension/e2e/matrix/dapps/xrpl.ts create mode 100644 packages/1mask/src/background/xrpl.ts create mode 100644 packages/1mask/src/inpage/xrpl.ts create mode 100644 packages/1mask/src/shared/xrpl.ts create mode 100644 packages/1mask/test/xrpl.test.ts diff --git a/apps/extension/e2e/matrix/dapps/xrpl.ts b/apps/extension/e2e/matrix/dapps/xrpl.ts new file mode 100644 index 0000000..09cc986 --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/xrpl.ts @@ -0,0 +1,49 @@ +/** + * 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"; + +const RPC = "https://s.altnet.rippletest.net:51234/"; +/** @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/packages/1mask/src/background/xrpl.ts b/packages/1mask/src/background/xrpl.ts new file mode 100644 index 0000000..c6a2e15 --- /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/xrpl.ts b/packages/1mask/src/inpage/xrpl.ts new file mode 100644 index 0000000..35d2641 --- /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/xrpl.ts b/packages/1mask/src/shared/xrpl.ts new file mode 100644 index 0000000..ae7953f --- /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/test/xrpl.test.ts b/packages/1mask/test/xrpl.test.ts new file mode 100644 index 0000000..377089b --- /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); + }); +}); From 57b115cd26f8a78aa5c50cb6ced297381db5e596 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:41:27 +0800 Subject: [PATCH 22/36] feat(1mask,engine,extension-kit,e2e): wire the XRP Ledger (XLS-72d Wallet Standard) The XLS-72d wallet registers under the wallet's own identity and sits at window[globalKey].xrpl; the router dispatches xrpl:signTransaction / xrpl:signAndSubmitTransaction through createXrplDispatcher. chains-xrpl joins the catalogue and loads lazily in the extension. Matrix target xrpl (testnet): L1 connect, L3/L4 a no-op AccountSet with a memo (rippled refuses a payment to yourself); XLS-72d has no message signing. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix.spec.ts | 2 ++ apps/extension/e2e/matrix/chain.ts | 12 ++++++++++-- apps/extension/package.json | 1 + packages/1mask/src/background/methods.ts | 5 ++++- packages/1mask/src/background/router.ts | 3 +++ packages/1mask/src/inpage/index.ts | 2 +- packages/1mask/src/inpage/n87.ts | 19 +++++++++++++++---- packages/1mask/src/shared/config.ts | 1 + packages/engine/package.json | 1 + packages/engine/src/catalog.ts | 2 ++ packages/engine/src/wiring.ts | 2 ++ packages/extension-kit/package.json | 1 + .../extension-kit/src/background/wiring.ts | 1 + pnpm-lock.yaml | 9 +++++++++ 14 files changed, 53 insertions(+), 8 deletions(-) diff --git a/apps/extension/e2e/matrix.spec.ts b/apps/extension/e2e/matrix.spec.ts index d410f19..b0d2a5c 100644 --- a/apps/extension/e2e/matrix.spec.ts +++ b/apps/extension/e2e/matrix.spec.ts @@ -245,6 +245,8 @@ const SPECS: Partial> = { 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/ }, + // 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. */ diff --git a/apps/extension/e2e/matrix/chain.ts b/apps/extension/e2e/matrix/chain.ts index d9b1ce9..2262a10 100644 --- a/apps/extension/e2e/matrix/chain.ts +++ b/apps/extension/e2e/matrix/chain.ts @@ -28,6 +28,7 @@ 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 ADDRESSES from "./addresses.json" with { type: "json" }; export type MatrixFamily = Exclude; @@ -39,7 +40,7 @@ export type MatrixFamily = Exclude; */ export type Target = | "evm" | "hedera-evm" | "hedera" | "solana" | "bitcoin" | "sui" | "aptos" | "cardano" | "substrate" | "starknet" | "ton" | "near" | "stellar" | "tezos" | "algorand" - | "cosmos" | "tron" | "stacks" | "fuel" | "chainflip" + | "cosmos" | "tron" | "stacks" | "fuel" | "chainflip" | "xrpl" | "provenance" | "initia" | "multiversx" | "icp" | "bitcoincash"; interface Entry { @@ -79,6 +80,7 @@ export const TARGETS: Record ChainModu 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)" }, @@ -89,7 +91,7 @@ export const TARGETS: Record ChainModu 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", "chainflip", "provenance", "initia", "multiversx", "icp", "bitcoincash", + "cosmos", "tron", "stacks", "fuel", "xrpl", "chainflip", "provenance", "initia", "multiversx", "icp", "bitcoincash", ]; const hexToBytes = (h: string) => Uint8Array.from(Buffer.from(h.replace(/^0x/, ""), "hex")); @@ -223,6 +225,8 @@ export function explorerTx(t: Target, id: string): string { 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}`; default: return `${n.explorerUrl}/tx/${id}`; } @@ -313,6 +317,10 @@ export async function confirmTx(t: Target, id: string, timeoutMs = 120_000): Pro 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 "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; diff --git a/apps/extension/package.json b/apps/extension/package.json index b76ab86..71d921c 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -61,6 +61,7 @@ "@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:*", diff --git a/packages/1mask/src/background/methods.ts b/packages/1mask/src/background/methods.ts index 1dfc0eb..9fe508d 100644 --- a/packages/1mask/src/background/methods.ts +++ b/packages/1mask/src/background/methods.ts @@ -6,6 +6,7 @@ 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 { cardanoSubstrateAllowlist } from "./cardano-substrate.js"; import { starknetTonAllowlist } from "./starknet-ton.js"; import { HEDERA_INJECTED_METHODS } from "../shared/hedera.js"; @@ -141,8 +142,10 @@ export function injectedAllowlist(family: Family): ReadonlySet { return stacksInjectedAllowlist(); case "fuel": return fuelInjectedAllowlist(); + case "xrpl": + return xrplAllowlist(); default: - // Families without an injected provider (xrpl, antelope, multiversx, icp, bitcoincash): send and receive only. + // Families without an injected provider (antelope, icp; bitcoincash over WalletConnect): send and receive only. return new Set(); } } diff --git a/packages/1mask/src/background/router.ts b/packages/1mask/src/background/router.ts index e7790b0..e2e1686 100644 --- a/packages/1mask/src/background/router.ts +++ b/packages/1mask/src/background/router.ts @@ -33,6 +33,7 @@ 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 { isCallsMethod, type CallsHost } from "../shared/calls.js"; /** Background side of a runtime port (chrome.runtime.Port satisfies it). */ @@ -625,6 +626,7 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { }; const cosmos = createCosmosDispatcher({ ...n87Internals, read: (req) => withTimeout(opts.handle(req), readMs, req.id) }); const tron = createTronDispatcher(n87Internals); + const xrpl = createXrplDispatcher(n87Internals); const stacks = createStacksDispatcher({ ...n87Internals, networks: () => candidates("stacks") }); const fuel = createFuelDispatcher({ permitted, @@ -673,6 +675,7 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { 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 === "hedera" && method === HEDERA_WC_PAIR) { const uri = (params as { uri?: unknown } | undefined)?.uri; if (!opts.walletConnectPair) throw rpcError.unsupportedMethod(method); diff --git a/packages/1mask/src/inpage/index.ts b/packages/1mask/src/inpage/index.ts index f6ec142..2a15778 100644 --- a/packages/1mask/src/inpage/index.ts +++ b/packages/1mask/src/inpage/index.ts @@ -118,7 +118,7 @@ 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 }, + want: { cosmos: want.cosmos ?? true, tron: want.tron ?? true, stacks: want.stacks ?? true, fuel: want.fuel ?? true, xrpl: want.xrpl ?? true }, ...(config.globalKey ? { globalKey: config.globalKey } : {}), }); stops.push(n87.stop); diff --git a/packages/1mask/src/inpage/n87.ts b/packages/1mask/src/inpage/n87.ts index 37d9be5..d1054e3 100644 --- a/packages/1mask/src/inpage/n87.ts +++ b/packages/1mask/src/inpage/n87.ts @@ -2,8 +2,9 @@ * 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) and Fuel (a FuelConnector announced with the FuelConnector event). - * Families without a verifiable third-party wallet standard (MultiversX, ICP, Bitcoin Cash, Antelope) are send and + * window.wbip_providers, WBIP-004), Fuel (a FuelConnector announced with the FuelConnector event) + * and the XRP Ledger (XLS-72d: Wallet Standard with xrpl:signTransaction / xrpl:signAndSubmitTransaction). + * 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"; @@ -13,6 +14,7 @@ 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 type { InpageTransport } from "./transport.js"; export interface InstalledN87 { @@ -20,10 +22,11 @@ export interface InstalledN87 { tron?: ClipTronProvider; stacks?: ClipStacksProvider; fuel?: ClipFuelConnector; + xrpl?: ClipXrplWallet; stop(): void; } -export type N87ProviderFamily = "cosmos" | "tron" | "stacks" | "fuel"; +export type N87ProviderFamily = "cosmos" | "tron" | "stacks" | "fuel" | "xrpl"; /** Installs the providers for every networks87 family present in `networks`. */ export function installN87Providers( @@ -33,7 +36,7 @@ export function installN87Providers( transport: InpageTransport, opts: InjectedOptions & { want?: Partial> } = {}, ): InstalledN87 { - const want: Record = { cosmos: true, tron: true, stacks: true, fuel: true, ...opts.want }; + const want: Record = { cosmos: true, tron: true, stacks: true, fuel: true, xrpl: true, ...opts.want }; const has = (f: string) => networks.some((n) => n.family === f); const g = opts.globalKey ? { globalKey: opts.globalKey } : {}; const stops: (() => void)[] = []; @@ -58,6 +61,13 @@ export function installN87Providers( 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); + } + } return out; } @@ -65,3 +75,4 @@ export * from "./cosmos.js"; export * from "./tron.js"; export * from "./stacks.js"; export * from "./fuel.js"; +export * from "./xrpl.js"; diff --git a/packages/1mask/src/shared/config.ts b/packages/1mask/src/shared/config.ts index 5c4ab79..c55c9b8 100644 --- a/packages/1mask/src/shared/config.ts +++ b/packages/1mask/src/shared/config.ts @@ -51,6 +51,7 @@ export interface InpageConfig { tron?: boolean; stacks?: boolean; fuel?: boolean; + xrpl?: boolean; cardano?: boolean; substrate?: boolean; starknet?: boolean; diff --git a/packages/engine/package.json b/packages/engine/package.json index cb1e3bb..4a14c46 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -188,6 +188,7 @@ "@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/catalog.ts b/packages/engine/src/catalog.ts index 1251cdb..aea58f3 100644 --- a/packages/engine/src/catalog.ts +++ b/packages/engine/src/catalog.ts @@ -30,6 +30,7 @@ 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 { FUEL_NETWORKS, assetFor as fuelAssetFor, specFor as fuelSpecFor } from "@clip-wallet/chains-fuel/networks"; /** Same mapping as chains-evm's (unexported) curatedAsset(). */ @@ -70,6 +71,7 @@ export function walletNetworks(config: Pick) ...STACKS_NETWORKS, ...FUEL_NETWORKS, ...BCH_NETWORKS, + ...XRPL_NETWORKS, ]; return all.filter((n) => (families as readonly string[]).includes(n.family) && (mainnet || n.testnet)); } diff --git a/packages/engine/src/wiring.ts b/packages/engine/src/wiring.ts index 05dfc16..f932623 100644 --- a/packages/engine/src/wiring.ts +++ b/packages/engine/src/wiring.ts @@ -23,6 +23,7 @@ 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 { createStarknetModule } from "@clip-wallet/chains-starknet"; import { createTonModule } from "@clip-wallet/chains-ton"; import { createNearModule } from "@clip-wallet/chains-near"; @@ -88,6 +89,7 @@ export function createEngineDependencies(o: EngineWiringOptions): Dependencies & stacks: createStacksModule, fuel: createFuelModule, bitcoincash: createBitcoinCashModule, + xrpl: createXrplModule, }; // One instance per enabled family for the engine's lifetime (modules keep prepare→finalize state). const chains: Partial> = {}; diff --git a/packages/extension-kit/package.json b/packages/extension-kit/package.json index 5c97282..accd1e5 100644 --- a/packages/extension-kit/package.json +++ b/packages/extension-kit/package.json @@ -155,6 +155,7 @@ "@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/wiring.ts b/packages/extension-kit/src/background/wiring.ts index 2dddc96..008cb7d 100644 --- a/packages/extension-kit/src/background/wiring.ts +++ b/packages/extension-kit/src/background/wiring.ts @@ -354,6 +354,7 @@ export function createDependencies(opts: WiringOptions): Dependencies { 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())), bitcoincash: lazyChain("bitcoincash", "secp256k1", () => import("@clip-wallet/chains-bitcoincash").then((m) => m.createBitcoinCashModule())), }; const prices = createPriceFeed(opts.kv, opts.features?.coingeckoDemoKey); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10bfbfa..8f14554 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -593,6 +593,9 @@ importers: '@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 @@ -1779,6 +1782,9 @@ importers: '@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 @@ -1897,6 +1903,9 @@ importers: '@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 From 8865ef46ddcf15f371defdd3c6de7438fdd25487 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:41:28 +0800 Subject: [PATCH 23/36] test(engine): CLPR coverage and supported-mainnet count from the catalogue Co-Authored-By: Claude Opus 5.5 --- packages/engine/test/networks87.test.ts | 150 ++++++++++++++++++ packages/extension-kit/test/catalog.test.ts | 3 +- .../extension-kit/test/lazy-chains.test.ts | 9 +- 3 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 packages/engine/test/networks87.test.ts diff --git a/packages/engine/test/networks87.test.ts b/packages/engine/test/networks87.test.ts new file mode 100644 index 0000000..134f46c --- /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((c) => c.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/test/catalog.test.ts b/packages/extension-kit/test/catalog.test.ts index 8e73263..eb468af 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/lazy-chains.test.ts b/packages/extension-kit/test/lazy-chains.test.ts index 540b50b..23f4d6e 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(); From e5aaffa83cc45b83045fa3ab2b1c4514f8aa83f8 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:42:03 +0800 Subject: [PATCH 24/36] feat(chains-antelope): Vaulta, Telos and XPR Network module: hand-written serialisation, accounts found by key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @clip-wallet/chains-antelope (family antelope): Jungle4, Vaulta, Telos (+ testnet), XPR Network (+ testnet) as antelope: (ChainAgnostic), public chain APIs checked with get_info. No SDK at runtime: names, assets, PUB_K1/legacy keys, SIG_K1 strings, transactions and an ABI-driven (de)serialiser on @noble/@scure; tests compare bytes, ids, digests and signatures with @wharfkit/antelope (verifyDigest, recoverDigest). - m/44'/194'/0'/0/i, one K1 key for every network; Account.address is the PUB_K1 key. accounts()/receiveAddress() find the names this key controls (get_accounts_by_authorizers on each server, then Hyperion get_key_accounts), or say in plain words that no account uses the key yet and how one gets created. - antelope_signTransaction / antelope_signAndPushTransaction: header (TAPoS, expiry) filled once in decode and cached; digest = SHA-256(chain_id ‖ packed_trx ‖ 32 zero bytes); the vault's signature is verified, checked canonical and recovered before it becomes SIG_K1 (recid + 31); send_transaction2 with a push_transaction fallback. CPU/NET/RAM refusals in plain words per network (Vaulta PowerUp, Telos stake or PowerUp, XPR covered by the network). - Balances: EOS (still Vaulta's system token) with A (core.vaulta) and Tether's USDT, TLOS, XPR; other tokens from Hyperion with look-alike symbols marked spam. buildTransfer via eosio.token-style transfer without an ABI call. - decode: token transfers (any contract with the standard transfer ABI; unknown tokens caution, look-alikes danger), updateauth/deleteauth/linkauth → account-takeover, any other action through its ABI as "Call {action} on {contract}" with its fields; no ABI or unknown types → blind. - No dapp provider: WharfKit plugins live in the dapp, Anchor Link/ESR and Scatter injection answer as Anchor or Scatter/TokenPocket (README). Messages in the chains-antelope section (English + 11 languages). Co-Authored-By: Claude Opus 5.5 --- packages/chains-antelope/LICENSE | 202 +++++++++ packages/chains-antelope/NOTICE | 8 + packages/chains-antelope/README.md | 112 +++++ packages/chains-antelope/package.json | 79 ++++ packages/chains-antelope/src/abi.ts | 315 ++++++++++++++ packages/chains-antelope/src/bytes.ts | 289 +++++++++++++ packages/chains-antelope/src/describe.ts | 156 +++++++ packages/chains-antelope/src/index.ts | 28 ++ packages/chains-antelope/src/module.ts | 385 ++++++++++++++++++ packages/chains-antelope/src/networks.ts | 172 ++++++++ packages/chains-antelope/src/rpc.ts | 185 +++++++++ packages/chains-antelope/src/transaction.ts | 115 ++++++ .../chains-antelope/test/antelope.test.ts | 225 ++++++++++ packages/chains-antelope/test/fixtures.ts | 58 +++ packages/chains-antelope/test/helpers.ts | 61 +++ .../chains-antelope/test/serializer.test.ts | 152 +++++++ packages/chains-antelope/test/signatures.ts | 11 + packages/chains-antelope/tsconfig.json | 1 + packages/core/src/messages/en/n87.ts | 34 ++ packages/core/src/messages/locales/n87/ar.ts | 34 ++ packages/core/src/messages/locales/n87/de.ts | 34 ++ packages/core/src/messages/locales/n87/es.ts | 34 ++ packages/core/src/messages/locales/n87/fr.ts | 34 ++ packages/core/src/messages/locales/n87/hi.ts | 34 ++ packages/core/src/messages/locales/n87/it.ts | 34 ++ packages/core/src/messages/locales/n87/ja.ts | 34 ++ packages/core/src/messages/locales/n87/ko.ts | 34 ++ .../core/src/messages/locales/n87/pt-BR.ts | 34 ++ packages/core/src/messages/locales/n87/tr.ts | 34 ++ .../core/src/messages/locales/n87/zh-Hans.ts | 34 ++ pnpm-lock.yaml | 19 + tools/release/normalize-manifests.mjs | 1 + 32 files changed, 2982 insertions(+) create mode 100644 packages/chains-antelope/LICENSE create mode 100644 packages/chains-antelope/NOTICE create mode 100644 packages/chains-antelope/README.md create mode 100644 packages/chains-antelope/package.json create mode 100644 packages/chains-antelope/src/abi.ts create mode 100644 packages/chains-antelope/src/bytes.ts create mode 100644 packages/chains-antelope/src/describe.ts create mode 100644 packages/chains-antelope/src/index.ts create mode 100644 packages/chains-antelope/src/module.ts create mode 100644 packages/chains-antelope/src/networks.ts create mode 100644 packages/chains-antelope/src/rpc.ts create mode 100644 packages/chains-antelope/src/transaction.ts create mode 100644 packages/chains-antelope/test/antelope.test.ts create mode 100644 packages/chains-antelope/test/fixtures.ts create mode 100644 packages/chains-antelope/test/helpers.ts create mode 100644 packages/chains-antelope/test/serializer.test.ts create mode 100644 packages/chains-antelope/test/signatures.ts create mode 100644 packages/chains-antelope/tsconfig.json diff --git a/packages/chains-antelope/LICENSE b/packages/chains-antelope/LICENSE new file mode 100644 index 0000000..fd2e97a --- /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 0000000..2f081dc --- /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 0000000..a8c2fc8 --- /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 0000000..42f2951 --- /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 0000000..f33a8e3 --- /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 0000000..616be48 --- /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 0000000..a16c2a3 --- /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 0000000..1cec9d4 --- /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 0000000..7581e8b --- /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 0000000..02dda71 --- /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 0000000..8343dff --- /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 0000000..c3fea19 --- /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 0000000..99bb1da --- /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 0000000..44fea9c --- /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 0000000..90c80a1 --- /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 0000000..00a91fd --- /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 0000000..6aa1037 --- /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 0000000..9d04942 --- /dev/null +++ b/packages/chains-antelope/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../../tsconfig.base.json", "include": ["src", "test"] } diff --git a/packages/core/src/messages/en/n87.ts b/packages/core/src/messages/en/n87.ts index cf04773..4fd501a 100644 --- a/packages/core/src/messages/en/n87.ts +++ b/packages/core/src/messages/en/n87.ts @@ -156,6 +156,40 @@ export default { "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}", diff --git a/packages/core/src/messages/locales/n87/ar.ts b/packages/core/src/messages/locales/n87/ar.ts index e94fc5d..838a041 100644 --- a/packages/core/src/messages/locales/n87/ar.ts +++ b/packages/core/src/messages/locales/n87/ar.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/de.ts b/packages/core/src/messages/locales/n87/de.ts index b035ca0..8d7f01b 100644 --- a/packages/core/src/messages/locales/n87/de.ts +++ b/packages/core/src/messages/locales/n87/de.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/es.ts b/packages/core/src/messages/locales/n87/es.ts index 0b43870..2993798 100644 --- a/packages/core/src/messages/locales/n87/es.ts +++ b/packages/core/src/messages/locales/n87/es.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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}", diff --git a/packages/core/src/messages/locales/n87/fr.ts b/packages/core/src/messages/locales/n87/fr.ts index be3f06f..9cbbb4b 100644 --- a/packages/core/src/messages/locales/n87/fr.ts +++ b/packages/core/src/messages/locales/n87/fr.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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}", diff --git a/packages/core/src/messages/locales/n87/hi.ts b/packages/core/src/messages/locales/n87/hi.ts index c737f37..d0a028d 100644 --- a/packages/core/src/messages/locales/n87/hi.ts +++ b/packages/core/src/messages/locales/n87/hi.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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} से अपने स्टेकिंग रिवॉर्ड क्लेम करें", diff --git a/packages/core/src/messages/locales/n87/it.ts b/packages/core/src/messages/locales/n87/it.ts index 3c97ac9..94e72c1 100644 --- a/packages/core/src/messages/locales/n87/it.ts +++ b/packages/core/src/messages/locales/n87/it.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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}", diff --git a/packages/core/src/messages/locales/n87/ja.ts b/packages/core/src/messages/locales/n87/ja.ts index 29a8d2a..6ec6ec2 100644 --- a/packages/core/src/messages/locales/n87/ja.ts +++ b/packages/core/src/messages/locales/n87/ja.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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}からステーキング報酬を受け取る", diff --git a/packages/core/src/messages/locales/n87/ko.ts b/packages/core/src/messages/locales/n87/ko.ts index ff2eefe..5ace0aa 100644 --- a/packages/core/src/messages/locales/n87/ko.ts +++ b/packages/core/src/messages/locales/n87/ko.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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}에서 스테이킹 보상 받기", diff --git a/packages/core/src/messages/locales/n87/pt-BR.ts b/packages/core/src/messages/locales/n87/pt-BR.ts index 035c9dd..42e62ef 100644 --- a/packages/core/src/messages/locales/n87/pt-BR.ts +++ b/packages/core/src/messages/locales/n87/pt-BR.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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}", diff --git a/packages/core/src/messages/locales/n87/tr.ts b/packages/core/src/messages/locales/n87/tr.ts index 7f63043..4454deb 100644 --- a/packages/core/src/messages/locales/n87/tr.ts +++ b/packages/core/src/messages/locales/n87/tr.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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", diff --git a/packages/core/src/messages/locales/n87/zh-Hans.ts b/packages/core/src/messages/locales/n87/zh-Hans.ts index d05aef7..72c99ae 100644 --- a/packages/core/src/messages/locales/n87/zh-Hans.ts +++ b/packages/core/src/messages/locales/n87/zh-Hans.ts @@ -155,6 +155,40 @@ const messages: { readonly [K in keyof typeof N87]: string } = { "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} 领取你的质押奖励", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f14554..f9ccb9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1112,6 +1112,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': diff --git a/tools/release/normalize-manifests.mjs b/tools/release/normalize-manifests.mjs index 4e0b1ea..85a83df 100644 --- a/tools/release/normalize-manifests.mjs +++ b/tools/release/normalize-manifests.mjs @@ -86,6 +86,7 @@ const FAMILY = { 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}`] = { From 5632493c664b76dc7a6761627200804f51d399b0 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:42:05 +0800 Subject: [PATCH 25/36] feat(1mask): best-effort MultiversX provider through sdk-dapp's undocumented custom-provider hook MultiversX has no wallet-discovery standard. @multiversx/sdk-dapp 5.x's initApp merges window.multiversx?.providers with the dapp's customProviders (deduplicated by type) and ProviderFactory.create({ type }) calls the entry's constructor, then init(). That hook is UNDOCUMENTED as a wallet channel (sdk-dapp's README has the dapp set it; the template dapp replaces window.multiversx), so this works only on sdk-dapp dapps that don't override it. Comments and the chains-multiversx README say so. - inpage/multiversx.ts: installMultiversXProvider pushes the wallet's own entry (type = global key "clipwallet", its name and icon; never "extension" or the DeFi Wallet) without replacing the array or other entries, and exposes the provider at window[globalKey].multiversx. It implements sdk-dapp's IProvider: dapp sdk-core Transactions are read with toPlainObject() and returned with `signature` set (guardian fields untouched); login({ token }) signs address + token, the message sdk-native-auth-server verifies. - background/multiversx.ts: createMultiversXDispatcher (connect, accounts, disconnect, mvx_signTransactions, mvx_signMessage through approve(makeReq)), multiversxInjectedAllowlist, MULTIVERSX_CONNECT_METHODS; every signer must be the site's account, and a native-auth login token for another site is refused. - shared/multiversx-methods.ts: wire method names, chain ids, nativeAuthOrigin. - chains-multiversx: a native-auth login decodes as "Sign in to {site}", with a domain-mismatch danger when the token names another site. - Matrix page dapps/multiversx.ts (devnet): sdk-dapp driven headless (initApp + ProviderFactory + native-auth login, verifyMessage, a self-transfer through DappProvider.signTransactions); devDependencies @multiversx/sdk-dapp 5.7.3 and its peers. Checked in a scratch run against the real sdk-dapp: the entry was listed, the native-auth token validated with sdk-native-auth-server on devnet, and the signatures verified with sdk-core 15. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix/dapps/multiversx.ts | 76 + apps/extension/package.json | 6 + packages/1mask/src/background/multiversx.ts | 85 + packages/1mask/src/inpage/multiversx.ts | 241 +++ .../1mask/src/shared/multiversx-methods.ts | 42 + packages/1mask/test/multiversx.test.ts | 228 +++ packages/chains-multiversx/README.md | 58 +- packages/chains-multiversx/src/module.ts | 30 + .../chains-multiversx/test/multiversx.test.ts | 13 + pnpm-lock.yaml | 1483 ++++++++++++----- 10 files changed, 1867 insertions(+), 395 deletions(-) create mode 100644 apps/extension/e2e/matrix/dapps/multiversx.ts create mode 100644 packages/1mask/src/background/multiversx.ts create mode 100644 packages/1mask/src/inpage/multiversx.ts create mode 100644 packages/1mask/src/shared/multiversx-methods.ts create mode 100644 packages/1mask/test/multiversx.test.ts diff --git a/apps/extension/e2e/matrix/dapps/multiversx.ts b/apps/extension/e2e/matrix/dapps/multiversx.ts new file mode 100644 index 0000000..844bdaa --- /dev/null +++ b/apps/extension/e2e/matrix/dapps/multiversx.ts @@ -0,0 +1,76 @@ +/** + * 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: true } })`, 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()`: sdk-dapp asks for a native-auth login token signature (address + token, MessageComputer). + * - 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 () => { + await initApp({ dAppConfig: { environment: "devnet", nativeAuth: true } } 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 || !login.signature) throw new Error("login returned no native-auth signature"); + address = login.address; + return { address, name: entry.name, nativeAuthSignature: login.signature.slice(0, 16) }; + }, + 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/package.json b/apps/extension/package.json index 71d921c..1e45172 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -72,6 +72,9 @@ "@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", @@ -107,12 +110,15 @@ "@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", diff --git a/packages/1mask/src/background/multiversx.ts b/packages/1mask/src/background/multiversx.ts new file mode 100644 index 0000000..bb7e61e --- /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/inpage/multiversx.ts b/packages/1mask/src/inpage/multiversx.ts new file mode 100644 index 0000000..448f380 --- /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/shared/multiversx-methods.ts b/packages/1mask/src/shared/multiversx-methods.ts new file mode 100644 index 0000000..1b3f381 --- /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/test/multiversx.test.ts b/packages/1mask/test/multiversx.test.ts new file mode 100644 index 0000000..3ef6149 --- /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/chains-multiversx/README.md b/packages/chains-multiversx/README.md index 0da15af..26c48a3 100644 --- a/packages/chains-multiversx/README.md +++ b/packages/chains-multiversx/README.md @@ -144,34 +144,49 @@ plain words there. Contracts are any address that starts with 8 zero bytes. `/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: send and receive only (no injected provider) +## 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 without the dapp adding code for that wallet. Sources (checked 2026-10-06): +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 dapp-side code: `ICustomProvider` objects passed to `initApp({ customProviders })`. - - `initApp` also merges `window.multiversx.providers` (`src/methods/initApp/initApp.ts`). The README documents that as - something the dapp author sets ("add it to the window object"), and the template dapp overwrites `window.multiversx`. - There's no announce event or registry a wallet could use. We don't inject into it. + 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 we don't. + `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. -- **WalletConnect works across dapps without impersonation.** Namespace `mvx`, chains `mvx:1` / `mvx:D` / `mvx:T`, with - the methods above. - - This module decodes and signs those requests, so wiring it into the engine's WalletConnect host is enough. - - Stock sdk-dapp labels any WalletConnect wallet "xPortal App" in its unlock panel. The wallet's own WalletConnect - metadata still carries its own name. - - sdk-dapp always asks for `mvx_signLoginToken` too, so a WalletConnect login needs that method; it isn't - implemented yet. - -**Dapp matrix.** -- L0 (send and receive on devnet) works today. -- L1 (connect) and L2 (sign a message) would go through WalletConnect, using sdk-dapp's `verifyMessage` - (`MessageComputer.computeBytesForVerifying` + `UserVerifier`). There's no injected page. + +**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 @@ -182,7 +197,8 @@ https://r3d4.fr/faucet. ## Known gaps -- No injected provider (see above). No login-token / native-auth signing yet. +- 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. diff --git a/packages/chains-multiversx/src/module.ts b/packages/chains-multiversx/src/module.ts index 1065038..736c0ac 100644 --- a/packages/chains-multiversx/src/module.ts +++ b/packages/chains-multiversx/src/module.ts @@ -111,6 +111,24 @@ export function normalize(request: DappRequest, ctx: ChainContext): Normalized { } } +/** + * 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); } @@ -170,6 +188,18 @@ export function createMultiversXModule(options: MultiversXModuleOptions = {}): M 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 }; } diff --git a/packages/chains-multiversx/test/multiversx.test.ts b/packages/chains-multiversx/test/multiversx.test.ts index 3fc3c0d..8ce3cbc 100644 --- a/packages/chains-multiversx/test/multiversx.test.ts +++ b/packages/chains-multiversx/test/multiversx.test.ts @@ -225,6 +225,19 @@ describe("decode", () => { 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" }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9ccb9d..d077045 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -363,10 +363,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(902de281d5a371348f6b5f944abdc922) + 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 +375,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 +396,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 +444,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 +468,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 +495,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)) @@ -622,10 +622,19 @@ importers: 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(f7f7eff1cae7b83564107d7da257a70e) + 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) @@ -661,10 +670,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 @@ -721,7 +730,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 @@ -731,6 +740,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) @@ -749,6 +764,9 @@ importers: 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) @@ -760,7 +778,7 @@ importers: 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)) @@ -784,10 +802,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(4e08597bdbcd476a3dac274ca280fa40) + 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) @@ -796,7 +814,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) @@ -814,16 +832,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)) @@ -874,7 +892,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)) @@ -895,7 +913,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) @@ -904,7 +922,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 @@ -1567,7 +1585,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 @@ -1586,7 +1604,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 @@ -6250,12 +6268,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==} @@ -6283,6 +6307,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'} @@ -6523,15 +6551,78 @@ packages: 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==} @@ -9966,6 +10057,25 @@ packages: 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==} @@ -10266,6 +10376,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'} @@ -10589,6 +10702,9 @@ 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==} @@ -10622,6 +10738,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: @@ -11009,6 +11128,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'} @@ -11137,6 +11260,9 @@ packages: '@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==} @@ -11213,6 +11339,9 @@ packages: '@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==} @@ -12424,6 +12553,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==} @@ -14336,6 +14468,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'} @@ -14353,6 +14488,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==} @@ -14442,6 +14586,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'} @@ -14479,6 +14626,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==} @@ -15242,6 +15392,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==} @@ -15281,6 +15434,9 @@ 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==} @@ -15290,22 +15446,40 @@ packages: 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==} @@ -16064,6 +16238,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'} @@ -16321,6 +16499,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==} @@ -16452,6 +16633,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'} @@ -16778,6 +16962,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==} @@ -17177,6 +17365,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: @@ -17260,6 +17451,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: @@ -17756,6 +17951,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==} @@ -17812,6 +18023,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'} @@ -18048,6 +18263,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==} @@ -18381,6 +18602,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==} @@ -19689,6 +19913,21 @@ packages: 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'} @@ -19768,12 +20007,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': {} @@ -20400,12 +20639,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 @@ -20415,10 +20654,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 @@ -20426,26 +20665,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 @@ -20455,10 +20693,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 @@ -20466,18 +20704,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: @@ -22001,7 +22240,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 @@ -22011,7 +22250,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' @@ -22030,7 +22269,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 @@ -22040,7 +22279,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' @@ -22058,7 +22297,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 @@ -22068,7 +22307,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' @@ -22086,7 +22325,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 @@ -22096,7 +22335,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' @@ -22695,14 +22934,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) @@ -22747,14 +22986,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) @@ -23227,7 +23466,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 @@ -23236,7 +23475,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 @@ -23247,7 +23486,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 @@ -23256,7 +23495,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 @@ -23355,7 +23594,7 @@ snapshots: '@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))(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)': + '@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 @@ -23366,7 +23605,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) @@ -23417,7 +23656,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 @@ -23428,7 +23667,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) @@ -26222,36 +26461,36 @@ 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(4e08597bdbcd476a3dac274ca280fa40)': - 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.17.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - - '@hashgraph/hedera-wallet-connect@2.1.3(902de281d5a371348f6b5f944abdc922)': + '@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.17.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@hashgraph/hedera-wallet-connect@2.1.3(f7f7eff1cae7b83564107d7da257a70e)': + '@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.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: '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -26395,6 +26634,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 @@ -27310,6 +27558,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 @@ -27324,6 +27580,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 @@ -27390,6 +27653,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 @@ -28151,6 +28418,31 @@ snapshots: '@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 @@ -28201,16 +28493,166 @@ snapshots: - 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) @@ -28225,7 +28667,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' @@ -28235,7 +28677,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) @@ -28250,7 +28692,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' @@ -30051,7 +30493,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) @@ -30064,13 +30506,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) @@ -30083,7 +30525,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 @@ -30621,23 +31063,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 @@ -30683,7 +31125,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 @@ -30696,7 +31138,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) @@ -30707,7 +31149,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 @@ -30720,7 +31162,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) @@ -30748,22 +31190,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' @@ -30808,22 +31250,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' @@ -30868,22 +31310,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' @@ -31586,12 +32028,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: @@ -31631,12 +32073,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: @@ -31676,12 +32118,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: @@ -31721,12 +32163,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: @@ -31766,12 +32208,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: @@ -31811,12 +32253,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: @@ -31856,12 +32298,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: @@ -31901,12 +32343,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: @@ -32077,13 +32519,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: @@ -32124,13 +32566,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: @@ -32171,13 +32613,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: @@ -32218,13 +32660,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: @@ -32265,13 +32707,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: @@ -32312,13 +32754,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: @@ -32359,13 +32801,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: @@ -32406,13 +32848,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: @@ -33039,7 +33481,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) @@ -33051,7 +33493,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: @@ -33091,7 +33533,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) @@ -33103,7 +33545,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: @@ -33143,7 +33585,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) @@ -33155,7 +33597,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: @@ -33195,7 +33637,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) @@ -33207,7 +33649,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: @@ -33247,7 +33689,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) @@ -33259,8 +33701,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: @@ -33300,7 +33742,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) @@ -33312,8 +33754,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: @@ -33353,7 +33795,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) @@ -33365,8 +33807,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: @@ -33406,7 +33848,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) @@ -33418,8 +33860,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: @@ -33647,15 +34089,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 @@ -33701,15 +34143,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 @@ -33755,15 +34197,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 @@ -33809,15 +34251,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 @@ -33863,15 +34305,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 @@ -33917,15 +34359,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 @@ -33971,15 +34413,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 @@ -34025,15 +34467,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 @@ -36742,6 +37184,26 @@ snapshots: '@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 @@ -37157,6 +37619,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 @@ -37590,6 +38059,11 @@ 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': {} @@ -37627,6 +38101,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 @@ -38053,18 +38529,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 @@ -38111,18 +38587,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 @@ -38169,34 +38645,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 @@ -38206,12 +38682,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 @@ -38221,12 +38697,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 @@ -38236,12 +38712,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 @@ -38251,12 +38727,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 @@ -38584,21 +39060,109 @@ 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)': + '@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 + '@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.0 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.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))) + '@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) + '@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@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)': 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/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/logger': 3.0.0 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.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/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@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/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@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)(zod@4.0.5) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.39.3 events: 3.3.0 @@ -38628,7 +39192,7 @@ snapshots: - 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)': + '@walletconnect/core@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/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -38636,15 +39200,15 @@ snapshots: '@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.0 + '@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.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))) - '@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) + '@walletconnect/types': 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))) + '@walletconnect/utils': 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)))(typescript@5.9.3)(zod@4.6.5) '@walletconnect/window-getters': 1.0.1 - es-toolkit: 1.39.3 + es-toolkit: 1.44.0 events: 3.3.0 uint8arrays: 3.1.1 transitivePeerDependencies: @@ -38672,7 +39236,7 @@ snapshots: - 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@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)': + '@walletconnect/core@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)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -38680,57 +39244,13 @@ snapshots: '@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@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/logger': 3.0.0 - '@walletconnect/relay-api': 1.0.11 - '@walletconnect/relay-auth': 1.1.0 - '@walletconnect/safe-json': 1.0.2 - '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.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))) - '@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@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)(zod@4.0.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.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/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.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))) - '@walletconnect/utils': 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)))(typescript@5.9.3)(zod@4.6.5) + '@walletconnect/types': 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))) + '@walletconnect/utils': 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)))(typescript@5.9.3)(zod@4.0.5) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.44.0 events: 3.3.0 @@ -38760,21 +39280,21 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@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)': + '@walletconnect/core@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)': 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@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/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/logger': 3.0.2 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.7(@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/utils': 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)))(typescript@5.9.3)(zod@4.0.5) + '@walletconnect/types': 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))) + '@walletconnect/utils': 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)))(typescript@5.9.3)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.44.0 events: 3.3.0 @@ -38804,21 +39324,21 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@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)': + '@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(@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/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.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))) - '@walletconnect/utils': 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)))(typescript@5.9.3)(zod@3.25.76) + '@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 @@ -39125,9 +39645,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))) @@ -39171,9 +39691,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))) @@ -39217,9 +39737,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))) @@ -39263,9 +39783,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))) @@ -40100,6 +40620,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) @@ -41748,6 +42304,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 @@ -43845,6 +44445,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: @@ -43994,9 +44597,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 @@ -44009,7 +44612,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 @@ -46839,6 +47442,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 @@ -46857,6 +47466,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: @@ -46985,6 +47605,8 @@ snapshots: ignore@5.3.2: {} + immer@10.1.1: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -47019,6 +47641,9 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.7: + optional: true + internmap@1.0.1: {} internmap@2.0.3: {} @@ -48202,6 +48827,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 @@ -48256,22 +48883,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: @@ -49716,6 +50360,8 @@ snapshots: object-hash@3.0.0: {} + object-inspect@1.13.4: {} + object-is@1.1.6: dependencies: call-bind: 1.0.9 @@ -50162,6 +50808,9 @@ snapshots: parseurl@1.3.3: {} + path-browserify@1.0.1: + optional: true + path-data-parser@0.1.0: {} path-exists@4.0.0: {} @@ -50324,6 +50973,8 @@ snapshots: pvutils: 1.2.0 tslib: 2.8.1 + platform@1.3.6: {} + playwright-core@1.63.0: {} playwright@1.63.0: @@ -50413,47 +51064,47 @@ snapshots: transitivePeerDependencies: - supports-color - porto@0.2.35(7042b70557cb9848dbae83cba6f51b7c): + porto@0.2.35(3196dd928d628e279521689dd6a3f17e): 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)) + '@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 @@ -50698,6 +51349,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: @@ -51534,6 +52189,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 @@ -51622,7 +52280,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) @@ -51630,7 +52288,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: @@ -51645,7 +52303,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) @@ -51653,7 +52311,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: @@ -51668,12 +52326,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 @@ -51687,12 +52345,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 @@ -51732,6 +52390,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 @@ -52434,6 +53097,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: {} @@ -52485,6 +53176,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 @@ -52644,10 +53346,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 @@ -52731,10 +53433,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 @@ -52922,6 +53624,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 @@ -53320,6 +54032,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 @@ -54597,11 +55315,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) @@ -54647,11 +55365,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) @@ -54697,11 +55415,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) @@ -55255,58 +55973,75 @@ snapshots: zone-file@2.0.0-beta.3: {} - 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@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) From ed21355c696e0ecc47dcfb2e80cf870c15fd7833 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:42:07 +0800 Subject: [PATCH 26/36] feat(1mask,engine,extension-kit,e2e): wire Antelope and the best-effort MultiversX provider - chains-antelope (Vaulta, Telos, XPR Network + Jungle4, Telos and XPR testnets) in the catalogue, lazily loaded; send and receive only (no wallet-side Antelope dapp standard Clip can answer as itself). - MultiversX: an entry on sdk-dapp's window.multiversx.providers custom-provider hook under the wallet's own name and icon. The hook is undocumented and only works on dapps that don't reset it; send and receive in the wallet don't depend on it. Router, allowlist and the connect approval (mvx:connect) wired. - Matrix: multiversx (devnet) target with its sdk-dapp page; antelope (Jungle4) as a funding/balance target. - extension-kit tests: 26 families (22 lazy); THORChain has no public testnet. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix.spec.ts | 1 + apps/extension/e2e/matrix/chain.ts | 17 +++++++++++++---- apps/extension/package.json | 1 + packages/1mask/src/background/index.ts | 5 +++-- packages/1mask/src/background/methods.ts | 3 +++ packages/1mask/src/background/router.ts | 3 +++ packages/1mask/src/inpage/index.ts | 2 +- packages/1mask/src/inpage/n87.ts | 16 +++++++++++++--- packages/1mask/src/shared/config.ts | 1 + packages/engine/package.json | 1 + packages/engine/src/catalog.ts | 2 ++ packages/engine/src/wiring.ts | 2 ++ packages/engine/test/networks87.test.ts | 2 +- packages/extension-kit/package.json | 1 + packages/extension-kit/src/background/wiring.ts | 1 + pnpm-lock.yaml | 9 +++++++++ 16 files changed, 56 insertions(+), 11 deletions(-) diff --git a/apps/extension/e2e/matrix.spec.ts b/apps/extension/e2e/matrix.spec.ts index b0d2a5c..54f5d77 100644 --- a/apps/extension/e2e/matrix.spec.ts +++ b/apps/extension/e2e/matrix.spec.ts @@ -245,6 +245,7 @@ const SPECS: Partial> = { 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." }, }; diff --git a/apps/extension/e2e/matrix/chain.ts b/apps/extension/e2e/matrix/chain.ts index 2262a10..bea77be 100644 --- a/apps/extension/e2e/matrix/chain.ts +++ b/apps/extension/e2e/matrix/chain.ts @@ -29,19 +29,21 @@ import { BCH_CHIPNET, createBitcoinCashModule } from "@clip-wallet/chains-bitcoi 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). * networks87: "cosmos" is Osmosis testnet (osmo-test-5), "chainflip" the Chainflip Perseverance testnet (substrate - * family). Provenance, Initia, MultiversX, ICP and Bitcoin Cash are funding/balance targets only: no injected dapp - * standard Clip can answer under its own identity (docs/r1/networks87.md), so they have no dapp page. + * 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" - | "provenance" | "initia" | "multiversx" | "icp" | "bitcoincash"; + | "multiversx" | "provenance" | "initia" | "icp" | "bitcoincash" | "antelope"; interface Entry { address: string; @@ -87,11 +89,12 @@ export const TARGETS: Record ChainModu 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", - "cosmos", "tron", "stacks", "fuel", "xrpl", "chainflip", "provenance", "initia", "multiversx", "icp", "bitcoincash", + "cosmos", "tron", "stacks", "fuel", "xrpl", "chainflip", "multiversx", "provenance", "initia", "icp", "bitcoincash", "antelope", ]; const hexToBytes = (h: string) => Uint8Array.from(Buffer.from(h.replace(/^0x/, ""), "hex")); @@ -227,6 +230,8 @@ export function explorerTx(t: Target, id: string): string { 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}`; } @@ -317,6 +322,10 @@ export async function confirmTx(t: Target, id: string, timeoutMs = 120_000): Pro 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"; diff --git a/apps/extension/package.json b/apps/extension/package.json index 1e45172..2fef339 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -41,6 +41,7 @@ "@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:*", diff --git a/packages/1mask/src/background/index.ts b/packages/1mask/src/background/index.ts index 5c878c1..30d8fa5 100644 --- a/packages/1mask/src/background/index.ts +++ b/packages/1mask/src/background/index.ts @@ -48,7 +48,8 @@ import { COSMOS_CONNECT_METHODS as _COSMOS_CONNECT, COSMOS_INJECTED as _COSMOS } 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"; -/** Methods the host treats as a connect approval (Keplr enable, TIP-1193 accounts, SIP-030 addresses, FuelConnector connect). */ -export const N87_CONNECT_METHODS: readonly string[] = [..._COSMOS_CONNECT, ..._TRON_CONNECT, _STACKS.connect, ..._FUEL_CONNECT]; +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 9fe508d..e903e26 100644 --- a/packages/1mask/src/background/methods.ts +++ b/packages/1mask/src/background/methods.ts @@ -7,6 +7,7 @@ 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"; @@ -144,6 +145,8 @@ export function injectedAllowlist(family: Family): ReadonlySet { return fuelInjectedAllowlist(); case "xrpl": return xrplAllowlist(); + case "multiversx": + return multiversxInjectedAllowlist(); default: // Families without an injected provider (antelope, icp; bitcoincash over WalletConnect): send and receive only. return new Set(); diff --git a/packages/1mask/src/background/router.ts b/packages/1mask/src/background/router.ts index e2e1686..dbfb1b7 100644 --- a/packages/1mask/src/background/router.ts +++ b/packages/1mask/src/background/router.ts @@ -34,6 +34,7 @@ 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). */ @@ -627,6 +628,7 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { 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, @@ -676,6 +678,7 @@ export function createOneMaskRouter(opts: OneMaskRouterOptions): OneMaskRouter { 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); diff --git a/packages/1mask/src/inpage/index.ts b/packages/1mask/src/inpage/index.ts index 2a15778..cfee508 100644 --- a/packages/1mask/src/inpage/index.ts +++ b/packages/1mask/src/inpage/index.ts @@ -118,7 +118,7 @@ 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 }, + 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); diff --git a/packages/1mask/src/inpage/n87.ts b/packages/1mask/src/inpage/n87.ts index d1054e3..3229158 100644 --- a/packages/1mask/src/inpage/n87.ts +++ b/packages/1mask/src/inpage/n87.ts @@ -3,7 +3,8 @@ * 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) - * and the XRP Ledger (XLS-72d: Wallet Standard with xrpl:signTransaction / xrpl:signAndSubmitTransaction). + * 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). */ @@ -15,6 +16,7 @@ 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 { @@ -23,10 +25,11 @@ export interface InstalledN87 { stacks?: ClipStacksProvider; fuel?: ClipFuelConnector; xrpl?: ClipXrplWallet; + multiversx?: ClipMultiversXProvider; stop(): void; } -export type N87ProviderFamily = "cosmos" | "tron" | "stacks" | "fuel" | "xrpl"; +export type N87ProviderFamily = "cosmos" | "tron" | "stacks" | "fuel" | "xrpl" | "multiversx"; /** Installs the providers for every networks87 family present in `networks`. */ export function installN87Providers( @@ -36,7 +39,7 @@ export function installN87Providers( transport: InpageTransport, opts: InjectedOptions & { want?: Partial> } = {}, ): InstalledN87 { - const want: Record = { cosmos: true, tron: true, stacks: true, fuel: true, xrpl: true, ...opts.want }; + 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)[] = []; @@ -68,6 +71,12 @@ export function installN87Providers( 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; } @@ -76,3 +85,4 @@ 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/shared/config.ts b/packages/1mask/src/shared/config.ts index c55c9b8..3e9b25a 100644 --- a/packages/1mask/src/shared/config.ts +++ b/packages/1mask/src/shared/config.ts @@ -52,6 +52,7 @@ export interface InpageConfig { stacks?: boolean; fuel?: boolean; xrpl?: boolean; + multiversx?: boolean; cardano?: boolean; substrate?: boolean; starknet?: boolean; diff --git a/packages/engine/package.json b/packages/engine/package.json index 4a14c46..e8e080b 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -168,6 +168,7 @@ "@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:*", diff --git a/packages/engine/src/catalog.ts b/packages/engine/src/catalog.ts index aea58f3..ca65a00 100644 --- a/packages/engine/src/catalog.ts +++ b/packages/engine/src/catalog.ts @@ -31,6 +31,7 @@ import { ICP_IDS, ICP_NETWORKS, LEDGERS as ICP_LEDGERS, ledgerAsset as icpLedger 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(). */ @@ -72,6 +73,7 @@ export function walletNetworks(config: Pick) ...FUEL_NETWORKS, ...BCH_NETWORKS, ...XRPL_NETWORKS, + ...ANTELOPE_NETWORKS, ]; return all.filter((n) => (families as readonly string[]).includes(n.family) && (mainnet || n.testnet)); } diff --git a/packages/engine/src/wiring.ts b/packages/engine/src/wiring.ts index f932623..b89ed2f 100644 --- a/packages/engine/src/wiring.ts +++ b/packages/engine/src/wiring.ts @@ -24,6 +24,7 @@ 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"; @@ -90,6 +91,7 @@ export function createEngineDependencies(o: EngineWiringOptions): Dependencies & 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> = {}; diff --git a/packages/engine/test/networks87.test.ts b/packages/engine/test/networks87.test.ts index 134f46c..cb2f6b4 100644 --- a/packages/engine/test/networks87.test.ts +++ b/packages/engine/test/networks87.test.ts @@ -127,7 +127,7 @@ const covered = CLPR.map(([slug, name, match]) => ({ slug, name, nets: match ? M describe("networks87: CLPR coverage from code", () => { it("lists the 87 CLPR networks once each", () => { expect(CLPR).toHaveLength(87); - expect(new Set(CLPR.map((c) => c.slug)).size).toBe(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", () => { diff --git a/packages/extension-kit/package.json b/packages/extension-kit/package.json index accd1e5..772998c 100644 --- a/packages/extension-kit/package.json +++ b/packages/extension-kit/package.json @@ -135,6 +135,7 @@ "@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:*", diff --git a/packages/extension-kit/src/background/wiring.ts b/packages/extension-kit/src/background/wiring.ts index 008cb7d..30fef1d 100644 --- a/packages/extension-kit/src/background/wiring.ts +++ b/packages/extension-kit/src/background/wiring.ts @@ -355,6 +355,7 @@ export function createDependencies(opts: WiringOptions): Dependencies { 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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d077045..0c20d5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -533,6 +533,9 @@ 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 @@ -1759,6 +1762,9 @@ 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 @@ -1880,6 +1886,9 @@ 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 From 20b6dda6ce17cab573f0b02720059ae112ec3564 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:42:33 +0800 Subject: [PATCH 27/36] test(vault): networks87 modules derive and spell addresses as the vault does; changeset Co-Authored-By: Claude Opus 5.5 --- .changeset/networks87.md | 22 ++++++++++++++ packages/vault/package.json | 10 +++++++ packages/vault/test/families87.test.ts | 40 ++++++++++++++++++++++++++ pnpm-lock.yaml | 30 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 .changeset/networks87.md diff --git a/.changeset/networks87.md b/.changeset/networks87.md new file mode 100644 index 0000000..44d721b --- /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/packages/vault/package.json b/packages/vault/package.json index 562364d..0793d44 100644 --- a/packages/vault/package.json +++ b/packages/vault/package.json @@ -70,7 +70,17 @@ "@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", diff --git a/packages/vault/test/families87.test.ts b/packages/vault/test/families87.test.ts index b07f8ee..290730f 100644 --- a/packages/vault/test/families87.test.ts +++ b/packages/vault/test/families87.test.ts @@ -273,3 +273,43 @@ describe("ClipVault: the networks87 families through the public API", () => { 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/pnpm-lock.yaml b/pnpm-lock.yaml index 0c20d5d..74994be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2709,9 +2709,39 @@ importers: '@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 From f126a9a679324ad78eb165ef0c547b455c502728 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:42:53 +0800 Subject: [PATCH 28/36] docs: networks87 gap table, the two networks without a self-custodial model, family counts docs/r1/networks87.md maps each of the 87 CLPR networks to its module and mainnet network id (generated from the engine coverage test), says why Canton and Mixin can't be supported by a key wallet and what the closest honest support is, and gives the true numbers: 85 of 87 CLPR networks, 93 mainnets by the landing page's rule. Co-Authored-By: Claude Opus 5.5 --- .harness/spec.md | 2 +- AGENTS.md | 5 +- README.md | 2 +- docs/r1/networks87.md | 200 ++++++++++++++++++++++++++ llms.txt | 4 +- packages/config/README.md | 2 +- packages/create-clip-wallet/README.md | 2 +- packages/extension-kit/README.md | 2 +- 8 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 docs/r1/networks87.md diff --git a/.harness/spec.md b/.harness/spec.md index 678eca0..356b500 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 f13c98c..74083ef 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 5701a83..a54584f 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,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/docs/r1/networks87.md b/docs/r1/networks87.md new file mode 100644 index 0000000..3b439d1 --- /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 7d636fe..f4b6aa7 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/config/README.md b/packages/config/README.md index 8a95d9f..adc8c46 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/create-clip-wallet/README.md b/packages/create-clip-wallet/README.md index 10cf26b..b421be2 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/extension-kit/README.md b/packages/extension-kit/README.md index 4fe8c1e..4ade23c 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. From 217c4a0c35481cf651cd930d0fac42dfd135de87 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:42:59 +0800 Subject: [PATCH 29/36] =?UTF-8?q?test(extension):=20e2e=20expects=20every?= =?UTF-8?q?=20shipped=20family=20and=20a=20per-network=20receive=20address?= =?UTF-8?q?=20(OSMO=20on=20osmo1=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/phase2.spec.ts | 30 ++++++++++++++++-- apps/extension/screenshots/home-dark.png | Bin 31785 -> 31782 bytes .../screenshots/receive-all-families.png | Bin 25732 -> 26436 bytes apps/extension/screenshots/receive-osmo.png | Bin 0 -> 16422 bytes 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 apps/extension/screenshots/receive-osmo.png diff --git a/apps/extension/e2e/phase2.spec.ts b/apps/extension/e2e/phase2.spec.ts index 3a08e2e..55faa2c 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/screenshots/home-dark.png b/apps/extension/screenshots/home-dark.png index 8455ce3c53ff09e28be6677eaac8791f4472a9e2..77fa0ecbc1fc4810152e309e0a3320cc509e9082 100644 GIT binary patch delta 29059 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

~`xXi`C@Y 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 Tc5JGH-#mLOt0Ge?WfuBB&Mn?b delta 29128 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

=Sh(5#xVQ+ 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 D9#AVk diff --git a/apps/extension/screenshots/receive-all-families.png b/apps/extension/screenshots/receive-all-families.png index fc1020b681f5f7c9079e96a680e784367393e66d..7b62c9cf98231741a75fcb444ef0872f87250790 100644 GIT binary patch literal 26436 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 G(Ek8+A=G35 literal 25732 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 diff --git a/apps/extension/screenshots/receive-osmo.png b/apps/extension/screenshots/receive-osmo.png new file mode 100644 index 0000000000000000000000000000000000000000..6a5d510f14df05a6a783f94e29148cd1a09889e2 GIT binary patch literal 16422 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 Date: Tue, 6 Oct 2026 21:42:59 +0800 Subject: [PATCH 30/36] test(extension): networks87 dapp matrix rows and funding list Matrix on the new families' testnets: L1 connect passes for Cosmos (osmo-test-5), TRON (Nile), Stacks, Fuel, XRP Ledger, Chainflip (Perseverance) and MultiversX (devnet); L2 message signatures verify with each ecosystem's own function (XLS-72d has none); L3 confirmed on Stacks and the XRP Ledger (both funded from keyless faucet APIs); the rest wait for funds and say why. Fixes found: XRPL testnet RPC without CORS preflight (testnet.xrpl-labs.com first) and the MultiversX page's L1 asking for a second approval (native auth off). docs/r1/dapp-matrix.md and dapp-matrix-funding.md list results and the faucets left for the owner. MATRIX_DEBUG=1 prints a dapp page's console. Co-Authored-By: Claude Opus 5.5 --- apps/extension/e2e/matrix.spec.ts | 2 + apps/extension/e2e/matrix/dapps/multiversx.ts | 12 +- apps/extension/e2e/matrix/dapps/xrpl.ts | 3 +- .../e2e/shots/matrix/chainflip-approval.png | Bin 0 -> 45131 bytes .../e2e/shots/matrix/chainflip-sign.png | Bin 0 -> 45316 bytes .../e2e/shots/matrix/cosmos-sign.png | Bin 0 -> 45926 bytes apps/extension/e2e/shots/matrix/fuel-sign.png | Bin 0 -> 45551 bytes .../e2e/shots/matrix/multiversx-sign.png | Bin 0 -> 46311 bytes apps/extension/e2e/shots/matrix/results.json | 439 ++++++++++++++++-- .../e2e/shots/matrix/stacks-approval.png | Bin 0 -> 37306 bytes .../e2e/shots/matrix/stacks-sign.png | Bin 0 -> 45861 bytes apps/extension/e2e/shots/matrix/tron-sign.png | Bin 0 -> 46211 bytes .../e2e/shots/matrix/xrpl-approval.png | Bin 0 -> 39411 bytes docs/r1/dapp-matrix-funding.md | 26 ++ docs/r1/dapp-matrix.md | 25 + packages/chains-xrpl/src/networks.ts | 3 +- 16 files changed, 476 insertions(+), 34 deletions(-) create mode 100644 apps/extension/e2e/shots/matrix/chainflip-approval.png create mode 100644 apps/extension/e2e/shots/matrix/chainflip-sign.png create mode 100644 apps/extension/e2e/shots/matrix/cosmos-sign.png create mode 100644 apps/extension/e2e/shots/matrix/fuel-sign.png create mode 100644 apps/extension/e2e/shots/matrix/multiversx-sign.png create mode 100644 apps/extension/e2e/shots/matrix/stacks-approval.png create mode 100644 apps/extension/e2e/shots/matrix/stacks-sign.png create mode 100644 apps/extension/e2e/shots/matrix/tron-sign.png create mode 100644 apps/extension/e2e/shots/matrix/xrpl-approval.png diff --git a/apps/extension/e2e/matrix.spec.ts b/apps/extension/e2e/matrix.spec.ts index 54f5d77..2dfc8cd 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"}`); diff --git a/apps/extension/e2e/matrix/dapps/multiversx.ts b/apps/extension/e2e/matrix/dapps/multiversx.ts index 844bdaa..2363ea4 100644 --- a/apps/extension/e2e/matrix/dapps/multiversx.ts +++ b/apps/extension/e2e/matrix/dapps/multiversx.ts @@ -1,10 +1,10 @@ /** * 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: true } })`, which merges + * - 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()`: sdk-dapp asks for a native-auth login token signature (address + token, MessageComputer). + * `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 @@ -44,14 +44,16 @@ expose({ }, steps: { connect: async () => { - await initApp({ dAppConfig: { environment: "devnet", nativeAuth: true } } as Parameters[0]); + // 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 || !login.signature) throw new Error("login returned no native-auth signature"); + if (!login?.address) throw new Error("login returned no address"); address = login.address; - return { address, name: entry.name, nativeAuthSignature: login.signature.slice(0, 16) }; + return { address, name: entry.name }; }, sign: async () => { const signed = await provider.signMessage(new Message({ address: Address.newFromBech32(address), data: new TextEncoder().encode(MESSAGE) })); diff --git a/apps/extension/e2e/matrix/dapps/xrpl.ts b/apps/extension/e2e/matrix/dapps/xrpl.ts index 09cc986..9159925 100644 --- a/apps/extension/e2e/matrix/dapps/xrpl.ts +++ b/apps/extension/e2e/matrix/dapps/xrpl.ts @@ -10,7 +10,8 @@ import { getWallets } from "@wallet-standard/app"; import { NeedsFunds, expose, waitFor } from "../dapp-kit"; -const RPC = "https://s.altnet.rippletest.net:51234/"; +// 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; 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 0000000000000000000000000000000000000000..b266597ef0d4c3e7677a2817568ea47bfee5a8e4 GIT binary patch literal 45131 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%0DBG0NEm;I*D%ko_-1TA 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de35715bd0835eb28019ab9e6b7396779dd31c1b GIT binary patch literal 45316 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 z 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

T5 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%0DHNOr%^0qDxT>y%!WpZ~?YGXPXe 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..66a5cd001b6bda1d033ae962957fd988badd991e GIT binary patch literal 45926 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..75ccd56bc2f31e695e8bc94912469f4693427197 GIT binary patch literal 45551 zcmce;byU=E81_kbN=kQkcZUkn2q+yA(%n6Dmmm#_Aks+ZP|_e0(%qdy@AG^2yzkjP zXV2N)KX&I2<-jmA-|rLmb$_nwAzDLC85@HP0}c)j`}HdYEjT!M8#p+G2{Z)o9lq-T zR5&<1xYr7@I^H>lxybH>gTxW+#}V@3NN-OIx|hJi!_2^tBr=lg`1lxa(TIRSCRByy z*ymYQb}G(FjGwn@WpHqB&$f)~_F(ygq;9{aW^x=2njA*NfB(`kP$0>^iU<#*K^shR z#zUfg`2r`DhCv=Gix+PBN(3!52@4%n7B7W%4Lq6e|MtmoyeedL`l<^4stmEwW-c`x z#=~`SnIXLV0v7$-DE-^7GlW7XQs(C7nwpwm-Ve4PBDKj;_^?8flkumhQGD}%Glwf8 z4=5|uiIk8tjZE}+jeN=C3w`BTK*GyAX{*Pb77|EGo-HGrDK+ukS)pdG3wHX7c=$5% zCBG|AHILODp-Sk4s=YmyX(xSuf4`EFZ}rv{6NMSaowh%z3K<`%{(C4AZPeEt;dj^x z#`W^jo+>FnP`dhDFqQCL$1Td==ISD@!k=ZT3SXMKbjkaWP(xGlI8bKF2CVI?(l@+aAJ2-H=+eBZpF!oA(lU{D) z!h3zT5h>Z~>2dN;Wv3r^BrF_JEel zz4T7Gww}L#% zjN>AhwI1i2tX4y5@0clCy-uQ|&}L)p@XC;|Q6!WE-o6cb*EXInMJd*l-}taIp10iU z)o51CRLT#&_PIU(Ruk1-cRo(agSh;Qw#mBA& zN8%p$d?tH%7o0_c(u^LX-yRB|Pi>&0uiEFF1RJdP&gok_k`X*3{ zJ6<1(f|c|3i$K8HdLJ0&q)C{Fc`q6vf2#G$P&zxiPU-G;MzQ}b2Dz}KrE47?a$@|a z*~$$%uj9omNk1VWXWw`-L2`kl*RRL(BoAhtg5$wF*(ircL^;35p%lZSuF-ZoheDw! zxKvtA&dD)E>_%P;;&jvf%#3i5F;Zaa$(yDCwe;iLJn*)!Tu*O>CJGp zkf%#ObkdF#edd0kk`C0Vll7(dCA8hbnPcZH#b>nbl=rg^>}y1{_ZCXJkfge z3*>S4byD#2qtkLKbH^{c>C3aTy?=GHORZjKMCuDkwDL!Xhr?;CZtJ~+U}RB{k=Ykr z&Nl~}(qdy(78)E*mRe`)ZR?y?I?7bqS}bj{%3Bzszfq7a&qpOB_&)5?K4W5QOX8gF zZ>bPiq~6~gclbP#&GasKIb{*I1zeC7Q=F`{gGrqbQn;2aTvfxby*&~bI9_Gay1$w< zwnU%*z{rKj6qFpBpU$txW!a?M@u0!`d!}5Eo0EexA9e?mP^unTKRTF2S>a8~WjQnZ zCCjYGNGFB7KHgN;9MUitAzR~j!xbl8__Cm->iw@5li}Lm)z&uR$%Pl{ZMBP)uCQM6 z{jD^@94LSkL`nob`G0$pt`vV~eS5xDS@~}ufs);@!fN0Z@ul5 zCgS1YVSgeuv;Wq~3AcTZx>7X;Ikl9(i2JTa2mJKr-YY7-v=cf6MrN&Kd0E$OP@K z_64g+_Ghb@7sXgub~~Rlz_ok*tF?mdZf#B1SbTq*Ep&6dq++!$$V}gDIL-T-QzfnrzVG2)KVCDl%H1rK$qlDZ`4;T_s>Qa_uvw$C zCYfFt=dZn#J$w)8v%I_&y{rvwm2rEhZM=;%n`G)u#(w3yZ*Q#9@A<8U1k8I^Y^dh) zqyog!qD~THVnQoeC}!%c6J0heN6)*%P+=y&?1+pXrhn)XwIF}^@IhF3Hdoy1?oP@F zc1o)>*1K0`*j($3A(nrCLycylTV;GSpyW}nwJ5jv#w@!aL)z~;hh0ZO0Vx%m81_S@ zZQGI|vs{dc^_vW<%|BfFpaK&0d|1oY@7wb}%#JDEYzs>Tg}k=o-<9Q`xkbf9A(6}y zj!A-_8lp}wUh;hmHM>9DOMTTm>$Y|N?dQK*S{-dbpdM!4MOuj!q&oL9ROP1-3M;CMgV}Uh`c37t7b|3P-mMxlp%)Oi6dt-*0A;>cnk0jeC#rJ61D)_*;nT@ThDPd+h|W|f5E9hJj9PDWnxuXr z^7FfOFk6oqin?wppoZZScc2D34$nB;4%-^FUag9+rCc5^eRk@}4S|FrhIfS4H1iCOth48?5Jb{+{btv6=!ZDHDr5Ox zeBT;Snlry#?N5_*Kq)u2INsNz{kL<};x~=uw#uaAc`IL%IO3So;UO&gg1HolOKsfY zZ?M$jq040K##gUkm4YMywrM<@qnaAtd1Y+f21$!lg(o374Q!ktJ&pK$5@Dg49ICTS z-p;5YUOkWdUpwj~I7pPfV5=#VN1YoXU1|z??l>iqdb&UP3pN3ahWa;n72#$pzdq?b z30Yojk2W|g1U|rAM6UPMWNRvn+kYr0=8=ZUye=N5+q@wT8NQUZ^nOx{$S74!Ax(d6&KgrwgN~vxcY;Hex)-5VmD3rc?tOZwt+BegRh!r5 z)b2^UR(PO(5+4&?xo6N}G2Jg~MvIB_v-9>aKm3rAhHO2$vfqz|E3rMg13H_+>XMQWib; z%jw!)oqku@W5?sk(>|#;Pdg`8nvF?O)s!TOT@(B7ibYjCHB8Jn%kPEubixKhFND}` zFgN(vG-q+nXfR9$gHldok1!C4mS0Zcj1EqqY%#FXv%VgLbJDr2^BrYa`5Qt)y+rZ1 zU@9{spicBRLgd9wMOcZdt@Kx?+y<(jXf6}l{Rid zL0&vF+I)RUC3Kx=VD~ZQR9*A(;0Io~n9aV72ZM>TK(SHF33L1fvloHjm4|EoWHqFHNdv*vr!=X*i<^6u)9UkR_1GAPYi z6-upwV0@p0>HmG*IyaHqL8`g{c`BrMxMfkaCj6Tfot)J8iCtgRN-P6)!dr|NycEL@ z@xcSuIg9MRf@GX~h6bnaHjEsEme2&89e!|7ELUO>vPK9*&K z^Pxg(>T6YUVaS0J5wC#2vS{&4`8{)v1RJH{-@D#@DW`nnA%l8&1{w+2)LHscV%5IKJxtDm zz^#6NiX26tJu=tiQgoA=iP_+t=rr``9}qbx`^8*PFlOlU5n( zuU4~_NvyCi&|&|R1xzI`OVKsR>gsI`-)9q;8 z7T1zbe-8qYxP`bFor2Xl*D>kA@pSDhRM_TXK<|2j$xUEIYtr?BuKaZI_WZ@ zQ}tCC;tvl4SDV*$cDKC=H~$rBx=`WlkO?v10N zTV`LmHS)SH{hHIJB!9!-JRxE^_;^%I&6V=sr1+JD9@vPx_G(s6u*RtH>hk&Sc+X;&2#b%k=#H?}i;#5Mr>LiwTA|NuxIzjS=_vV)>K`PG2MjZQ&_*&Komt93p--JqaY%ciE>~%GE*px!yue03Fu{~~n zjpxKViCB&j!1{OY$5dCwL%+jKtsdNee`$nqA(_7-0bMCFmY$i9O2mD7cw=?$Uuoa! z;_WywK(a?+)!7uun)W{~;jI|++6?YkWuWs@DQ<|J#?zD3fnRGHcNJRmY;o zT*Td2ZHY&)&(s`0+3<)7I9uB8AP|WOQ?w0C{I*<38=B9D&EH5oV1_drONt=xD6J@XidXNuV9=B-a-Q*6v($AE88}gP53i&h|cy zB|W&-!$wR(LcX$IL|UaUh#H^HjCmR`^^SB4uT+)32DoEu&5<%3aMOHS#`!^;N$#u_ zenvQ&jDy|Vukdlu0HK0JsxOj6_*M-w8w;P-L?8zOM;n^3^-`TFGMwNr6Ps}IY*R^`~?d|+Ci_Gl|RFCWS zheUG&W}$(7r=RE5Y07ontGeC04@93nl6^6ZZO-DQe?8oEmRU+eEFm&ff%Rk(7+hcY z?@#=|arP|XQ)pJYJj&-26@pFfm9vTUh9_Kgde^dZj<^aLd{^R%ai;*s1!-Ne5j`J< zbxeF0Q(A>*np1?@}X1MT5r%9>sKWBOFKj zxN~Ecd1JX33(8wpb|D#yrW0Iq+!{rtr!+`>6^DHJ3u0QLKF8laR8So|+L;}sbdn*( z+D4vNAoH~nUD!!{Tu6J<&qRz?{9_oN?(L~Kx0qyA>dSEXTxkSK&1>^|_;%hy%z87( zBd?|z24a3o@R=Sd-K&3nVew?Y$D*^`%8~V|`xh7L0O{gQ^6EF!wo`yw-gvd=1m7`2dR3od7I_T4B zwERLKB{EkaQ^qAmWsXy1&h?ZLEWBpwn$O51W!QO!(OS7^&gP!ms zA0F-qd?cf$b^;Gm(}Ls7fctB1!zLaEh9Kwtq{4?qd(9TsM$1zTaO z1qG|-eKC-Ti1Wnn>iI+2!rb&xmS!s*0gv~%tG{$sHwOu*sHoy55ZVnWtjBWpmRfPK zu+PSJcQct?XR1sZz0cO!bbp|k+}?M-&_R?4e8>zBdqtH%Ej{^NeWj^MjLkCY5EtjKraQQLeN- zk~Q?1+oaLaNM0V});5;PFx{xt^Xg#Mq>Wi6g9Gm*DJcnX#DV_)n9lcg9|a{yIKX5QD^j7dW$ zL_807O@go!;~u!wdBl*`WEORJ#K44;31TWmDK)4=DN~}G&=rC9^aKP1KoYUZh46r; zeo)H(IQpgCcT4)y3`fV#@ltE`3r!s~X9^K#4Ut-{Z!w^;BQ_DGqg$V=u~5&GfY@r4 zYS1cZO?^{|eEiVpNRYt#p&>3cl|ZP{xP5=;CiGgp9=}Y@CcS^PTc;ZHIrr;*vFv)4=STMcYN4dt;%1hTzL#^8lQo`4 z^XtL96z$-Li_LDRSsPnhkey$ECU~9QoJi-0dkN933VxA2UTij9q5Wa~@ngv`EHjgI zGa@muDskLGUcPvd&vk1gYpSYe_# z`x^teV`E(2*AKTB|Ni}}37>0rvsP8*NMT;~dAy7N{hNh}Kb2qar@rQcu8vMxW23On z_EKvjARz!uX&b~6+PSv-dQ|-UIboG2;_|RP`hig;!+xSb>i!7aaw&fpT%tTIQ!2oB z%VNIXHeL%~iY_gl?FleUfPpZ@8}0vb5GMez^)`!7kcUTOE^Kc4;r2P~7oF56@CI$> zYcPj}hKpRVjsUUk3P&e+SMfd`gOlQLZLI?kP}M9xUd~`kK+pDjqf;^NHJRTx*o%6f z(xj`iq5Pm_AR@wn)|mA~XcjABCi_S2wfS5Cz!rG~RwJ$9&FzKtn)|0upT0Ke*A!uV zR*Xob6hA&)lSh+?y^)_SQO&~Q)lx(EA55ly4pAwsrgkvBJ~S9o%Ky8;ZPFnDAlC#I zUzZeVQ5ha&@tWe zcW2myNi-CtLf}&v)qSiGfxvw+F4OvkPS7te56Q(4d}$T1+6BKDOL7U^lOUC~iRoAJ zE9Q1~Z2sY;u}3mE`|jlUWMWEVIihZn*!L=hBN}>QUdOIy>x>zAz)1oaz@)*xSzi$D z?slePf#t7gdI4gYPFZcO9gojV=#Pj4mPU4LB-BV5ye$e473dU#NgQ4R^BKelY)woGej)VA2|gLIe+=CA|zH(qJ>`3b1vw z6qV{1fil~vk3&D;Pi<0})%&&C57iBCp>&^?+ipXVG1FA=5>7jx)yfU(wg(cav&L4H zyhi{VW&MiRr7z{NuLHJ^Gn1t~Z#$M&sLaVpWF-{-!{4bXZ5SFs3-EnPxxju&JZpN? z48RY#ZbDC2ZbA|Y9#ff6J@32Ay&}a}oxrQz$zqp{ep@L@rS0Fz^pHw)+$i`0;Bv@6 zguAv)E)>Fuq9(4x+u@F39v&TC*q` zCV7t-sRKjXK)}H{W;yJRl6udfjqA&yExMlDgtNlxZ6+Uy6Nb$wrSt$(HMYas1)!&_ z4U^6{MJ!z)h6*YNpfx@TivKGG8Q!LJ=^*$^TAnv%!>^m?JJ;WZ$Fu@owlkg&Rt0ti ziEG{}ky@@;d<^Vng+75w5(^dEsRHB2wWyQ{<6vV%US0WP7?$0<#H=zc=_&**`*LnbE z-jH6DTsztXxI9myY=+nycG?YmKHx|ZpM(pC=fW1M%_OC$B)su%UUYD3eMQUJ1yc&x zPKBUYLIa~j+W6DGWIK_U4kwH6&$ot?JdWmnS#{F;lIqIpI}WDw4fQjtb`-lN_3_99 zuK!um&vW#1C?fZPuNNAnM^k}!aGqm%-r)sq<8M0&N@!l_v5I8i&KM;N4mFZyK){Dz zc74U!aO7a3lS&6lJbMGrNrK~xSJANEZ@geGoP?=?_!|{2 zFQgP)ASPFY$bKA8_*||0Q1ij(rgKHZy^$>bL+d92EfE>R4z^;>_n~8Ot~CDkD;>FV zdcq$^Y|O~DScF26@S<*ybi?+}(l6p(@{eWmQhM^OE7_QWYb3^<;`5n}nG4CzE|2gM zMk3D+3(cEx=8A!$6ET@$Q#&fz*W@YL|_*lffq92h;`FZnF-8i-JQ zb8;xMVtOC2MCC<*w)^QE4EtqdvT_pt9aJova({CC-hZx1FBDKEBx7ZU35{zreB0pU z-X~dPnga91|M3oc!~^nMfZ1j1{_g6N3y=2$3>^&;gNwIx5Xu* zRMb|)H_U&jvC!E~zUq{v9OBB1pM0W&>S*Jn{%dzKo2BJpa!1&8xVxctKA3sy@jdA7 z^7WjOQ-rbF8r&IArdXP?gFxHcf)rp;g2JP{c>7i4WaZhNPNP|n{4MZ?H^`lX0pUQon|cQ5fEg<%-kSvQP~u{bNa-#fZYz6A>AbY>UY6 z*~N6vbDkyAL1c=HjP#4dIWoX7vwyf4W!Evmj7RIC5OH0DB1iq{9?~w$vkB*^pv8z5 zr)o#=ML?dGaZ0R72NRJkKd*zn2P zTG;z^6@zGKXDA&Lp!>TPj{}1@G=X7b{rzMcLu5>1ngx{<3{Q7gj=0{W78d)oW!^7nG1&nyir>p(v!d&vy^A{OJRvBa4u$6 z&tq$vj&kdUy^sQW7V=oO%SMr$tVx=+vjBF?ZkwX>;Z7h|@PY({_3jb$j~6>ti)(>V z4T8w^Rk$Acb!vz0(QN15BQHfgJw?)GYgfts;BTSDQNdX-P9`Qd%4xOQ z@H1qW@Aa-*Z_F-Z2R9zFg&fRZqtAy;kWM(T^&;R>OF^~J1bCVPp#uhWAKBYXZaSYw zt4unj`dMcxjUpwi&#gn6by_`hffGl_ZM+Wl1!y-OGUTSFho^wj0 z|NN;ClSQFyfQ0jIideq-owq!-)Yxjb{OS^mv?35F<6Lchs9q&+vm2ZttL)v^{~8?p zK!-lunGd${Z`7xE17)25M z)?>k1Xxuv%SkFxqTVaz-1pXVXLC=9AB$dCaQM+|$^Ta@k25e+C42bFd!{uocD8yWb zO=dkPnM0+#4~;$|GMRKVS-=b(2f-AG*K4mqO(`tRhzM+QzT3LXqlKH3&d$zNG{L=z zLPSqGp`ugm&%j3u<#{dH0b0d|2BFjJ%*EF3ZViGt%3bCHv{QWuiFz+XrN5aoP;A=X z!p_7R?7fi^@WD^f-ieM4HOPT5Un@FXk3)zd6Ks=)ti(1O?LEK2c56r;LS`mkN0nQ-9Y(moq z*U>N2U#xjp^(qfTkuM;L6(1U@M4W@0Fd?k8@8AWT;tD~x%7i~aFPX~pmN-$N4TxB9 z*&5j448$Dz1$>ZbE)y0q9Mk>5Jjr{YHNzYA2u4@C!%(ovN@=g-#M%Px8+zN>&KC;u zZ$vb3-+bn3ci+=$`Uz!elmX2qe)!2`achPTHYT1RT>B81?vD?~1$4gN-a2JkFR5yw z{^qY`K~w>UT1voqm4>u5)FtGNsN>=sn8~cqGVG!uVou9#w!q2*oBNArXW|NB01SSc zVKKxeMoLCek!ImGo*ggwK~LV8UAHzhnG)LnEe-0;EBU(t`iAZG^#>q7&63e;XuXS_ zUkYTNfn~*lGcpa7ca>o?h&X6NlWo?}4nUp(x5sRKZCN2KQgKHU^e9B^?{Y+39i z|NW;VdIh>oyq~Xk-7?&85YB=2etfv)e?v$_ndcVTA9cN2i;PM9Lh5*~25~x#5qsBu zlFC_GheQH$-IG6vxOgw@v`ph;^QSuJm(L(}7(J=M|@+-l;<4DI%M~7%~A71W1X3MDN1VU{KP~Z(M#oY!Vksel3jyB z%HDkwE%gt=)ynM)!N>01fh0zCb%%L4S9f}ybilS0rD!2ghGd2P$f28?E}M}|UJk*K zU^yF`-O=nu7$zYs(Ip;OqR~MK*iLR!CGp0_#&NGcY1}?heeEYjZ2~`i%N z5TCa%D0 z9L-@R@Z;yN1CoG0lt$oO94+z~i%O!@aY+m`hm}UHGt{*OII_D4=mTEozs-TtV@1rBCT!kFu1f7o;|AF~VOG^vh z43NUA$jC_VcO6|_T^0j@W4=dgYsP?Woe_B><4Mi@(A94HiTeY)Nbfgsa2ovw=Ku(M zHA86+Yo^6uL}fAC)6>(pHv!MLbs-_}^E~TmXfLBalN?W%?*L9#a|U)Vuv0{l9Con+ z4De3>I%2TIyZJv@GE^S^aX6y_w3>i+uEOv1#eQCOFNpx1EJ%HLP87(3jMcv(9ilNgM$ckpEp>fJgX<2$_1Gq0!K441(4zq_+2!;h$7ueL&eOw zfZYjN&{kQsgi!QU5WOP`rW*Ys=yo{g0u80l%To*|BF$=Zpb@JB^&0X|3>zE!)6CE4 zZ~&v!=bAkZ8o<53a3Ciqr_LV$!6Q;W%P5*JsvVczz0m|@0yYI3248MZ*AS`p0V2Y< z_%5IOT{SCiahrN$V*?DG|Iz%-Hw2Jp^nH2|HT%P}Dg6fCs#_XVdtVSOgGT?Z0c|kL zc)ju0SIO{*ce6iqRpyFs#D4lo4QpS$k@!($mB2`T=bwo94PxB#I<*8NHs$$dMa%t! z+qmtgW^swgz%46C<}O#8g?aCMeVzJ6$f3m5VGZng1xXRO>JNl4K=ep-(>_UkaU4fg|nW87n7L7u;o(PY{s6~+teoHbdG1|Khy%Vv9i)F zQiula@7(-Tj-VeXS77+8&ayeK!EBw{1FiqhmiR0?xgd%mp{OAB(nx2cX$NCeM7%O<#c9)c8w$Z7=jWQ zaeavsfIjU081~p{J$}se_6yp**}cQRS|YXW`Cp#^Uq%R`^}EhLP(wvVzUUjsv?n@X zg$AbGq_ckpF`YUv7(fihR5&goJo{blKCMIYq3cF~O!TTfu&O7;hkS7Fo5N*yx!8Ui zOU(JVS`Fh1*9bwO_ry4bs4EhL^U}TmP{|f`9pn#68gxu14f`uW&+Q%u+aQU>3_gsm z*6yPSk2;|I!;O>7pqV&a@53Y)`gwCLg?*wVatJ_SR5nX~LI{QuhOKp6~CZZeY&&i__7CR0eWgW1nc^c-}?(&$9)OfvtI1@LlMZe(y~YqH}|%yGA^BJy5cOBM4$T4vV>$7?KQ z#*5ibf0d}-E}xsRcKHXZ|K40Dd-<~2^XPqV<7th2fx&m;=Yxl@_bp64UVU10d=h@y3KgOMqwG zYI)d1ihJ6hEa)+zZf|dcd15n~Ek(_*pEctTzk&QZwl<1OSnn-H88zu4`Aoo&?XVB@UELQj$67R-8{UUYonF>4cv z;12ekXz?KT2wMNqFY0yNR*kJJZ>GtpuZw{whS1Q^;9Rn*b=K701)`CCRD$z~LsQCe zq_m#36PtK~gi=M)oh}0EK z@8`UrN6{%$aL!w^1p*e8#O#GEb!nR&x+B9=6x>(iu@{UmYx1zRA`8=fpX1M!`wYajimevUKuVepGX*Kv?M{9Wp{LxunSaqf8uvZ} zGn!qLp5D&kT#dGbF+0lgyBc%FEHXYz*tglxU(tCA`V?@q?pxY&^16o#gw=|%85lFH zBLq5ATHoG)-r3kqJ?cGLv!=eiAjjcnT6rw>y)6!S3kw1lEkW+9A3U$%cQ7SmpuC`l z@6XkQ&D|)dK?i{btAZwj*68Mvvv~baQ;X zpt;g~WTW*+CTpBN<~KNgybOr9Gd#VC`E)4|S^oO%q;!}tk&h#Dmc$8!BQ*?+9&b0p zSdu4hph$hr^1zIlgq)9Pk@&ySdRJu}aC3a~`3oiyTljLEQ!%q_flgbTM$us2<9F?N zeE8#j%&ujiLHk6giyooZr)xnVypAIeXDB#vfhOyR9c5y>ks}=>`!G2@t!rZK!sEzh z-y!w-jnPr{OGAV3?drJD5bagD2*3;^3BvSK z_BNA^YrqTmvZ~f46=Ra=urcQ=&kVUqn;o^C1iXpmWH{(v1+h3S6|lIZk`{ z%(9e^mzpSoTBNhRr{Z^9_Lm2np;dT9Qy*qRtc zR(u~eAmysiEf-3>t@_YN{63;aRX@R*y@2T<6U<@@b+Ewj1A^-s08W5llF(7ZPB&8+ zV{g5Y{u=PyO?);@N*^2$f!qaCAvE zwAC1hWQfVA2}bD8I8g0Dm#vCwXjE6sTX2APos-_vxHc}Yp020Dp;wthLt!JQ7~|d2 z+-x?Nypz|`_CmY9l-aiv99Gy+Ut_myg&Bn@fsW3l3xpZL8>GatKvndJB5)kXJ!ceYs0|o8pct}Zx?tuw{F8&TiN=5Yq89G6JX>Eeet=0A)&>~y8Jsg;#{Mq(HZG#kR>LukwiJ`*v56Lf&r zqJP&0QJ(!XhXJ3}5Mj*%@{Z?Z5oABmH#T?$$Z6AZVjb}SMl(*_*|5{AahBDcF4nhqsJPy{Lyn)fnRLgHdDO;%~83$f4ymnLd(>nPZ!eV3c;6f?7f~BRi-6K#$QXu$5&E{DEYX}#PrcyK!P?e4IilxcYR}$WT0Q6_)|h$ zOq|TD{YOW~kB;55xvF$2Z$wPB!yyqM)K&45lcS;l5=RsD71cAqTc71Ubvc>~Fobfgh9q{_4riAZEaJ~vMi4DzgPWu(+&RMM&wPx324 zG&W!U86UBf$N*w=rFW3=^PvBr~-VZ*v zcEP`*Gd{`*TRQ6AkMVC)Y}sf+X)ck!R3sp>iwWL0&CN07a)g5$4mT>qg$$vWlFCCd zN2ePN`B=VjU;OtFJv2!b0}bGY6M)&v4$YB~k72`F$TS%>x5f zsZT9STW$BP0gwck%-nY!oH7BT%iK1H5HDd%<#XIi!SnfPc8q&H4wINiJuVaTAQYew zxOGZ3viPikAD~rj+6~To7A&1t{QxNvV9VGaf*uXzd2Ej64JSa7(74?<==rg++8S8W z>1?`R#Jwu3t$}U?2Z40HD*#a0cLq`fUDx1@;Tz@^|a-iHVn zw)^UC>1nis*YN`NSyg=`UpfdFjU4f*e3frN#snNQioQWY3G}%f!qNm2^ZS&!M{3d2M34o98r1- zx2Fdmfcf0q9@_%yK-C4Gw7PyAl0`(p*==2OQaH&@Sj_)%oR_de8dfZ-2|)qu5LY= zvs`Z7xHnY-ju(5GK1<12h*t-#WKPoGU+#ClTLhkrASvrF2GFMehi{`5|Wabge*L+o3D30I?AO^z<7YDUY>& z>H`+W>{b_LLd{tLl6|0|>3s%5lI=U*_ZtA8b43%n-PUUH%nb)X6k)Y1gqN51foJQx zQU^b2^Z#T45vzA|`n8tmLaXYWMlFQQ>j^AmIk8p$g;#7Q>6jX!z$JM+%C={#0CAZO zI)CYNAHd;;%H){3I^@Yr^&ID_Lrx9>GXviBZ9`{g5a=Ahf(t`{42B`uFSQWeCr5xz zX07%&9ibPXERz)ac63(ZJ@ftjOG_JZH(v3|#gZFGM_amLLPBbDb(UvPuagyzgULGi zNHx7m!{v^*8HF0*FQ=YHO$xf2&{K!f%XyxIE>Hx5r+`{3-^%G%96yGwt%fAG+*1gnw<%)D-;jUaZ^75Jf7+|3I2JY8y z;~eOcZXU{4ygg-W5PHgcoyt`6R{^8FgJ^sxy{g)0DxzfcLqRE(L=!bJ@xDPlsU9j* z>Vdw!@^E;OrcLZY>IkkLx zS+X?(hdg<9iIO@u*9hhE01+|LhrLQ--FSdqJIc2;N0$R(;YynmQezaQ8zNU}1KSZSvl?z0u zq~5=OUo=`^RH#{@UxVNY&!E0WDDcXL_{n-O1(_O*PU2I35b}4pw^7pKWausKuL|qx z_)`}I!IF}{%WB)AL1F0qG~wiC4L^?Qg@uidt-Ds@&5{$!2hpK5MH2o2Qff9?NqU)_ zczl)#L*JF|3ZcIl*JP8tVJRRms?!81F5Up`G&uW-$`1E=3D~Vw`QV(qY!W#cgB0mC zeI;U8IcOxR#|jLN&wUx;zJ(ElBlI=?k7((fIkb^Sh4wS3cRQa$JJ3vMi;)m1(&2xF zGK#ZaFI%6zUQUu}VE?gFq!at$67gB*RxT_H3)0HLJ6!W5DuxnoexAaoyT(C%Pi{!@b z<`j?k$6)qsF6MuKM?>R1_=5y(o=1MGGrhg~Ot0^VaVo8LJ-x#wshT)TS>|~7`NOWt z4{3Os8xgq4j$T#(Z|8h2>^bTykI!4ho}MuJ@=2-5`U&(NF`1o`i4QSA2H-r&fAvl{ zd&N1nUR3Y`3q_VJ95IPORs^&Q;rPViI{A`*{p5&_qZjaG1G-^@Jv|zz-7Y;uFfg|( zNM`jqSVySy-AGAycXz{V@16T^=Kh#9v)23tzK;|8?DM`) zz1CHn@OHsrAqYgA18ew~i*FV1${1KDmC zUsk)5Vb7#eB47Hc?aa;V>)3e7tg~_u7AO@w zOi7ijq0!}IGgS`L9J)`m;>tAT>(a89htp?{gFY){TXyB8f`K9H-yJD}p2?L!v_3up zB-CQ19{S)t$JwNM2Qz?0Dmd<-`cr^gE{rT^zw4*WZSgubwE{&+7q5apNIl3=Ho7y? z?Vdb8WUn9->ZBQ_7UoI+IrO~O;o!c+)am{s?t>OLyArx<;J(^VD;0dy4JeN-X}Nl* zhY($QYS{H^ebF^k{fmV7cJ3185=(toR&(KlK~#aRy9dde zHx(Hdvj*nU+n0A<-lOxqFw*L^Y9&YW`uADI#d4~u&#&LmNHCg2A`3c_L&tjEyS@s0 zoaIO?e-sNxqCB75Q4xkQG9;e_27s8NppZaed>QeX+#++)K=N=-qv|)b2==k*LHV)# zOQ=75qlx*?Fyd6af7aLIe$4Ym#0>pRAgiOT{WT%#4MeELy30R5FPU*+#8_au*abUs z&lZOYw>=*o*qwLDChWO9y%8&zNnuqqn!-ZO&HcN5#OKwEALoFp({6 z0oId3Ksl-+<1GRkTOx}lNcm8bf7Oi?PlI~AMYNC35%El7t^2Z#sI}Sf0zk8etAq3X z1dUT=IhYZAyBr`Q569>_4?X|-%I+TmTY@R2#IfY>7QRNdWE#Ab6w|4iZ$iT7@aBWy zt*@gZ{>mKGZ9vRX*B4nVK&0U6TqH86q9;r`sNPB8mkpl(i$6ue+d&oUR$iID^LNt9#j7={^9H%lDzp}Ej z_ZwSCbuOrZ4H{kpstF~sp!%mIV!M+;jfuILPg?CZ_D%j5lxG|04J4?C+vACf%hqOh zj+Q{ypK=&uh&W6i@I`;)yEp&an+4A>`kC)FQZg9-p*MY-H zx;Eh^*Zsxju*}xh=@{Ox=-q}?GI_cy^>#=H%8#g`t66)0e|gQ=&i6$=5X~CaeKGGJ z)#oxB7>IwaBeO<8?_SOLF>7X5VX2lN$c`rjOJC@J-xqBV9Q z)JT|Awb(ZlJ7FdvjWzbGBHZ{wNKY{5+RAV7dBPhh-+Z-T@Rdj0WN2|TL3aTi&DU@y z8{bcIZen6@A_?c<4?f8D2UW8Yie3c{_Ci*YwZPJnrAyb-w-6#}%MMCltQ8owz;7LnHdqg#OW55HF%37xnYbrpfqumZfGSMIOT2{vj0Ex}V+_h^=szBzl?@ z6cw16S$YNAyut40MF}xp-Bb`7kXi zR>zTScaV0^qXVmg0%@wh0qEzj1mNYB??3%pP{pE%R8$K=JsG9xdt_)xld$JHNgNnU z7&qjvi042%A`?BqQC+aRO!Rt1@as#BGEIpOFA5hwqTqzZ-=eV`@9LqCN=0fD>zjYl z`787vqw(EBy1nn#Ub-%<3g1M=nR$ib2scixb5k(qQKdtDAJQ67Ss5ddG$*HFT;%_z z)a?*YPB64rNvN=?N~gnUy{52Xt41S^k7*|j-ek}c{;NUHhv zx5bDqAVMFz!*2IH5%JD#r<+*huR9`@;xHb!ARxl6)Dz}A)m)sNRT;FbNfK$lx0R?C z0dxzam5R6+DC(`pN7ST0xvPVY9?```-Ad%YjR}+Dq_BQbLurZaz6$8Vt~lTWy$}$b zg}l@Kh~T{Q=bv0nocQrS)E{OPm@jen&}BcnP!%prY?mFmF<*J2*ZTcHEHC-$}$8 zi_!sHIOxJJ$%N@dsfndHbJ_hEKRp3q03r?}^&hSRWW5X!HPh78#B2R`3rNB?s_N}m z4RR%5^kjwDC|W2)LM(yj#OOV|A~#5z0R;e%qi6vv^zr8SaXVr|t=1vu%hg`M*8Srn zleg zpQzG-vLrWmVQ!AS&{|`9S!LTc9|&T0oxRuf8srE$#kb9qc4f>j2+$nz0t;x-ewuN| zapvedhLPR4f%fL}N()1lIaKT3>&va^&th;JfV5ix#Jb?4KGA&|$Kd7{iPLQJz8|j# z!~q-{B|zT*Op%^OB@+-1t#N$zU=i^X5RB0$4iz;H|7(bNnnDBqF~Ip<5fP17sVss#~s1;`r+ zJZT?~!zxhab3w22WD2YUy-XP$p}XyAGTlK49b>J|yV1G1x$N!d^8D!J&5dP3$1D%H2 zn~|iq62?tbM`7D&%|S|1F93jmh9Ts&9k5FVV9xdaB%B+B1TyF>R8OWs3LSQ`BL-y- zSahECD#o?CC1`mL?zAzXi|Q~R{tF9Wzdt%-o~MWil8eeIAx{sV2m^9l;Ar$1h@}XJ z|6~x`VmJuat z@eU6_+aBIn0!}{!D!h&VSW(Ni`QCk!T0nBs@~$U{y|7UQqdWlC_$ZV{5Ilj5=HOic z&uT&p9ffY19sZuN9Iwc~sE7{oyOkQgyT9GW)>g0zChcqv;0KJgpe1qOh4TtKJycQ$ z(7K7yvAgoPm;QhY7aR(F?mt&7@l%z*?d&~zbBCrZ*_hLNZ*fDj3eQZ-Aa zN*@T5o9;O3_oorUc16mhhgu}<#27X#$}+#06E`y}m_oox)6>Y&2CEe7hF+nr^dkDe zf+z&pIHsut_LnER$lC*;ZOI`7C%7Hd{4w{dclav79X7tplmXaswThSKQoYl{rHWw_ z+S?#f#GiC=v^X~SLWTq{=4Fz0=|cl@qCT--qjl}dEZ12ofl~)GZ+t;)uQ<9NrI{l- zbllP&HF5RK#-{6Y61O<|ATq#2ZD9AQi9=;%p5Dp>h?rvB76nU(TbD}!0S;1>V}b(! z)7$_$RY61ytEqYC;<8~{2L25Ge*&>ss8|Wb1Mo8FQt6N*Gjr72!QDMQ+`PQ3w6y5} z?v)V(V6XHh$YnZoC{Jtj1n@Q$JY|$Wtj&)d%l?J@W(O^Rf{*7gix(h_t4$b9*rt63 zQu*)^N(_olaqzFz(9#(R%cCV371->fb#$~PyYo~{`qTo+f5Js!E8?Ie??;jbcLUAb zLLV=Hy}@xX!h&C@_q)sRKipS*D@kjt`MN*bG4}7f5Yp)g8PXXi;l4b^5gn2B=ntHL zpTZN;rWYA}$Bq{9BeAhF#Umn;cY_lzU>v8OV4o05dl|PDK}{lmz{Dakw@Df8Q7d|@ z03SGxAth;S(;+#LU(q?y?!bPJRDpjjuCpD7pMDWU9#6zaYZbKDI{|x*GmD-;-PO63 z&_Fc-h=N!&BzYzhKNU&z;ACI~4GRmo_}w!8aK#y#rvO~ma9KD%sW>US1_sb;Elo@; zY)x#<%%m*f#rKf#@vN-C?U~VD|4i$`aJFZIp*La( z9Xc1OngKZ~a;t1Q4L>7w@9vPt&OTiTXDT_x1*9LIt-+#bH)Fp zSz5YNN4eVa zO=pC-es8DOvf3;{up^S9*@nYb-6x)NH;#5~R-`MtnDwtZiCc2R?~ zZCPf&)o9hL?cUf}vD0=b(#|b2KRV%Nxih|f?|XBoK6&C^f&Flo+(pu!CFiT%8v zxrXRyR3SvV#7E0FUb9G#G~4nL(eE?&OM-#`Vwe_&IpgUtYjAE5tYNsOKEA}7KXGWU z#Sr5`$weh8S>>piEgyNd-#(Z^Eo#}w&lyk)+z5C79K(LtFdoPq7ujO2Jw9#n-SQY zh`mn{6Xpp!4=U|7n_%;*UtSgc)lGEEC4R{0D-d%slF?ehCGay85JKlFUA>aZ(lbL! zN;0{RHJqV)bJte$*2~9;PAYsz%ebi?)l|60e(g0SyD5Iz-eRTF4U4=Mv}$XsiSmgP zjv*FAi$Ov1ZT<3a+;=ZRvQT^>7Y;)mq!vVe6`jR`8cfut&;M0igG%0p2`x=VYwOH` zsL_9rC3?X91(&r^I?T`T%R_h8^bTtyl3lxEXCL*MxTdVjE{x)lL3w*cPCQ{Ka;JEV2Ta6HuY}$x&PBwY7N5;8#7cb92+3|L zI7YUtGTZOMgv;8#uVh&ECA+8->TWYYN5NmM*eCM{Dj-ir&Qn5W`89j|>FB}k{+VtM z*9|?Pss|kqzoxsphN9ij2n5>k7{c(fwgbO!o0Eys({;C*Lf=^KSO{aRtr>2owQbJ> zrTp6$HeT+5fuWhgRWoi6+8BSc!>ifH%;9Xsz0ZdAZa1A=Sx=KU=Q%Rfs^6PplKBM> z0>*>e&pg|G7`n;;2fAOzz0(SpMd?{(^T54JmR^Eq8xJhK~)PPR5M^n>{F3G6! z<1Mm{e33TsR4kb=4ZNs`h=(g&$lw5c@GM`%6aU++>|vh;lK=071pl}D)PFF`3D8{6 zeSOu%@VF#{CML)mo7Ge+9kB#NTv%KdpGK`d8wbig?zz^Q$jNAhi>oz+N^U5!q!#iF zf;WZr6&*2#F>LPYPa)owq3ASS&Oq|Nv|qkC28UTy5sJB0dz)*6pHM!aqs|c;cYaig z&&0&&E4TzwU?7tLdKP9@*67o(F)`V^b`K!T4WA+e=vh!m1Z=y45ty%YS2>@7jvS~O zf`Vvf|8m0-!em{6#)SFngf^dvDG2y5F{7ho&f}qf0a{6Hh7CY5m0fHzCDN;0?2PEn zy#mA;@HrKFjqw;dbwK$7h$u-xpb+T&82FD;G-$aS$#~1yK5d`&dEsY?&aVUDn=oz{ z1U0P3U*NJgeiu0^KE%E(g!=unY1MWOI5x?7rSE*X4(naMzHGp(y9#<50I-@5#I7Gq zGr76BMUJO$_A9Nue#Ppqvm6`!7zhNCK=nL6ImxA4Z#%U5G3|$wOC6ul=VAiMuQ6ZG z4Fw;aj=!gTYLfBw$UlLso&3cm06YmGQ8gI=(V+fJqE`WC@*O}&rSl#e%Gt`v$pLSA zD9U35+5OgF0?5>DYWCu`TQ~w2syH2Zpn6^auW0%Roc7@Q{z+l;0~9S3BxIt!3lQD` zvTb`b6An%?BQM`H5v@ZLeD6Qqah8zh?uy$RGp1ZmP3N&W z4;|W);d}V|K8X>S2%BZ1(q&^vw;#{39f*x&D7M*4yuH2eUI4e{elW%F_X0V$TA_Oj zs{V^-U{?Ta0t@JRLJ)DrvPBJRObWsGUsOQ?{s4_krPho=%W@zYc+fZyR@?nzuCn*MW6?vJ6KATB1sDZv&v<;-Fq%1QqpuR7U~>YZqjHeN`13YqT&k zMQjb|&2G;I!t}m<`-XW_rVw*h0Praw*O%1DM-=vXNc0Q&Ns$2VWVnJ%=w{%6_qJf^ zA2-*pVM-8Qh$*hl!w{FAK$o^*mLv8rq~LpSeOGrELorJ+7hsxW6AOSBG0#QW6J&@W zJ)%a1?;|V+Q!z>`zg4nC@oDx-vi@PqEIwr@6$z}8;Bd#MRK#=12O#H2fTngZm4hmM z12_PbuhYtPpmPU+NL>Ag0)dy8ml=H=Ix%`mry zqJ)AX7Z7~ny~k4}SXKG#8NgbQ4t#%x_*!jv(B+Tb1@U`<$AJY4jL+OkASe0 zP!Dq*J31u999%z&&bd&jWENdmC1^XKn^XlZF+@C80JWo0QBnpg1pNRRqDerLgmKSq zJ2(E1&o5Sn(0%~=Z_gXpJeKW|47k-~z~O*|G+yzK`*r|EV>VFd*c|4 z-IF)KOag53xGV=>k!4?i)kkYR`k+={eieW zI+bJ^^ZIfOB=7)v9)zT$r3Jq#ARv&Z0nR#($3|n0u(%VxCz|;Fx0A^?T$U!Q2?;g4 zy=U-P022jVro_D1EVxwrJ3GA2JIs1!AR(3h1#CchR3W)4rD_Z=kbES+;1)qRiqCo1 zSZ^{P@da za9f@*r=uP<*rLr~<{<9~XbQ>L;2T7Rz< zgL}QdSV0La&AZ=xz872X=J0twO&v_z7^zPKl48)+82G5Vrf*I~*ar$3aKtPg8$wwOBU#$89`DJ1+Qx&>+2X4TVcGSar)3GJh z`Z0xCRaKSZ9RN2Zw9Y{4_<$HbA9z(sa>Ak#8yOjOxX8SXppvObSzq#v_0+)ou_%o# z1_dV+f*#e9r!7u?j;ST}z zLtB|bC(%EEnW_l9{&vQ)vzhQ1>9^j5vOXNsDS3m52)XdIw6yfD@We+LQ!HwVQ*Vev zHNsStmmQILfj$Q^X&ph*yw@*M_9`(*HT932Z`V}Ni*}YZ&%JD0r@IsQlkyY2k$eAF zE4u2t@sO@>0Wfr{RRZ#g3X#VC#ZQRKwFwE6fCxp86W>RA#4u)2ACkWd8oGRDpO2;3 zyRV7*jz?$~xR1f80M^yL>z%9~N)~oIp_A?tj;v&TnSxL)lVs?XgMtJ5RPb|Y(pggK zpXlL$S74v>wp?jBxtI#I%BEI}0iusJT=q)i`kbBO2pM^4x`?25l}f4xxfa~8+*dRt zBxxch@?C&S2HF8l!p&q+E0cODJ#lFLkeTPF9#O=RcO85E@?~8dCgb)UV$#Q9ttieN ztD+Rn?@-WD!=p<|(t?7ZRwsdefQw7pGQ1u{nSTW?ge+0Nb>L>$JHI(y4OYZnEMx2p8Au$0SG-Iw2d(*DBtDs$rJ}{SmX&>?@XV9a2Al7fkJx%#8drSw31+M zLK2#!^}48zMH(k36uv}9LAC24Zz=`*CnJ?U!-QeR^GKqf1vsbEc_2hQmFx#w zu?q^ge`4wzkeM=ZeGD*TfV771=OO7dyRjS)d}q+g+WU|#dfVN3rjcX9_X4uHHu`%e z#f2T8K^cVT$o3QNDXcE&4SGWR4JO$br{!n?#V_e^11F>#%b{<73{5`2mmf76>rgRB|yA9)DLPLv~#&&%*KGh zM?bs2L6*BbaA`5G@OhWd%AfR5oTwfc$r5fY*%MRyY&Fw7_*6lZ^Vy9^)cZhIYK*R2 z)_0I+@kT)%Je%jt4QIthGc!6NFql$w>*2C^xpfm1j|ks!bGU%DVisWSDr&{6#Sr15 z6!iXU>38gNw(Rih29z1&d21G23}>v6Z32{w4j85;)GZcyV&J`t4jr;O0jeQ@qK#(U zwu8rADUFjrFc&*KE%$?A6E%}PNM{sV_Xal;z;K-gj-J5Zv+txMW-nf5904T4R3>z6DIU+?^U+N{3I zKN(;*4)kwBJaiW^2J-SC#^^JaJlHPH5X3m))>E3v*oLD7J+w`Uq% zBPEPF(*a|S&4GK(zj*itkh^6H-k`LNdowp&9g2==@>eB|x~6@yTzeA4{ZKuiiqg>U z5t$87Odvp=m5}!tmutAM*cv{38QyU4oA;k-em-0|(TfX@p#PUL)UawLIsmx^|1byz zcjH{7^Ts^XSjUV&hQqiV*a?H)L6~ap{V|3Cmj>P#TMLD`2IKt;y%(f;=PS zI5}FRiWq5s+#n2K8E*7Ahs-MgQh)ypM0vytJY49&%QI6@!iN4p-6d|SbKYfz?py1i z5~0+41qu6C2h&`foQ&ihew~3ZOK6bcb9T#3Fud%2Jm?%DR~?lTIfUe(E*s#U+5x7N zemC2I@`EB2aydJh%TFpyp4fv>xmJm;yo9e1A0s}o)7Eh-}N0;GMwB#Ql@1v73> z<=&7(4iBkn2N)iZ8g8l+q{SV0`M`O=-V~Z#605`4Do58u5)#Dm0?t{**sQZtHtmn6Fvg{x1j;O(#rWVipU`*Pe^o>o%msxZ4y&o!-AoIj@^*) z@Iz7izx$V>Ev_g@$*~7!;e6&mRdxO$!}tE!tLJBXIJWYfH{W|2{BlOaHb`6EI~aQ3 zD^mEo%e|(Xy}~*iWdcL7(nKJK>3^T(t}}>nDxLykLtL{6>N#EAMWb;%FOW{aBGRXa znh@xFnEXacRX^@)f7f;NxSv%vsc&+iLxd7H$9ft?MmlgJGTs z`S31xU(q&yManT(V<;;H=hAS%8<3lfe8ghNqR!mi43FZtdx)b+_IQlsE?e#I>fh+8L*hX zR<$O;;hzum(Bl{I(C5tw(EkC-qE>9l{2$Fe`rp7Bd#ZeaK|$U=q<)V#>MfU-MM~*o z4`=JGW1F&!ny}M5R|T{J0uO))1ml=6nh^W_BJj>&RwX3@-`eY)?%wLPT-dFpuw}fh zKtn?cV!eBKpwE6enxAa-as#oH9~(Oo9UcIU<#xKz8UMzyOKk-v&+V+kAX#hlyNK)Y z0nUwX&T}2Lz9&Wrbn8g}H84tCff*m=>as196#7j-U5QUX;DdrJbm+sVl+}W0dEfp; zyI6Vsqk*}afhu1n4S{iPE+ZDLa*h#23>gy|7If!Cj6)6^AU?%yZI3R@P(}(NV{P83*bHt;Gp@M8>Y&&7vrG*&uk*Qj^UvFhir<;~l=TdYQTs2n%tF-`Lm~&EkJP+Q*`zGNoQzmaMmM z^6+T5-A@4BJwZllBE5F0gYnqmY-Mn0sCv0tTJ|sV$xtBqFn4xoa^9V36?6yrnK>rr zL&>g^m0G-zCFoIsN>3-TGFdWC3;fDe(xaXFvF~EGgDbsy+=F*xeXJ; zrettCq+Uv~oi0zuM@_AC(t^XJxlRyVA_lap;iubB&6{OZJ+-vwPyk~t>FaL|Ac zUuBE8zCJ30m+NM~nHC&IRUiRebU*sD#ljoY>=k3+^FEo;XX6fI-Q_$A z^2t!x_bNxajF(0CS3uC^wN1{@m6WWsFvzY{tkI504gk}cn%&_9C#*mut46Dg9re!h zqdU;afBZ046hk(Cr2E_2(#F#LCi3k~PfwqWl*~mGNP<9E%@SmKLCV9UjQ9A{OeqRXzc**nuJ#nSVj-_)xsgTJ>v?D7u*T6bv&p4Or*$Xla5h%6s}-9S zbBHbP>TaXR)X6$1_B`{9hn$;4kcqXy>+~cOG6eDW;rQU6KSy&D-3s+;pYDK%yi}uX zqE3@6^*eo!;qb61`I`D=a)qG#aexC3HErkL{ka+>?!BSy2!)eDMp3>v!todf{6TRv z3loz(P9314G$A)M)O}(RAsk}gh&bW3ox;{<6)^LH3j+x*B?jvsdCu2OcKkx=?;nWn-_!sFJ+VRZ1l&=;*Hh53JIXJ3Gva6)#(vtvzc zw3e(a-A@I_z75LiNMD1VWl1*-^_Z$?2DbSR>S!MdU_VZL-`tD- zywX65eHE4Qz(_>F_+7RQQunuQZGA1PPC4Hn%_MN?ShtzDO>&{n+5aF@xYlB8_Yaqu$rw;TE zKixZdd8WKGY!|0uAv5fl`lGPzyKWp6r5I}=6sN0qok{3Ut6yUDIBOwX;9?5O~7^yRNmMS-!2=qLNX5LmO)c7ps z|N7NbcD$;o!OU_2qMw?Z`|9ynLUN*xfavhyZcl!_;>_-+q)^CE_Zo`0Mw8oN$vAlo z34dgCt9W-`b#uM_VwZcD&*D$x<45;X2gKV)qo70vO$fJ;XV1j{!UCf1^pKE{w3xuw z5yE}6+*+m^WANk0kFMXI%Z_obrz>qdTs)GC8n<^cp`jC^uwB2>PDQx49&y$PLPA3* zj2%A3zQHD@R7DR5ZBclP>=9Jqe48m=0g6FnsB3)hV0W*lU(QA6*C{d1kaWP3PE;y~ zO_BIlDaUYGB>uv3F+4z(Y(Ry8`&$l#1h!bvJtRR|=L!KH2wulA` z_i0*V1ii`W<9#f{yWMc+taFhqEgfw)bL?Sb>11P>-xF-ySThZEF!E3Gx9k6 z8{f)_3H11Wvf~y#e>IW2A|pBfssl|?#$v*6?;eVXZy zg5pHq)GskQ*`md__T@S?$UJ`r2N{aI>5cpAD=HT2H!*xs$k`zdjjl_#@!1AqIaEpR zl>_;V6$BjGZKf;T4|iPUw2YOMTpACe%MJ2pa-kkIs`PO2@UjU$#nCXinRuALwh1dE z2J>?c9YdnVyBrPVmWsx!_0-LQ{v=LrjvU{=SYI~T2|1sb-(~G!JK`)5ok<*)aR~#D z-|@-nI{g>7kO=dSyx#Y=i%keVf_kFK=nXqr%Yu3Nc_51Ca3-Zug@4Iusol$Rq!j0z zS)^GnvWHyh=5+b^@DhF^6+}UPYN(ABGNm%bk9XZ3RJ~tA=ZmD-#*U)I7*wid9l$2S zGNq5nEfsw8{L$N+SSN1hP)bS)f7tk9R-J+vKh;R*hd8cs2CJ0CONo-FxNC8Ldp+lq_W2Fsx)%Ei1OlOjgz^Y+w4oA=a?&YR6YNY6cZyHAIg7T zfH)|I-rqB&mU$RvOp+S5V8#|7Ch&&GvYl3Gy%WYTRE{B@r_VT1SwZ}$(B=(pZFQ8m za>~&{DF0jx1UW5-@c}$4;zF&bWI$lZxM_D(m;X=LX`}Y{p|PZPYEU_LOuFjHrK3sw zRGMs}4Sr}A(HF;wWQGUB7(;{A<9AaFa6ECmFw;h*q~F!3df#2ji#ZkPH;U5|J>dzg zuVd_UnRV=0fAWw-$h-k!QqYCcw%Ev`KZMLg(}0pcLE3(pt%L&;hVi_+3nv|hHzRsIs$wJrk{o+Qd~-VW@r0( zn#-E54XGV>t{CK*$}nlZKYfiHjvy&rheVtH`#3Y&DJaD%k zLkeZW3&dKK-J;TeVF^Q61|og7L3=<@d&5f1I6;%i$nU=6()WYqijw%$9`73k#(&#!T?ozRO-1v|S-R z+*Ig=yk{0MsOSC~APzENoxw27@AVCHS|Ldt>a3^U#|KGZI-rT}5{0P}uWXUae%mlK zCxLZe0+83`WqFT4Z`XEIcnaW<_ERh?RjB+fcLxribUiWu_SRO(LPm8^Jk)wlK2%p% zA20Iav8iB=TmUV1@dv((jyB;NR8)FA+ta1CUx}Iobe5Li!<*rJ?~k^&^h)0I;ENn+ zOv@~;)LB~2JK^*k3gsw|W}f>#Tp_qrX`N$+L|`YW{mLPtrS0n!7iXN+S8AqKkCgPh z-pR?yF_lG2p;xIjEvm%b!7chgZ2Ac|PNPwz&VOk7Yv(YY@2|s|D($k-Ya7iw21YQv zh2`nu-nkC90tuUWAVm>&rAVnO-gdcOnDEc9vXuO-2%>lT2in^g!-z3Vx&7ZOsNpP!ePZytef+kkDA%!;hEH#U!suF z<*Fuf1iYW5A`Re$oevvD2q-;GIXWk;mOzC@L>%X>fqnHR+lmF(>7+qHROw zt#RGJUNDl*q6>7_mw5Ey$9sEwKtrporNjH_FY*YVE-!%HvN-Mu@vH{XVQi_gE`axx z84NX?ArWb42WoKMV*#L%A`_^NDbZFZ3PGJ7$ZpT+I$)LUxSkfG?UwvtS)3e%Qp&0awbSY5%1=}NWt_FdRc;jEa2QKj z1p{trn#=MNxZ3lF)XNaoRi_AG;$@_QY zE;Om)^zQ~JWXdJ@Wu6#}V!h6c!C7HRxbPWZL(CHPB0SxnS_ncC zV5ekesaR?EVv?^DdTwBd53}(FQzSOn zr}O7xYmnPE6d{*Cto}cjTR>q*bxsY8;XCDR#+mA-`#_}&6s`gk88!hXV;b(C->WY0 zqoxY8CU%SUe6`QIr&)w6?iWEXmwhf01vi++rCIXVyz#mc6D#4g>@PlH4gt}nr4&z8 zOoVDmeRgB%fiXrIW*4rTlj5uIb8xc1GAk0qNKAf9`P)*Imt^L2HsWsGw$!8fWuqnu zHs#DUi8j=;FfU3&0;~^{bazq`wG`O57ohG~RRYs4@#U*me?2a(-TI zV$f6cOtQy^cL>K+2p#I5KvsiVeqr)7gZMr7+g&rYWifX3+>0>Q9t`@8_!bGGKb)Q1 z0(v2^hF4ay{I(AtQ{7=>wx>HfJH_u=2MEJ|$hLXQqLQrbK*5Csx=>0)Vv36#&9&hK zP`^``7wg|1Q6(C*bR=hlSoH0$XBluw^>f%sOP4NCuN-5}adI^=|EbrT9%SrB4+&|{ zZocl-?)4b@&hS9{@?w~HB<9BxOMFxZx*oqg(r|I{z^7?*Q{k^#^t$E?J+mtJ)%a3i ze(&ZGj{N$)#}{0m#~l<=70;wo@B2$K5tldbXA7*IQ3jRo`O?MhN@jB=YN*1thlgZv znblGsksLH9ZpohuhZBRMyV=)S5$esEJS>O)D2;(pv}sP;zwMWi5&R#=b5yWUC}1Ot za(oS19HL`}9ZIK*JQO8FiqBYggiHN)oo0}?pP?+?@+tK z@OOiF4wI=YAFc@Sb3?HWw=?8W{&QIQgDdm@syHg3T!%+~x>#({ltpiUTO!Bjc04!g zhgE?DT&Yj8=lBN8tu}B#`-&hZ-8E4n|NDER6W21I`_9iZEIuI1f(J7w`>J~|2?%N& zpLGCX4!|~HZ-=p-p2XJ+%iZV-ApHzOBLVhu7bhpwGe6g(xj0Zc0lt@%%(0`1r_5=P zHZt}Zd1`j|yfsSG)y4}PeR)MiP*9K(v7i&MC0iw6h4@h6o%Gsm9(emEE( z;7+!V(69r3T;G%OTiyGyZptny(Ck#KR@Ycgm<(b0rKP5tjefre9hWagc7M#>Sfzu8 zo^AOMK{nv6E;d5QqCFpX6!r9unC`DsAzog<6x9t){9Y_c4)wa&k`_L<&|v8P{$BT! zh3>yYh!5&5QVgl0xfB$Pep~O+gF!yBjy@Rka=)a#_jFJFImc=O6A%b*d3e{*fePIsq^>COBFdHUx^P-zneiohNP8fW4Ha!* zd_4BmVoL0aOW8KO$buYRLJaL+ifW(%36V1N`4cv=E0!NcUqApFd{}=Kmrq&7IfBmR zMQ=oPHM1;@So%P z{|@Ynxhq_(nw%~HUt%x_5U_Q0j(KXMc~HLT$EU! z1@*qA{0CDCTU1okpkYqzg-0nkSFxG$`Ee_Z7G?Z;<+qy&pm5kIFQlP@ivKSx;C)Jx zy&wg>E|}iEeFcvtiQp_o<8cnnf(lR8Rm1P6FA+liV&?LoEu6me&HVM6h#mi%5w&c0 zCs->q9*!Xd**~Q+zUCGf=yjJ5K%c|qW%J91q_y!FQ8%Nl< zy0Z$p!|&Sw87zZg#w`JkEdlSo2WYnh5XeI!=jBnR8E`}*aYdpf9m0sCGazGAk9IJQ zj>qrg_#na^A>}ua2i?&F1?MNql@9Kiaa>;bNvtnesNHuC`E!762%mai9_RBt0A>>I z0UOo`3Q61fSm$@9!&{ zWZM0LFop>o_p~?X;fjrVp$bo_m}j~X0W;E`M*zBjI4SCjI=jG4gfPv-MvF zO>AOXjOp+njXTp4f3%mFjg5izzhBQ{*~GIA>!t!uzJ87R`Yjq!DKFxcx4_>)Z|QB! z;0Q`v+>{uew>`0po$?Fg*5TtXtWS@kZGT)g`@X6RzRspS8;H%HW-XVG#O=C`2vmEM zC&06|`8q+hJ#(y`hiRgvilAfW=Ppg3ADkDGI14i4D|;BMdLPnGU*0x$-R9?A*nD~| z7(2~0V6TRch}k7;Z}H--%Q#HxmJfzPt;F}XT^3~=!}%Nr%}k*2{#j9L?>8D7>335G zaS&(|;&VB~iytg%>yRx?=bIQMU_wcC!@5sGKwAEk^vh>Q#2HwNG^3xbsS~Z^i~oMn zn+zyRlZsSiiBuf9zj;PlJhh_xxIiZ(*;ux9zpUDf=Q6yloxiAd`tsU*tX=ZFLemPF zrKjq74(&Lk&&epXCD~#6f)Q_)J<nLl}bW$ zLSz%Ce5i$qgDMuCETrp-8HTz-kYg=;p|mSKJrsXom-iILRy_q zGgnD*FyhPPu60O?!opxLKHH5nq-UuMBv!tCgIBU((bcHWm3fg^KEJ-|igj3j?(+s(^w<|OTDpSl3A-os<)iSqUSY3J*0wBM>wYn-s(y~61I-G^=Hv-*IHnPc8D zug9s_j^B;nraF3~6#uLUwQsk&I@J*BkJ)B_vNHSFV3L8!)5Ns8SAW|lXvqr;*XGQo zQe|=(NBaX;b~Xx#NlIqiDBD_nZF?iacGFSxx{;Ad_2<1DUtf*nEgf0Z$|jM+{(N3A zBdmNgJUlSBH9F6-&)J+*IlEe6L=(cDvtlUnb1!o>)3)*3wAI^Q-quYv2BFrM!+uR6 z;k_-DADxCP4Gk%k^Nvj+zo06~2(KpobnPVH!pdh{0ZsVGh7P{$hWJ@nl&UTr2(Ez9 zx!IbjIeE=m7vw)RSQ2h^{ri}5Yxmc@V#-8qR6-pG^M|)!R_aIBFHDJowcAId`@U$S z3qOW63a7QU?+7bZ+XkyClNQcnV1|Dqqdk9nSz>TU*rrpgK2jK|sBOTxxNS@voR>@v zh++MrvF~9#Mm&X%4b6)AbWQS?nD7dLW%V@ccU+Bi%_n_jdIc268V*ahr$gt>*nj}z zK^KRP=$rkgKf=;N=nof~nBJ>2@aj~ZPZAeb>mN+HPbTLDSon%_2j#Ko6yclJDl zfd0|fySq3~U2yt{`+4LtLr%!8Pe#_qdo##+qFR1>-`V>0secxgehJ=gf1|CD#^zRH z1m45xdg&x>PP^32-erR9i7anHf9|cO#v(HEN^66&*@%N$TRoL~u5)*NavE%umxuXS z%B?So7n|ETeY3swY6luykM-RRgVCy=kKSdH4W|)}&UIF=@aCo;!oVgXhVg1O2JxL6 z_-%yx;gZwbkJMs#HJ6u))n~4~`$LyBJM#?V*S{}M<;c;Vh>6yrQ6}nwyNr4$pQa05 zo%Z&}hGjxebjDHgmcI13{oH+iy_TfHSk+uUj>Qjgii!H#M;mGB&w@I&Dv#~kxo&e5)j!dBB;yfrL$=9-@J!6G*W5U z20uAE)c89%w9-?pCY!l*_`CekX3o&Xa50=dyb`zfZ`UoF%~h=(OKp|wt@n(WUVk>u zy{Pc9Cza73jBh0uv2YuVaoWHWw^Kf8X5=Plb8^Aqo?5izt$(zPsU}lr6gg=&wg)Or zBdZ=Wl7W!Df$9Z)i`;yNQC&n_Dp4_hh%Q$|bu0ZG1yx(EMe`N*LJR?6+wP{484C#u zPedxo_GN)zxE3LN>YAu5rAwd6W#4JxR-8LVvDSf z<9~fQZ+?3F=kWM)ao!2n{5^ErE!Xc_Hm!R86W#V%N+=6*Z}pmaxu@Tb1K$*6!Y8@} zNRTN%W6hg8fSIN6`7yebkUuOml%cJfp4wn4iFKYh*5XVnHx`yYGAlaabwsU5j5m>xYd-!#hN2;%Gv83wjnvuEfzY(xTSj$>)i#x z1Vwm0`wed+a~uPDot#n!b>xEgtBLjRhiXj(^Tfv-2|R6I<=rv_dn{$QOeGc<1Y8G3vUn~ zFBdK6`eZCpAiYKPiXz~4A`*h5pC>cu!k2xo{DNa@Y9_4oi@DkBja|wKRZ&nHhI>q( zjI^q)q}|8t?ynYZuO|=rI0XbC?btdMpwjd%5Ksf1v*P*mzaw*NKFPB6i{>fj^_F6z z_UqA{Nx?JiuuUSH!11p=hCC^x(7>U>-#{$#uLaqz5^g9vFkbWr~HwiY6#Kirs7&$VT zx5`llqU~RQY^an{Bzy}fKjr!DhJIQ$epfY$6>~1i2kC9;VpcpD(?OarO>qg0WyCB% znu&Z+Yzy_*)QFte{|$6$l>`2fvDR7*N1QUm6NR0eKLpSxkrMIgH}D+InNtl3H`R{y z{K=EszF`)ZT31=86e2(*g=mT7YjB=eO;|Um4UmZ3+;t|%a7pu~RH_j;o7H$yMc}|| z%O(=}h=7J56%5HiMQ@a4^6QwB&)QSaJg+M#&Z~<`Y!9OxgY3;jKtZ0*H}|g0`>Q_9 zWgeask_0BC&%tM({7-oXI1*-WX2(8vTqC_aWZfp7J)$xn#*EVHMpF`+6bkh)6_7pT z87)(b4w^Zh#m#bx220u+8tsOBu3R7@A@n!b%731Ea*A1-*}sk-l@4=r>%Usaw9K00 zo@tfR|J)5_6UKa}%E#wZFXWqNTs>x=O`ZD2g+VBk5ci3oUO?z6<|Rgjf<4c|JmsBR zhm9d0;RbK$!JfZSgyv?pBU@zX0MlV1)TQwcodt6#e_RTKqeeeRI7Zcq(OMwq`mKN{ zmr)(n7gU7hmoYI&0xNOf*X>J9fjSe_B%k|>yyaWFjckg(NCQ2`#dA;!+SsP?|7h#X&E zzrE|6FYo(xW(~~T&$ITk@4fGRT@p#siLaO-X9P;?E?F~Q`&Td7eevg{Uq(>QbE=IYAt3Fq3H*WOM`yU2>+{BVCHr-k|j-mKmB;$=!0dF3}}fNA5pJw3A$1j z;u==I?WyUSvwLt9C0y9;rSqL)0=Ud@gAlj(QM7FGzZt)Xq`Xo=Lgx<9KOxsAh<)1U z40GX%M~qMI&0`9+w}pmG_UT@=KNe1oO0}St;zBM-vuJ#hca#tP47b}cC>K&KAAq+b?K0Ueci?3!Dsq;$4$Db;M^5M3JGliKwj45#&Y}uc!X*?haBRn15A(Gpp z40**D^fr*Z2OVWOX;!d$_Ev88BTYNa13W(nRvN z7w=-VTkS42ybJu07q{|mn?1aAKhe90R-qIh1S}(bgZi^G@`eo-?RvFnQrxT5kt)rh zWE@Jm>&&7_*C7DxMBuYmK!*DS&5Xeg=3qmOXM3id2)?%e8;3AH43sCpLC#z1b)X#rv336n@ zT0mkrmWTBFBZF0Vy6z$MM^4 zCLPVvkTI=hGQ0}~918w{(x3GT9wb$ff-jr}8p5vkb4hkTNk*uS8#&|UK_^H%)=FlG z{aNVk!uN_x`y*(#n3nPeN_2okX{=hh=M>_duw%~c+0L3AydakhD{s!Fp8>%k?z6R z(yXw2gc3j9V^($|VX+JhOOA?dJuC@Af+$j(Q^z&co5d$@B;L&oj6ap!tp=56N^WjE z8^JdPFWfp~Q)O&?^;!7O*@ejzIae?fhUVu53B$`?ZPai!fO||^4_l&7@grwPjF?s} zOqKmO<{7~Dpw3OFA{kKOBmvNe=B-m#3bX5xFVrpKbxo}^)Xzih#{SL zo(pnIt}-L^P|^2g2hUSC-TuN_qITCml$`+S4}m@5zgIb8P>`iRPls4in5?;?mwBXf ziwvBLh1|i?HgA+K=skq~eKs z@6`jLRQ{&?O6`TaCZj8FoM1v?+V((;*6gYgag&{qP~^+q?{8HLSEU@MJ++Cty53@d z$M>HP%Mx|Bl{urSbM|eT~O8rH_)_);xLg@HRQ*+x(+{O$$k z<0R#u_rW1kPGu9&<*|{G$gh>Hi>)BYoc=iWxd__&-nA2>#6w4Vuk27w?*EHwn@4h*3mnN<56a5we^_6_vr4|6!hJ;a?X;a!4(JVMpfgKp@kgt z{sF%>eR+;;CQW;mo?cW`j4nE7&k}fCti!}Y3&*ZLQ(?Ckb2|rS{Z39ug5{jU=3a9N z7?Ib`5-#pzX_#+Sjrzy`Sm0}YeLT^>3b|Sp?}C&zb!u7jX%p_#>1{PtsYO$GKbD)| zIm(Oq{>9%%Ko$TXi_wveO2q`=XbBA$lKepl%n8g8p0`f z3sd1X4?Rde%ob%i`Md{P#3Iq0Yv@s*JhIg$@ebJhptd?f{7m)&?zAr&&3r!OY-?`) z_F=zTLlES$gjCKi&2nd`g&h1$eU#kr-k)_RsE-F?Cf$8?=otnlU*GpYd$QV8rTl|N zk?O+K>D8-9=X)Ks%+_j94+v(3Zdzp;C6etZML-7=7zB5jSEnTt=x_K6nO)HRJ;rrE zt0J}x6f=z92kLuImGDzq3g-Ly?0ibppi8>uj+%+_*klbGjYv*PmJ=Pi+)i_FX5GA~ z!7N{EmRlZ308esR#LLRD{Jt%tercPhC-zu6J|Qx=WqdCcv;Sx=Tf4nXxqaCJE0Wu? zk)Eylc^A4%mvcbwl@-zb#r4lmz3*ag6xIU4Uhapkvj7Y{oU1xt-HNg{IriExhvXdT z`nO2Hs(D-sx429t5*!(C!J5Ll$#lgg3ZwT9N#F!0qWyKm7QE0UgZ4ppSzJ{Q9_{0$ zcTm#2yNOMzwBe?TKF#(Q+v5cSdD9kWg@(3c22OL?4xn&0sZ-N3x>)>8Sh(43WH%jR zR8b{;EwWs~|KW1p2y{D7^($($pDsJ@C*KK|E_P?QL zC{i9Q%$Bv=^jS(>utP^^p(^HKyLmgAx=WGm80^V-@7B6DS$a4wO`_bE#LOHv9lF}9 z7KzLBKV7YwSMUXLvSy#wy3O@FM)ya{ zl}AqL)-LlDWH02OhGuUJA5hmL=byciDN1goR6W154|R)Zg4@3yjhQ>)%$;#7;nk){ zL=_B#Mh+d7>yim(Al6TB#$ThMxpByKhnPFCnkL$TIfcD82i|tWhEGa{w7S}2lr%WN zX3F}sn_-~0!uD;SvQw!ne~v$;{j1;S!z1gfWe)dgjdoiX6Xd%r)sD z6}5N0n3L<3xa{IhruG-au(5XdS>@AnxMb7zau&U8PRaUqWiOsKid)8#LPE`)8w* z>TQj=^#7I=v_&Qg-+o)P@#gRf_@(d(xwlIp7|vYv#XsSvF5Ch# zTf}eY#?xTf%l`FvE_)AC06VXU#})3QwRVZ?Yj#w#dLgKGq-rRDxfTUY#MaCC7FD#JBmo3OOUw;C3Cdf32&J_CkL`dl~!G?le$z0dQ#>=Ey_se`I)tKxN zw*j4J&$x1O0TxZe_40lzc#G``r^OUI?IqV9E@cFrRHrBMwtCL>LpT-kLTafw%Vykh zlKc@1HCt#kznw?mk3Ovgr>j9YeE8+1mHc@JWV)ayr7qj2Xo6uSQB;tQhI~h&IcHFl z`VGC_a#NLhGzD%>*#7DLmedRCNOpq0wV|}p2)!(gOky5G32WI8ye+uYC|Qarecv-8 z&L283)Ed(0`Q*INK#SS62W4uI#PSRBHa1SZ=pIJC>ZNq&9h%Z`E{Ofo`l0|5#E*)z zC$!s)Q5t+X>8eOy;_}`?5eJ9htwf{k0MtZ4`v`TcdHh{>{_T&a-)<=>s`6Kk{xng8 zjXl?*d3{9EXld|cktF3Hx&ZAW^`_ZBXuP$(4BY&%xs3#kl0jvZ7u$)B_h(yqIIDbh z%9zFq}E@hl2)RLgw3NNI{|57EfYoK;MERTZx+=A;rv ztz&0WF)7-XFU`8^*EBIqzaO`!cAJzm7R{CGfM_`qDU}ewO|uzSyyTG-QvikNE|WfY z`x`E=u0+8Kpm#Gf32ONdu^W6^9hEg6-(8FntI$iTHEpz=Lbl1{a|Q=prIEO_{3O)2 z1Vcx^Ek04lt3sl&qL^0~SJp$=SY#yQyHzQ8PYw*nM4>oHL+R<1Z*uC2@YwB~zT1*X zPgI5I3r&@9Q&MRx@i*yyd5(*%`>Iw__E83ZA+z$Gr zm><3Ocqb9{+6*EYYJSy&HF4jfb`1Z05w|wwO0vB-7a1Nt>VN%0X0=fI%2jsCrd{%%yv7)o_m3Hkh4f)kE?Fn?in@r&QiZZx9pYbUvH9*#a{R`%V| zqp5z|OSbBheJ(~B9dx1rdaP%(Dg+9Ra{u0_Q^5&62=YPcWrR&*ft*AwI&*z7bOgLo z{2*6ci$C=2@0+RueeQ?zXRRT=RKJZ!q*prY?@s4}LF`INND^!E%so!(2jLdnVVs7| z#9xBXZ-1QPmP#Z`jo?GtEK}81WK*7i3fDxiYH`2|gN&*{N=!GpjNDsfNDI{JUrKye$efm}n9gA9E8M zPeCzqNuqmVdZbNz)rJfVyM8?C4D`{d)ezYC$=mU~Ta&-CreuE{O{^tSNDG?tWt0ib zr5T;_tUq^rHNhT{g7D%nbE=$*wn9lcYE`nmVxvty=aPLiFT{2(v$q0E;? zg@X(fQ|I31lI>>ePykX|BR*^MmC+cFPKMyU zTKX&`exE?Nq$nTXPJ2ULoz_ExvZaJd7n z*5sflI{PM>u7$`)Ci546`mvP=LW%61Ab5f=P?)dB+gZmihvTsD`S|&C^S7Qr=vpGv z8sfGcqftZogoSjn=bqlg!2~stRS4d{mA2WJNpxR2UCHiN*Fl{_3d;3Y+#JiTkJJ*g z$iE=&Pjes$Ww5W99UdHl8%xB-M!zih0$Q*nQ5ATPVx`}ZO)>fvV7)n-@uJViIUSC&eX{(zlmC0%8#*r6HxE zp^6Gy)(TajsnXs*#L+*KDJ4H!5}ZT^-;BqS3~&d3);WR)m9Ibj9?N5z#PZ-8k!A#@ zofP~i*sF7*P#r#ilgurYX&y3dR+dGZX=yOnyl`v1VC(dh6E{tNuo|u}x8TX8jlw?j zikPPHw-9&S0RFBLO_Nx!fl0et@HWtwP80S{f)o{7Vl7^j z;l|KmTk-Q{TrA0hs4APprIQ6drCYLH{WrjK-$!%?-*R-RqNLbp7^h{uv6+-RB@zmks zUx+_>p@xPAzg_#fLTnB+rjpqn+`bCVe=KxB}wl}^VnD#Q9iHXpqd(K_#>Y0nIx*=DK&RYW)P2mkB37QdGTHC0xP{fBT z8R$!0qlO3P^Y{2pS3-Jqzov*=yUnqu>Sw*@S?Pr}Ts#)a?e2COaZ{|#aBos<#uyh3 z8Y>Q%5H(jUeDmLYe_WZ~vbCoHt^mZmL z$*}*7%#P8_;Ju;KKHXcE&I1k(Bk0RR<;Lq}Xx8O;mnij@{LEU!-GKV!_+Y_ZSA6*x zXOm6hv-!<}nn4Dr0pCoGg=P+oKD0r?E~iWQKUOi5r zn}Lr`mHx!VfiFyxFc}Dmz}z7)DtG{W5HA)%K-gi}9hja~#z7{;-5mtP3*gyeAi&zW zzP|2r3tM#u7!$ym-~tNuM*iSyub^hYdIxkXz4y-nx=RB5;^8?4Zr~W&Uu@;Mj3!_U z_kKN(#hi{Ts1iH4e9kHUw1QTqcNfV_x^XDErkz@%J{?+NRg1X3((T}^Y3KN*pZe^m zhv7&HaR=6gwI!IYIwSen@#Ma@l_GoNSd4*=-4Ah*#imFbUprdmuPDE6A+|1G<8?C& zEG^laWX<6xS^UZ$-@Yg9(*2$UwhKTSz_tU#gm{a+0|VCgw^#F+ z#1DVS0*Vu$cld4$;eNXM?EhCeLq@&s>AKu8xT%d_^$>3iXyD$zf6@|F$3T1={1#BA zdWON2Co^+8I-Kg&Nf!Rs79Kk5)6<%cbT_90j)c632>}~~C)xGYT$(q(!2{)=O+3fh zYNobl8_#xkRa(69HtJga-lX2G^55JHCkbpysEytiE-~GheGEV7wqvoi@1HzMH(<#z zvHk&e6rA$HZ|mNu7n@32Q_|W{ccXSAs<0}xisjts%^hVp&B=Ry_DllW84LlS&v4}j z6s+qhxAlr1B*`$dUR9Vx?G`T+G^`LAE7K~u-J05>m*0p%Q+qq`e235VE(F^1VAL#J zB&1RAFk9zGpX`W84r%{aZLN@=^J9AH&*@JHSN;D~jn{;wJiKG)$VwqL3VHYZ6PD5S zFA@}tWSQbwNNBtp4n2fMo*j@!z|8L{7%S&f!tP{t{80nP;+p~7ym)-2?DM}|Zsm~q zBH7mS^41EX=GifvXNcHEG^`BmGLezUcJ1Czsm@=M7?Sg2(KM zQ$jZ>oPq&6!WUvT0JB^i8Hv5a=p|B>`yVPRVr7 zq7XH-D#9+Db{Bf@si1EAgkm&aRroA@P?wc}m;TLZJL8Snmzm3ytu1nrPjP`9-r{_f z#s|MIj$P7LALcYYl-PwzUF_x_;I~A-kK+7(ZFEwpPlFtK_va1+4sgU^1AwyxAc7QW zFZJo?F)%URJu^@*iW%U*P)YV`7{Hv9k`Qe*8Xg2|=4U4Z9EHb$^`{3PIpbjlIm@C~ z!H*{k4tqq%8bM~oeO~>H5qhur(}8vt7i0P|BuGd`3lNL1w6B%+=~p{=0s;k?A3d+UBR)dos{DyYfsy5 zR0?(3-`{@^W5qqz$PZJUEm6dFmk|VQqZr1`;@iA6FpgDJ#DLSr7OSDIj^jjduc7R) z{Vzp;z+=B`)l5O5yHSM3XkW(RSwmKeb$@kuNpGWumHy8-Cm(v~{0EZ&1GX1=aj4_9 z*2i8|UUq_Ey>IFYt>3A!Ghe@0&rrBH_w_@0^KdR()SXG#{Ap}TxM|OTmVZY%&DPzC z*7I(MTSAl&t((jXXPMzD#3`BmL{rDw63mwvyns(-3EfLRAJ>HmDBp!-gx%SHU@Rse#8Z*a2(v%o zByd*=mW{DSReb+YyxE_bc_W)5K$CIewVZ;sgQxXp9hZO7tPV`Tu>L~U`S>{jNlBVd zUCzJ`$KXGC)?*m2kWfNd4(lw0O@S+`J(ffJHDNcZI9a(ypM|9Z)@62LvLG%<2Vb3S zpps1tkk^qN=f@D*(~vO5r|*S*bbscFJvP0@pj2j{6~v}+n3CeSkOJOb#@6*7~>CZWPpc^YaVAcV}^*m~q4VtneF@j z1lt-pH#s612R|!Dj*CT6o_N4*^?82*HtuaOW!iPm2*;3?S4uRyd8$G<=64-qF=19# zVPU1k%;~tY2Yt@g)=t(=IJ7^{f|C@?-R>e`%~ojmbrdL)#fpbI6rI0MHQG1MGvhh5 z<%chYzFWu;K8s)|lhZ)?KaFAhzjSI_{@<%zU@(CH5!hy`L`S~<-y-M!PkfF0KprO~ zqz4SD3bHRK|GkHd{{Jt%EC2@G<0By<5KFDge~^%n(EhH~LnJu*rp=|$T0VFMNmWrp K;iH^+#D4(%8S0P# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1039009a35eee49903e95b0e4c082ed8927840d7 GIT binary patch literal 46311 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 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 0000000000000000000000000000000000000000..efcd671bb90e498c5f100e8e22c46e8548207816 GIT binary patch literal 37306 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2d34398bc475aeb33ca60fd5edc9fbda49be0dd3 GIT binary patch literal 45861 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

yZ4G;Y%TM- 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%0A+VAI`gY>PQn} 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 A)&Kwi literal 0 HcmV?d00001 diff --git a/docs/r1/dapp-matrix-funding.md b/docs/r1/dapp-matrix-funding.md index a646e06..63cff53 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 fb65bec..70973a5 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/packages/chains-xrpl/src/networks.ts b/packages/chains-xrpl/src/networks.ts index 971aedb..e0fa23b 100644 --- a/packages/chains-xrpl/src/networks.ts +++ b/packages/chains-xrpl/src/networks.ts @@ -33,7 +33,8 @@ export const XRPL_NETS: Record = { testnet: { caip2: "xrpl:1", networkId: 1, - rpc: ["https://s.altnet.rippletest.net:51234/", "https://testnet.xrpl-labs.com/"], + // 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", From 3d111b27c4811a3d6cf14542d7d0e12fa8ea128a Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:54:55 +0800 Subject: [PATCH 31/36] fix(networks87): node types for the chains-cosmos tests; the derivation-path check covers every family - chains-cosmos tests use Buffer: @types/node as a dev dependency, as the other chain packages have it (vitest 4 no longer brings it in). - extension-kit's derivation-path test (every module's derivationPath() names the path the vault derives) expected 14 families; it now checks FAMILIES.length, and all 26 match. Co-Authored-By: Claude Opus 5.5 --- packages/chains-cosmos/package.json | 5 +++-- packages/extension-kit/test/derivation-paths.test.ts | 4 ++-- pnpm-lock.yaml | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/chains-cosmos/package.json b/packages/chains-cosmos/package.json index 1cd4542..98a8bd1 100644 --- a/packages/chains-cosmos/package.json +++ b/packages/chains-cosmos/package.json @@ -80,8 +80,9 @@ }, "devDependencies": { "@cosmjs/amino": "^0.39.0", + "@cosmjs/crypto": "^0.39.0", "@cosmjs/proto-signing": "^0.39.0", - "cosmjs-types": "^0.11.0", - "@cosmjs/crypto": "^0.39.0" + "@types/node": "^22.20.5", + "cosmjs-types": "^0.11.0" } } diff --git a/packages/extension-kit/test/derivation-paths.test.ts b/packages/extension-kit/test/derivation-paths.test.ts index 04175ad..b36459c 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/pnpm-lock.yaml b/pnpm-lock.yaml index 74994be..e60d57d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1244,6 +1244,9 @@ importers: '@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 From 4697f6f06e4cf1dbc3e18b7d5d898861c3ec0d71 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:55:09 +0800 Subject: [PATCH 32/36] docs: Networks reference page, dapp guides for the newer families, README network table and counts - reference/networks (in the sidebar): 85 of the 87 CLPR networks and 93 mainnets, how the count works, every supported CLPR network with its module, network id and dapp connectivity, the families' derivation paths, why Canton and Mixin aren't supported, and the limits worth knowing. Built from docs/r1/networks87.md; the coverage is the one networks87.test.ts computes from the catalogue. - Dapp guides: Cosmos SDK (Keplr-compatible API at window.clipwallet.cosmos), TRON (TIP-1193 + TIP-6963), XRP Ledger (XLS-72d), Stacks (SIP-030 + WBIP-004), Fuel (FuelConnector), Bitcoin Cash (WalletConnect wc2-bch-bcr) and MultiversX (best effort, through sdk-dapp's undocumented hook), with typechecked snippets; Antelope and ICP are send and receive only; Chainflip in the Polkadot guide, STRATO in the EVM guide, bch in the WalletConnect table. - Chain modules, vault derivation table, vault cryptography and the family counts across the site follow the 26 families; the docs workspace depends on the new chain packages so their README examples typecheck. - README: 85 of 87 CLPR networks, 26 families, 93 mainnets; the standards 1Mask speaks; the network table gains the newer families with their dapp matrix results; the internal review covered the first 14 families. Co-Authored-By: Claude Opus 5.5 --- README.md | 48 +++-- apps/docs/.vitepress/sidebar.ts | 8 + apps/docs/package.json | 9 + apps/docs/src/architecture/chain-modules.md | 14 +- apps/docs/src/architecture/extension-kit.md | 2 +- apps/docs/src/architecture/signing-flow.md | 2 +- apps/docs/src/architecture/vault.md | 14 +- apps/docs/src/dapps/bitcoincash.md | 21 ++ apps/docs/src/dapps/cosmos.md | 30 +++ apps/docs/src/dapps/evm.md | 8 +- apps/docs/src/dapps/fuel.md | 25 +++ apps/docs/src/dapps/index.md | 12 ++ apps/docs/src/dapps/multiversx.md | 26 +++ apps/docs/src/dapps/polkadot.md | 11 +- apps/docs/src/dapps/stacks.md | 26 +++ apps/docs/src/dapps/tron.md | 31 +++ apps/docs/src/dapps/walletconnect.md | 1 + apps/docs/src/dapps/xrpl.md | 21 ++ apps/docs/src/guide/index.md | 6 +- apps/docs/src/guide/repo-layout.md | 2 +- apps/docs/src/index.md | 2 +- apps/docs/src/kit/index.md | 2 +- apps/docs/src/reference/index.md | 4 +- apps/docs/src/reference/networks.md | 194 +++++++++++++++++++ apps/docs/src/security/vault-crypto.md | 7 +- apps/docs/src/snippets/dapps/cosmos.ts | 30 +++ apps/docs/src/snippets/dapps/fuel.ts | 17 ++ apps/docs/src/snippets/dapps/stacks.ts | 20 ++ apps/docs/src/snippets/dapps/tron.ts | 37 ++++ apps/docs/src/snippets/kit/inpage.content.ts | 2 +- apps/docs/src/testing/matrices.md | 2 +- pnpm-lock.yaml | 27 +++ 32 files changed, 627 insertions(+), 34 deletions(-) create mode 100644 apps/docs/src/dapps/bitcoincash.md create mode 100644 apps/docs/src/dapps/cosmos.md create mode 100644 apps/docs/src/dapps/fuel.md create mode 100644 apps/docs/src/dapps/multiversx.md create mode 100644 apps/docs/src/dapps/stacks.md create mode 100644 apps/docs/src/dapps/tron.md create mode 100644 apps/docs/src/dapps/xrpl.md create mode 100644 apps/docs/src/reference/networks.md create mode 100644 apps/docs/src/snippets/dapps/cosmos.ts create mode 100644 apps/docs/src/snippets/dapps/fuel.ts create mode 100644 apps/docs/src/snippets/dapps/stacks.ts create mode 100644 apps/docs/src/snippets/dapps/tron.ts diff --git a/README.md b/README.md index a54584f..fca0f46 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.

@@ -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)). diff --git a/apps/docs/.vitepress/sidebar.ts b/apps/docs/.vitepress/sidebar.ts index 8c62d80..6112f9d 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 b6074a6..a5a16b4 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 b613e9e..5f5ff7f 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 fcd8ba9..851b437 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 8a7590d..cc58c95 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 4f1f9ba..fa317ce 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 0000000..65e3f8b --- /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 0000000..cf6a2da --- /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 8519350..2cc5775 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 0000000..972b297 --- /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 f236b40..44a60db 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 0000000..f8ac8dc --- /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 dc409f4..3d611ee 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 0000000..b2512b9 --- /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 0000000..1d97e3b --- /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 bf35838..18c8a9d 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 0000000..80f188d --- /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 2ca6f86..6246f3c 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 a7257d2..866f048 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 c604ee8..8dd2b64 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 4165644..fd1c879 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 0772eae..ea48b94 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 0000000..91109a6 --- /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 47e5873..56e6cce 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 0000000..98a4d9c --- /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 0000000..fcb0dfa --- /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 0000000..309d412 --- /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 0000000..8d6f0d0 --- /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 6b42b40..8101136 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 9735cce..843b964 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/pnpm-lock.yaml b/pnpm-lock.yaml index e60d57d..39d122b 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 From ffcd993f6685732a06c3f4d751d75f77f8c1a2dc Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 21:58:39 +0800 Subject: [PATCH 33/36] chore: gitleaks ignore entries for the networks87 test fixtures and Initia feature names Reviewed false positives of the generic-api-key rule: Antelope PUB_K1 and secp256k1 public keys in test fixtures, XRPL NFToken and offer ids (public ledger object ids), and Initia's chain feature names. Co-Authored-By: Claude Opus 5.5 --- .gitleaksignore | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.gitleaksignore b/.gitleaksignore index da21e04..ab0b24e 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -102,3 +102,26 @@ 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 From 7eb9727cfb24bef71f81ca88511e1bcf0f2a8e85 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 22:38:00 +0800 Subject: [PATCH 34/36] fix: CodeQL ReDoS findings on the code paths this branch reaches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trailing slashes are trimmed with a scan instead of /\/+$/ (backup-client's base URL, chains-cosmos REST endpoints, chains-hedera's mirror node, CoinGecko's base, settle-on-Hedera Connector URLs), and route's short ledger names drop a trailing "(…)" with indexOf instead of /\s*\(.*\)\s*$/. Same results, linear on any input. Co-Authored-By: Claude Opus 5.5 --- packages/backup-client/src/index.ts | 9 ++++++++- packages/chains-cosmos/src/rest.ts | 9 ++++++++- packages/chains-hedera/src/networks.ts | 9 ++++++++- packages/features/src/prices/coingecko.ts | 9 ++++++++- packages/route/src/client.ts | 6 +++++- packages/route/src/settle.ts | 9 ++++++++- 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/backup-client/src/index.ts b/packages/backup-client/src/index.ts index 2464d36..3329364 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-cosmos/src/rest.ts b/packages/chains-cosmos/src/rest.ts index 0cbc7ae..a5b1bc6 100644 --- a/packages/chains-cosmos/src/rest.ts +++ b/packages/chains-cosmos/src/rest.ts @@ -1,6 +1,13 @@ 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): @@ -35,7 +42,7 @@ export interface Rest { } export function restFor(ctx: ChainContext): Rest { - const bases = ctx.network.rpcUrls.map((u) => u.replace(/\/+$/, "")); + 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 { diff --git a/packages/chains-hedera/src/networks.ts b/packages/chains-hedera/src/networks.ts index 58b8d5f..98f9f49 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/features/src/prices/coingecko.ts b/packages/features/src/prices/coingecko.ts index 3cddaab..ec6f996 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 f0987f3..752cce4 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.ts b/packages/route/src/settle.ts index 11efcdf..a37fc9b 100644 --- a/packages/route/src/settle.ts +++ b/packages/route/src/settle.ts @@ -31,6 +31,13 @@ import { } from "./settle-quote.js"; import { JsonRpcReader, MIRROR_LOG_WINDOW_S, MirrorNodeReader, type HederaReader } from "./settle-reader.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); +} + /** * "Settle on Hedera" client (Phase 3). Reads the order book on Hedera, asks Connectors for signed quotes and * verifies every one before showing it, and builds the requests the wallet approves (deposit, claim, withdraw). @@ -276,7 +283,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), From 671fc343f80fcd9508879e2fb6b6181c61ba70a0 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 22:48:54 +0800 Subject: [PATCH 35/36] fix(route): one linear trailing-slash trim for every base URL (CodeQL ReDoS) packages/route/src/url.ts withoutTrailingSlashes() replaces /\/+$/ in the mirror-node reader (which then drops a trailing /api/v1 with endsWith), the tracker's base URL and the Connector quote URL. Co-Authored-By: Claude Opus 5.5 --- packages/route/src/settle-reader.ts | 4 +++- packages/route/src/settle.ts | 7 +------ packages/route/src/track.ts | 3 ++- packages/route/src/url.ts | 6 ++++++ 4 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 packages/route/src/url.ts diff --git a/packages/route/src/settle-reader.ts b/packages/route/src/settle-reader.ts index 685c596..ea34014 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 a37fc9b..40c2822 100644 --- a/packages/route/src/settle.ts +++ b/packages/route/src/settle.ts @@ -30,13 +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"; -/** `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); -} /** * "Settle on Hedera" client (Phase 3). Reads the order book on Hedera, asks Connectors for signed quotes and diff --git a/packages/route/src/track.ts b/packages/route/src/track.ts index 3b5c95c..76bcd6e 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 0000000..d5dc364 --- /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); +} From 6dc87858fa000c721831d6ff1b2f0b6567424499 Mon Sep 17 00:00:00 2001 From: Schayan Salehi Date: Tue, 6 Oct 2026 23:09:50 +0800 Subject: [PATCH 36/36] chore: gitleaks ignore entry for the ignore list's own comment The comment that explained the Initia feature-name entries quoted them, so the ignore list itself matched the generic-api-key rule. The comment no longer quotes them, and the commit that did is listed. Co-Authored-By: Claude Opus 5.5 --- .gitleaksignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitleaksignore b/.gitleaksignore index ab0b24e..b2250f6 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -117,7 +117,7 @@ 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") +# 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 @@ -125,3 +125,5 @@ 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

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