diff --git a/README.md b/README.md index c849847..db58353 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ npm install @meshsdk/wallet ``` > **Migrating from v1 (`MeshWallet` or `BrowserWallet`)?** This version has breaking changes. See: +> > - [`mesh-wallet-migration.md`](./mesh-wallet-migration.md) — for `MeshWallet` to `MeshCardanoHeadlessWallet` > - [`browser-wallet-migration.md`](./browser-wallet-migration.md) — for `BrowserWallet` to `MeshCardanoBrowserWallet` @@ -19,6 +20,7 @@ npm install @meshsdk/wallet - [Headless Wallet (Server-Side)](#headless-wallet-server-side) - [Simulated CIP-30 API (headless)](#simulated-cip-30-api-headless) - [Browser Wallet (Client-Side)](#browser-wallet-client-side) +- [Bitcoin Headless Wallet](#bitcoin-headless-wallet) - [Low-Level Components](#low-level-components) - [CIP-30 Compatibility](#cip-30-compatibility) - [CardanoHeadlessWallet vs MeshCardanoHeadlessWallet](#cardanoheadlesswallet-vs-meshcardanoheadlesswallet) @@ -39,20 +41,20 @@ This package uses a two-tier class hierarchy for both headless and browser walle ## Exported Classes -| Class | Purpose | Use When | -|-------|---------|----------| -| `MeshCardanoHeadlessWallet` | Full-featured headless wallet with convenience methods | Server-side signing, backend transaction building, testing | -| `CardanoHeadlessWallet` | CIP-30 strict headless wallet (raw hex/CBOR returns) | You need raw CIP-30 output without conversion | -| `MeshCardanoBrowserWallet` | Full-featured browser wallet wrapper with convenience methods | dApp frontend integration with browser wallets (Eternl, Nami, etc.) | -| `CardanoBrowserWallet` | CIP-30 strict browser wallet wrapper (raw hex/CBOR returns) | You need raw CIP-30 passthrough from browser wallets | -| `CardanoInMemoryBip32` | BIP32 key derivation from mnemonic (keys stored in memory) | Deriving payment/stake/DRep keys from a mnemonic | -| `BaseSigner` | Ed25519 signer from raw private keys | Signing with raw private keys (normal or extended) | -| `CardanoAddress` | Cardano address construction and utilities | Building addresses from credentials | -| `ICardanoWallet` | Interface definition for Cardano wallets | Type-checking and implementing custom wallets | -| `createCip30Wallet` | Wraps an `ICardanoWallet` as a simulated CIP-30 initial API (`window.cardano.`-shaped), with `enable()` extension negotiation | dApp integration tests / server-side agents that need a real `window.cardano`-shaped API, no browser | -| `createCip30Api` | Wraps an `ICardanoWallet` as a spec-exact `ICip30Api` directly, without extension negotiation | Advanced: you already have your own `enable()` gating and just need the spec-exact endpoints | -| `ICip30Api` / `ICip30InitialApi` | Interface definitions for the CIP-30 API surface | Type-checking and implementing custom CIP-30 adapters | -| `Cip30APIError`, `Cip30PaginateError`, `Cip30TxSignError`, `Cip30DataSignError`, `Cip30TxSendError` | CIP-30 error classes (spec-exact wire shapes: `{code, info}` or `{maxSize}`) | Catching and pattern-matching on typed CIP-30 errors instead of plain `Error` | +| Class | Purpose | Use When | +| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `MeshCardanoHeadlessWallet` | Full-featured headless wallet with convenience methods | Server-side signing, backend transaction building, testing | +| `CardanoHeadlessWallet` | CIP-30 strict headless wallet (raw hex/CBOR returns) | You need raw CIP-30 output without conversion | +| `MeshCardanoBrowserWallet` | Full-featured browser wallet wrapper with convenience methods | dApp frontend integration with browser wallets (Eternl, Nami, etc.) | +| `CardanoBrowserWallet` | CIP-30 strict browser wallet wrapper (raw hex/CBOR returns) | You need raw CIP-30 passthrough from browser wallets | +| `CardanoInMemoryBip32` | BIP32 key derivation from mnemonic (keys stored in memory) | Deriving payment/stake/DRep keys from a mnemonic | +| `BaseSigner` | Ed25519 signer from raw private keys | Signing with raw private keys (normal or extended) | +| `CardanoAddress` | Cardano address construction and utilities | Building addresses from credentials | +| `ICardanoWallet` | Interface definition for Cardano wallets | Type-checking and implementing custom wallets | +| `createCip30Wallet` | Wraps an `ICardanoWallet` as a simulated CIP-30 initial API (`window.cardano.`-shaped), with `enable()` extension negotiation | dApp integration tests / server-side agents that need a real `window.cardano`-shaped API, no browser | +| `createCip30Api` | Wraps an `ICardanoWallet` as a spec-exact `ICip30Api` directly, without extension negotiation | Advanced: you already have your own `enable()` gating and just need the spec-exact endpoints | +| `ICip30Api` / `ICip30InitialApi` | Interface definitions for the CIP-30 API surface | Type-checking and implementing custom CIP-30 adapters | +| `Cip30APIError`, `Cip30PaginateError`, `Cip30TxSignError`, `Cip30DataSignError`, `Cip30TxSendError` | CIP-30 error classes (spec-exact wire shapes: `{code, info}` or `{maxSize}`) | Catching and pattern-matching on typed CIP-30 errors instead of plain `Error` | --- @@ -61,7 +63,7 @@ This package uses a two-tier class hierarchy for both headless and browser walle ### Create from Mnemonic ```typescript -import { MeshCardanoHeadlessWallet, AddressType } from "@meshsdk/wallet"; +import { AddressType, MeshCardanoHeadlessWallet } from "@meshsdk/wallet"; const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ mnemonic: "globe cupboard camera ...".split(" "), @@ -76,10 +78,14 @@ The `fetcher` is needed for signing transactions — the wallet uses it to look ### Create from Raw Private Key ```typescript -import { MeshCardanoHeadlessWallet, AddressType, BaseSigner } from "@meshsdk/wallet"; +import { + AddressType, + BaseSigner, + MeshCardanoHeadlessWallet, +} from "@meshsdk/wallet"; const paymentSigner = BaseSigner.fromNormalKeyHex( - "d4ffb1e83d44b66849b4f16183cbf2ba1358c491cfeb39f0b66b5f811a88f182" + "d4ffb1e83d44b66849b4f16183cbf2ba1358c491cfeb39f0b66b5f811a88f182", ); const wallet = await MeshCardanoHeadlessWallet.fromCredentialSources({ @@ -111,7 +117,7 @@ import { CardanoInMemoryBip32 } from "@meshsdk/wallet"; const HARDENED_OFFSET = 0x80000000; const bip32 = await CardanoInMemoryBip32.fromMnemonic( - "globe cupboard camera ...".split(" ") + "globe cupboard camera ...".split(" "), ); const paymentSigner = await bip32.getSigner([ @@ -219,11 +225,11 @@ const wallets = MeshCardanoBrowserWallet.getInstalledWallets(); ### Common Operations ```typescript -const balance = await wallet.getBalanceMesh(); // Asset[] -const address = await wallet.getChangeAddressBech32(); // bech32 string -const utxos = await wallet.getUtxosMesh(); // UTxO[] -const collateral = await wallet.getCollateralMesh(); // UTxO[] -const networkId = await wallet.getNetworkId(); // number +const balance = await wallet.getBalanceMesh(); // Asset[] +const address = await wallet.getChangeAddressBech32(); // bech32 string +const utxos = await wallet.getUtxosMesh(); // UTxO[] +const collateral = await wallet.getCollateralMesh(); // UTxO[] +const networkId = await wallet.getNetworkId(); // number const rewards = await wallet.getRewardAddressesBech32(); // string[] // Sign and get the full transaction back (ready to submit) @@ -235,6 +241,76 @@ const signature = await wallet.signData(addressBech32, hexPayload); --- +## Bitcoin Headless Wallet + +`BitcoinHeadlessWallet` is the Bitcoin counterpart of the Cardano headless wallet: a server-side / Node.js wallet driven by a pluggable `IBitcoinProvider` data provider — the same fetcher/submitter pattern used on the Cardano side. + +### Create from Mnemonic with the Maestro provider + +```typescript +import { BitcoinHeadlessWallet, MaestroProvider } from "@meshsdk/wallet"; + +const provider = new MaestroProvider({ + network: "testnet", // or "mainnet" + apiKey: "", +}); + +const wallet = await BitcoinHeadlessWallet.fromMnemonic({ + network: "Testnet4", // or "Mainnet" + mnemonic: "muscle urban donkey ...".split(" "), + provider, +}); +``` + +`MaestroProvider` implements `IBitcoinProvider` against the [Maestro Bitcoin API](https://docs.gomaestro.org/bitcoin)'s Esplora-compatible endpoints. Any other `IBitcoinProvider` implementation can be swapped in — the wallet code doesn't change. + +### Query UTXOs + +```typescript +// All UTXOs across the payment (P2WPKH) and ordinals (P2TR) addresses, +// each annotated with the address and purpose it belongs to. +const utxos = await wallet.fetchUTXOs(); + +// Or query the provider directly for any address. +const addressUtxos = await provider.fetchAddressUTxOs("tb1q..."); +``` + +### Coin selection + +`signTransfer` selects inputs automatically, but the algorithm is also exported for standalone use — largest-first selection with BIP-141 vbyte-accurate fee estimation and automatic sub-dust change handling: + +```typescript +import { selectUtxosLargestFirst } from "@meshsdk/wallet"; + +const { selectedUtxos, change } = selectUtxosLargestFirst( + utxos, // from fetchAddressUTxOs + 50_000, // target amount (sats) + 2, // fee rate (sats/vB) + 1, // number of recipients +); +``` + +### Send a transfer + +```typescript +// Queries UTXOs, selects coins, builds + signs a PSBT (RBF-enabled), +// broadcasts through the provider, and returns the txid. +const txid = await wallet.signTransfer([ + { address: "tb1q...", amount: 10_000 }, +]); +``` + +### Other operations + +```typescript +const balance = await wallet.getBalance(); // { confirmed, unconfirmed, total } in sats +const history = await wallet.getTransactionHistory(); // newest first +const signature = await wallet.signMessage("tb1q...", "hello"); // BIP-137 ECDSA +const psbtBase64 = await wallet.signPsbt({ psbt, signInputs }); // P2WPKH + P2TR inputs +``` + +--- + ## Low-Level Components ### CardanoInMemoryBip32 @@ -300,12 +376,12 @@ Endpoints implemented by the `createCip30Wallet()` / `createCip30Api()` adapter `MeshCardanoHeadlessWallet` extends it with convenience methods: -| Need | Base method (hex/CBOR) | Mesh method (parsed) | -|------|----------------------|---------------------| -| Balance | `getBalance()` → CBOR hex | `getBalanceMesh()` → `Asset[]` | -| Address | `getChangeAddress()` → hex | `getChangeAddressBech32()` → bech32 | -| UTxOs | `getUtxos()` → CBOR hex[] | `getUtxosMesh()` → `UTxO[]` | -| Sign tx | `signTx()` → witness set | `signTxReturnFullTx()` → full signed tx | +| Need | Base method (hex/CBOR) | Mesh method (parsed) | +| ------- | -------------------------- | --------------------------------------- | +| Balance | `getBalance()` → CBOR hex | `getBalanceMesh()` → `Asset[]` | +| Address | `getChangeAddress()` → hex | `getChangeAddressBech32()` → bech32 | +| UTxOs | `getUtxos()` → CBOR hex[] | `getUtxosMesh()` → `UTxO[]` | +| Sign tx | `signTx()` → witness set | `signTxReturnFullTx()` → full signed tx | The same pattern applies to `CardanoBrowserWallet` vs `MeshCardanoBrowserWallet`. @@ -317,9 +393,9 @@ This package (`@meshsdk/wallet` v2) has breaking changes from the previous `Mesh **Do not attempt to upgrade without reading the migration guides.** Key breaking changes include renamed classes, swapped method parameters, changed return types, and removed methods. Many changes compile without errors but fail silently at runtime. -| Migrating from | Migrating to | Guide | -|----------------|-------------|-------| -| `MeshWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoHeadlessWallet` | [`mesh-wallet-migration.md`](./mesh-wallet-migration.md) | -| `BrowserWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoBrowserWallet` | [`browser-wallet-migration.md`](./browser-wallet-migration.md) | +| Migrating from | Migrating to | Guide | +| ----------------------------------------------------------- | --------------------------- | -------------------------------------------------------------- | +| `MeshWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoHeadlessWallet` | [`mesh-wallet-migration.md`](./mesh-wallet-migration.md) | +| `BrowserWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoBrowserWallet` | [`browser-wallet-migration.md`](./browser-wallet-migration.md) | The migration guides are written for both human developers and LLM agents — they contain deterministic SEARCH/REPLACE patterns that can be applied file-by-file. diff --git a/bitcoin-milestone.md b/bitcoin-milestone.md new file mode 100644 index 0000000..75283f5 --- /dev/null +++ b/bitcoin-milestone.md @@ -0,0 +1,41 @@ +# Milestone Evidence — Standard Headless Wallet on Bitcoin + +This document maps the milestone acceptance criteria to the code in this repository. + +**Milestone output A:** Develop and deploy a standard headless wallet on Bitcoin. + +The headless wallet is `BitcoinHeadlessWallet` ([`src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts`](./src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts)), a server-side / Node.js wallet implementing the [`IBitcoinWallet`](./src/bitcoin/interfaces/bitcoin-wallet.ts) interface, with BIP-39/BIP-32 key management, BIP-84 (P2WPKH) and BIP-86 (P2TR) address derivation, and a pluggable [`IBitcoinProvider`](./src/bitcoin/interfaces/bitcoin-provider.ts) data-provider interface mirroring the Mesh Cardano fetcher/submitter pattern. + +## A1 — UTXO querying based on Maestro as the data provider + +| What | Where | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | +| Maestro provider (implements `IBitcoinProvider`) | [`src/bitcoin/providers/maestro.ts`](./src/bitcoin/providers/maestro.ts) | +| Shared fetch/error plumbing | [`src/bitcoin/providers/common.ts`](./src/bitcoin/providers/common.ts) | +| Wallet-level UTXO querying (`fetchUTXOs`) | [`src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts`](./src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts) | +| Tests (mocked HTTP, 20 cases) | [`test/bitcoin/providers/maestro.test.ts`](./test/bitcoin/providers/maestro.test.ts) | + +`MaestroProvider` covers address info, address UTXOs, per-transaction unspent outputs, paginated transaction history, transaction status, fee estimates (sat/vB with closest-target selection and a testnet fallback), and raw-transaction broadcast — all against Maestro's Esplora-compatible Bitcoin API using the platform `fetch` (no HTTP client dependency). + +## A2 — Coin selection algorithm on queried Bitcoin UTXOs + +| What | Where | +| ------------------------------------------------- | ------------------------------------------------------------------------------ | +| Coin selection module (`selectUtxosLargestFirst`) | [`src/bitcoin/utils/coin-selection.ts`](./src/bitcoin/utils/coin-selection.ts) | +| Tests (fee model, dust handling, 12 cases) | [`test/bitcoin/coin-selection.test.ts`](./test/bitcoin/coin-selection.test.ts) | + +Largest-first selection with BIP-141/BIP-144 vbyte-accurate fee estimation (11 vB overhead, 68 vB per P2WPKH input, 31 vB per output). Change below the 546-sat P2WPKH dust threshold is dropped and absorbed as miner fee; insufficient funds throw a descriptive error. + +## A3 — Send transfer transaction + +| What | Where | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `signTransfer` (query → select → build → sign → broadcast) | [`src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts`](./src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts) | +| PSBT signing (P2WPKH ECDSA + P2TR Schnorr) | same file — `signPsbt` | +| Tests (broadcast, fees, dust, RBF) | [`test/bitcoin/bitcoin-headless-wallet.test.ts`](./test/bitcoin/bitcoin-headless-wallet.test.ts) | + +`signTransfer` fetches UTXOs from the provider, fetches a fee estimate (falling back to 2 sat/vB), runs coin selection, builds a BIP-125 RBF-enabled PSBT, signs, finalizes, and broadcasts through the provider, returning the txid. + +## Usage + +See the [Bitcoin Headless Wallet](./README.md#bitcoin-headless-wallet) section of the README for end-to-end examples. diff --git a/jest.config.ts b/jest.config.ts index 4cfaa0d..3a55708 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -7,9 +7,12 @@ const jestConfig: Config = { testMatch: ["**/test/**/*.test.ts"], testPathIgnorePatterns: ["/_backup/"], setupFiles: ["dotenv/config"], + setupFilesAfterEnv: ["/test/setup-sodium.ts"], preset: "ts-jest", moduleNameMapper: { "^(\\.{1,2}/.*)\\.js$": "$1", + "^libsodium-wrappers-sumo$": "/node_modules/libsodium-wrappers-sumo", + "^libsodium-sumo$": "/node_modules/libsodium-sumo", }, transform: { "^.+\\.[jt]s?$": "ts-jest", diff --git a/package-lock.json b/package-lock.json index c56c4e0..101c14b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@cardano-sdk/crypto": "0.4.4", "@cardano-sdk/util": "0.17.1", "@harmoniclabs/cbor": "^1.6.6", + "@meshsdk/core-cst": "^1.9.1", "@simplewebauthn/browser": "^13.0.0", "@types/base32-encoding": "^1.0.2", "@types/bn.js": "^5.1.5", @@ -28,7 +29,7 @@ "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.4.1", "@meshsdk/common": "1.9.0-beta.100", - "@meshsdk/provider": "1.9.0-beta.99", + "@meshsdk/provider": "^1.9.0-beta.102", "@types/jest": "^29.5.14", "@types/mocha": "^10.0.10", "dotenv": "^16.4.5", @@ -683,74 +684,61 @@ } }, "node_modules/@cardano-sdk/dapp-connector": { - "version": "0.13.26", - "resolved": "https://registry.npmjs.org/@cardano-sdk/dapp-connector/-/dapp-connector-0.13.26.tgz", - "integrity": "sha512-4GptUVsGmgZhzKs+yp3360dA+HNdMi8IW1r2n1H63PYOJDPj2bjopBeyOGFn8Dmkzt+64rm2KyVyZM2SlcUq9Q==", - "dev": true, + "version": "0.13.30", + "resolved": "https://registry.npmjs.org/@cardano-sdk/dapp-connector/-/dapp-connector-0.13.30.tgz", + "integrity": "sha512-42BZ4imKWInPbUjUEJ/B4EjHyr0qL60nrcx7OposNaTIOGJtY26+J43n6A4I3Qx7WQhzYNg0tYIznhJTjiVmLg==", "license": "Apache-2.0", "dependencies": { - "@cardano-sdk/core": "~0.46.12", - "@cardano-sdk/crypto": "~0.4.5", - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/core": "~0.47.0", + "@cardano-sdk/crypto": "~0.4.7", + "@cardano-sdk/util": "~0.17.2", "ts-custom-error": "^3.2.0", "ts-log": "^2.2.4", "webextension-polyfill": "^0.8.0" }, "engines": { - "node": ">=16.20.2" + "node": ">=22" } }, "node_modules/@cardano-sdk/dapp-connector/node_modules/@cardano-sdk/core": { - "version": "0.46.12", - "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.46.12.tgz", - "integrity": "sha512-yUA/xBUQMiMqIWiZPvIhM911pL3jNKg4PkZQ8qP9R7yU3NQ5x4RQkZ+zFDlVLxUt+gJiwIW2es0iPd8ObIKCxA==", - "dev": true, + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.47.0.tgz", + "integrity": "sha512-UANc6oY8Emjf5ID+Qk6XKxujPfrdxu0rDNDNy+jYJGMYh/kyUrLzOK8Ge4ydblqKQwohRXmMKagFusad3yHoDg==", "license": "Apache-2.0", "dependencies": { "@biglup/is-cid": "^1.0.3", - "@cardano-ogmios/client": "6.9.0", - "@cardano-ogmios/schema": "6.9.0", - "@cardano-sdk/crypto": "~0.4.5", - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/crypto": "~0.4.7", + "@cardano-sdk/util": "~0.17.2", "@foxglove/crc": "^0.0.3", "@scure/base": "^1.1.1", "fraction.js": "4.0.1", - "ip-address": "^9.0.5", + "ip-address": "^10.2.0", "lodash": "^4.17.21", "ts-custom-error": "^3.2.0", "ts-log": "^2.2.4", "web-encoding": "^1.1.5" }, "engines": { - "node": ">=16.20.2" - }, - "peerDependencies": { - "rxjs": "^7.4.0" - }, - "peerDependenciesMeta": { - "rxjs": { - "optional": true - } + "node": ">=22" } }, "node_modules/@cardano-sdk/dapp-connector/node_modules/@cardano-sdk/crypto": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.5.tgz", - "integrity": "sha512-ymliqxdmen5dGVaiMVQ0VnhrwaYUjbPD3sHoMj8NI6MTuxrREp3pLJASREtWhwmv9k+QzDT6CoyuIXnlEQiWZQ==", - "dev": true, + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.7.tgz", + "integrity": "sha512-4aCsLOkqaIS/gSGw4SBqNGcCIp6hZ8alQWJx58U/OiOxC43t7i2JpKagdyWfCNekPM14hO7BWER7y31wf4BFxw==", "license": "Apache-2.0", "dependencies": { - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/util": "~0.17.2", "blake2b": "^2.1.4", "i": "^0.3.7", - "libsodium-wrappers-sumo": "0.7.10", + "libsodium-wrappers-sumo": "0.8.4", "lodash": "^4.17.21", "pbkdf2": "^3.1.3", "ts-custom-error": "^3.2.0", "ts-log": "^2.2.4" }, "engines": { - "node": ">=16.20.2" + "node": ">=22" }, "peerDependencies": { "@dcspark/cardano-multiplatform-lib-asmjs": "^3.1.1", @@ -769,21 +757,50 @@ } } }, + "node_modules/@cardano-sdk/dapp-connector/node_modules/@cardano-sdk/util": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@cardano-sdk/util/-/util-0.17.2.tgz", + "integrity": "sha512-LdY33OQG5H4p0rppHtjnM+akc1poKhz+02T9zsXsbShfTbeU9HXlA0nJ8xj9FNZMs+ozDR0HWrUjDc7fkNZI7w==", + "license": "Apache-2.0", + "dependencies": { + "bech32": "^2.0.0", + "lodash": "^4.17.21", + "serialize-error": "^8", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@cardano-sdk/dapp-connector/node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cardano-sdk/dapp-connector/node_modules/libsodium-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.4.tgz", + "integrity": "sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==", + "license": "ISC" + }, "node_modules/@cardano-sdk/dapp-connector/node_modules/libsodium-wrappers-sumo": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.10.tgz", - "integrity": "sha512-1noz8Mcl/LUzO/iSO/FJzoJyIaPwxl+/+E4CoTIXtsPiEEXQx2sxalmrVWxteLpynqgX0ASo28ChB9NEVRh0Pg==", - "dev": true, + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.4.tgz", + "integrity": "sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==", "license": "ISC", "dependencies": { - "libsodium-sumo": "^0.7.0" + "libsodium-sumo": "^0.8.0" } }, "node_modules/@cardano-sdk/input-selection": { "version": "0.14.28", "resolved": "https://registry.npmjs.org/@cardano-sdk/input-selection/-/input-selection-0.14.28.tgz", "integrity": "sha512-pbysJUaIbbpesbv/f0XfFPKBb+bLjCmPcMfNJzpePSZBvr8bUcFpnfKtq28KthVdpe2mgL3k9ebTTcBSk7aERw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@cardano-sdk/core": "~0.46.12", @@ -798,56 +815,44 @@ } }, "node_modules/@cardano-sdk/input-selection/node_modules/@cardano-sdk/core": { - "version": "0.46.12", - "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.46.12.tgz", - "integrity": "sha512-yUA/xBUQMiMqIWiZPvIhM911pL3jNKg4PkZQ8qP9R7yU3NQ5x4RQkZ+zFDlVLxUt+gJiwIW2es0iPd8ObIKCxA==", - "dev": true, + "version": "0.46.15", + "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.46.15.tgz", + "integrity": "sha512-ubfUpDaSSEGxfmdCvV8xN7niXy1cR3HX4dCA/zJH91GpuV5XGGttI5pFbxpqDZubYHU2lw+QQ2kWJJGHQmjbog==", "license": "Apache-2.0", "dependencies": { "@biglup/is-cid": "^1.0.3", - "@cardano-ogmios/client": "6.9.0", - "@cardano-ogmios/schema": "6.9.0", - "@cardano-sdk/crypto": "~0.4.5", - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/crypto": "~0.4.7", + "@cardano-sdk/util": "~0.17.2", "@foxglove/crc": "^0.0.3", "@scure/base": "^1.1.1", "fraction.js": "4.0.1", - "ip-address": "^9.0.5", + "ip-address": "^10.2.0", "lodash": "^4.17.21", "ts-custom-error": "^3.2.0", "ts-log": "^2.2.4", "web-encoding": "^1.1.5" }, "engines": { - "node": ">=16.20.2" - }, - "peerDependencies": { - "rxjs": "^7.4.0" - }, - "peerDependenciesMeta": { - "rxjs": { - "optional": true - } + "node": ">=22" } }, "node_modules/@cardano-sdk/input-selection/node_modules/@cardano-sdk/crypto": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.5.tgz", - "integrity": "sha512-ymliqxdmen5dGVaiMVQ0VnhrwaYUjbPD3sHoMj8NI6MTuxrREp3pLJASREtWhwmv9k+QzDT6CoyuIXnlEQiWZQ==", - "dev": true, + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.7.tgz", + "integrity": "sha512-4aCsLOkqaIS/gSGw4SBqNGcCIp6hZ8alQWJx58U/OiOxC43t7i2JpKagdyWfCNekPM14hO7BWER7y31wf4BFxw==", "license": "Apache-2.0", "dependencies": { - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/util": "~0.17.2", "blake2b": "^2.1.4", "i": "^0.3.7", - "libsodium-wrappers-sumo": "0.7.10", + "libsodium-wrappers-sumo": "0.8.4", "lodash": "^4.17.21", "pbkdf2": "^3.1.3", "ts-custom-error": "^3.2.0", "ts-log": "^2.2.4" }, "engines": { - "node": ">=16.20.2" + "node": ">=22" }, "peerDependencies": { "@dcspark/cardano-multiplatform-lib-asmjs": "^3.1.1", @@ -866,27 +871,56 @@ } } }, + "node_modules/@cardano-sdk/input-selection/node_modules/@cardano-sdk/util": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@cardano-sdk/util/-/util-0.17.2.tgz", + "integrity": "sha512-LdY33OQG5H4p0rppHtjnM+akc1poKhz+02T9zsXsbShfTbeU9HXlA0nJ8xj9FNZMs+ozDR0HWrUjDc7fkNZI7w==", + "license": "Apache-2.0", + "dependencies": { + "bech32": "^2.0.0", + "lodash": "^4.17.21", + "serialize-error": "^8", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@cardano-sdk/input-selection/node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cardano-sdk/input-selection/node_modules/libsodium-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.4.tgz", + "integrity": "sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==", + "license": "ISC" + }, "node_modules/@cardano-sdk/input-selection/node_modules/libsodium-wrappers-sumo": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.10.tgz", - "integrity": "sha512-1noz8Mcl/LUzO/iSO/FJzoJyIaPwxl+/+E4CoTIXtsPiEEXQx2sxalmrVWxteLpynqgX0ASo28ChB9NEVRh0Pg==", - "dev": true, + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.4.tgz", + "integrity": "sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==", "license": "ISC", "dependencies": { - "libsodium-sumo": "^0.7.0" + "libsodium-sumo": "^0.8.0" } }, "node_modules/@cardano-sdk/key-management": { - "version": "0.29.12", - "resolved": "https://registry.npmjs.org/@cardano-sdk/key-management/-/key-management-0.29.12.tgz", - "integrity": "sha512-bctIVPg0DBCECnECIPCBfHwnF3En+AVJzpUdje+Q2a+/kryolw99i5Y7le+rpjq1LRypWUG0sUAGLY8D850epA==", - "dev": true, + "version": "0.29.18", + "resolved": "https://registry.npmjs.org/@cardano-sdk/key-management/-/key-management-0.29.18.tgz", + "integrity": "sha512-5flm1jQwLJj5dKh5WYxFgifAPcTFZ7lwLAY0Ys0WZ+25Dfp3b/4z0JH9d1OmtteIEX8O7rccQ3eEu1p1cUcl0g==", "license": "Apache-2.0", "dependencies": { - "@cardano-sdk/core": "~0.46.12", - "@cardano-sdk/crypto": "~0.4.5", - "@cardano-sdk/dapp-connector": "~0.13.26", - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/core": "~0.47.0", + "@cardano-sdk/crypto": "~0.4.7", + "@cardano-sdk/dapp-connector": "~0.13.30", + "@cardano-sdk/util": "~0.17.2", "@emurgo/cardano-message-signing-nodejs": "^1.0.1", "bip39": "^3.0.4", "chacha": "^2.1.0", @@ -898,60 +932,48 @@ "ts-log": "^2.2.4" }, "engines": { - "node": ">=16.20.2" + "node": ">=22" } }, "node_modules/@cardano-sdk/key-management/node_modules/@cardano-sdk/core": { - "version": "0.46.12", - "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.46.12.tgz", - "integrity": "sha512-yUA/xBUQMiMqIWiZPvIhM911pL3jNKg4PkZQ8qP9R7yU3NQ5x4RQkZ+zFDlVLxUt+gJiwIW2es0iPd8ObIKCxA==", - "dev": true, + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.47.0.tgz", + "integrity": "sha512-UANc6oY8Emjf5ID+Qk6XKxujPfrdxu0rDNDNy+jYJGMYh/kyUrLzOK8Ge4ydblqKQwohRXmMKagFusad3yHoDg==", "license": "Apache-2.0", "dependencies": { "@biglup/is-cid": "^1.0.3", - "@cardano-ogmios/client": "6.9.0", - "@cardano-ogmios/schema": "6.9.0", - "@cardano-sdk/crypto": "~0.4.5", - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/crypto": "~0.4.7", + "@cardano-sdk/util": "~0.17.2", "@foxglove/crc": "^0.0.3", "@scure/base": "^1.1.1", "fraction.js": "4.0.1", - "ip-address": "^9.0.5", + "ip-address": "^10.2.0", "lodash": "^4.17.21", "ts-custom-error": "^3.2.0", "ts-log": "^2.2.4", "web-encoding": "^1.1.5" }, "engines": { - "node": ">=16.20.2" - }, - "peerDependencies": { - "rxjs": "^7.4.0" - }, - "peerDependenciesMeta": { - "rxjs": { - "optional": true - } + "node": ">=22" } }, "node_modules/@cardano-sdk/key-management/node_modules/@cardano-sdk/crypto": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.5.tgz", - "integrity": "sha512-ymliqxdmen5dGVaiMVQ0VnhrwaYUjbPD3sHoMj8NI6MTuxrREp3pLJASREtWhwmv9k+QzDT6CoyuIXnlEQiWZQ==", - "dev": true, + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.7.tgz", + "integrity": "sha512-4aCsLOkqaIS/gSGw4SBqNGcCIp6hZ8alQWJx58U/OiOxC43t7i2JpKagdyWfCNekPM14hO7BWER7y31wf4BFxw==", "license": "Apache-2.0", "dependencies": { - "@cardano-sdk/util": "~0.17.1", + "@cardano-sdk/util": "~0.17.2", "blake2b": "^2.1.4", "i": "^0.3.7", - "libsodium-wrappers-sumo": "0.7.10", + "libsodium-wrappers-sumo": "0.8.4", "lodash": "^4.17.21", "pbkdf2": "^3.1.3", "ts-custom-error": "^3.2.0", "ts-log": "^2.2.4" }, "engines": { - "node": ">=16.20.2" + "node": ">=22" }, "peerDependencies": { "@dcspark/cardano-multiplatform-lib-asmjs": "^3.1.1", @@ -970,14 +992,44 @@ } } }, + "node_modules/@cardano-sdk/key-management/node_modules/@cardano-sdk/util": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@cardano-sdk/util/-/util-0.17.2.tgz", + "integrity": "sha512-LdY33OQG5H4p0rppHtjnM+akc1poKhz+02T9zsXsbShfTbeU9HXlA0nJ8xj9FNZMs+ozDR0HWrUjDc7fkNZI7w==", + "license": "Apache-2.0", + "dependencies": { + "bech32": "^2.0.0", + "lodash": "^4.17.21", + "serialize-error": "^8", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@cardano-sdk/key-management/node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cardano-sdk/key-management/node_modules/libsodium-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.4.tgz", + "integrity": "sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==", + "license": "ISC" + }, "node_modules/@cardano-sdk/key-management/node_modules/libsodium-wrappers-sumo": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.10.tgz", - "integrity": "sha512-1noz8Mcl/LUzO/iSO/FJzoJyIaPwxl+/+E4CoTIXtsPiEEXQx2sxalmrVWxteLpynqgX0ASo28ChB9NEVRh0Pg==", - "dev": true, + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.4.tgz", + "integrity": "sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==", "license": "ISC", "dependencies": { - "libsodium-sumo": "^0.7.0" + "libsodium-sumo": "^0.8.0" } }, "node_modules/@cardano-sdk/util": { @@ -1211,7 +1263,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emurgo/cardano-message-signing-nodejs/-/cardano-message-signing-nodejs-1.1.0.tgz", "integrity": "sha512-PQRc8K8wZshEdmQenNUzVtiI8oJNF/1uAnBhidee5C4o1l2mDLOW+ur46HWHIFKQ6x8mSJTllcjMscHgzju0gQ==", - "dev": true, "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { @@ -1739,7 +1790,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@harmoniclabs/bigint-utils/-/bigint-utils-1.0.0.tgz", "integrity": "sha512-OhZMHcdtH2hHKMlxWFHf71PmKHdoi9ARpjS9mUu0/cd8VWDDjT7VQoQwC5NN/68iyO4O5Dojrvrp9tjG5BDABA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@harmoniclabs/uint8array-utils": "^1.0.0" @@ -1749,14 +1799,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@harmoniclabs/biguint/-/biguint-1.0.0.tgz", "integrity": "sha512-5DyCIBDL4W+7ffR1IJSbGrCG4xEYxAlFH5gCNF42qtyL5ltwZ92Ae1MyXpHM2TUPy7ocSTqlLUsOdy+SvqVVPw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/@harmoniclabs/bitstream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@harmoniclabs/bitstream/-/bitstream-1.0.0.tgz", "integrity": "sha512-Ed/I46IuCiytE5QiMmmUo9kPJcypM7OuUqoRaAXUALL5C6LKLpT6kYE1qeuhLkx2/WvkHT18jcOX6jhM/nmqoA==", - "dev": true, "license": "MIT", "dependencies": { "@harmoniclabs/uint8array-utils": "^1.0.0" @@ -1789,7 +1837,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@harmoniclabs/crypto/-/crypto-0.3.0.tgz", "integrity": "sha512-UvmGQOLFVFhRIDYLpcWbPQLXl9advCt0h02Z/BtBuXtHiy35WRxKQ3njcUKI0v6zGITuvqQhsf6VOPMeekLdeA==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1807,14 +1854,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@harmoniclabs/pair/-/pair-1.0.0.tgz", "integrity": "sha512-D9OBowsUsy1LctHxWzd9AngTzoo5x3rBiJ0gu579t41Q23pb+VNx1euEfluUEiaYbgljcl1lb/4D1fFTZd1tRQ==", - "dev": true, "license": "MIT" }, "node_modules/@harmoniclabs/plutus-data": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@harmoniclabs/plutus-data/-/plutus-data-1.2.6.tgz", "integrity": "sha512-rF046GZ07XDpjZBNybALKYSycjxCLzXKbhLylu9pRuZiii5fVXReEfgtLB29TsPBvGY6ZBeiyHgJnLgm+huZBw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@harmoniclabs/biguint": "^1.0.0", @@ -1830,7 +1875,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/@harmoniclabs/crypto/-/crypto-0.2.5.tgz", "integrity": "sha512-t2saWMFWBx8tOHotiYTTfQKhPGpWT4AMLXxq3u0apShVXNV0vgL0gEgSMudBjES/wrKByCqa2xmU70gadz26hA==", - "dev": true, "license": "MIT", "dependencies": { "@harmoniclabs/bitstream": "^1.0.0", @@ -1847,7 +1891,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@harmoniclabs/uplc/-/uplc-1.4.1.tgz", "integrity": "sha512-sELKStjxPBPBxBMylU4oBSUe0/8eJe2HqRblNSwrMu8Fso4YpSPDqHZ33iDZ8QAadVUsT5r2EQKX0TLrj7qXvQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@harmoniclabs/bigint-utils": "^1.0.0", @@ -2710,36 +2753,99 @@ } }, "node_modules/@meshsdk/core-cst": { - "version": "1.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@meshsdk/core-cst/-/core-cst-1.9.0-beta.99.tgz", - "integrity": "sha512-sAdxOUqz3Qo7+6uz+/gD/uebWcwR6Eljybc2zvJT6QZBsJYeM1UCKO6ui15yedqEfnnDxdQJYSfRtXZPR1FXag==", - "dev": true, + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@meshsdk/core-cst/-/core-cst-1.9.1.tgz", + "integrity": "sha512-1e72mtTehjV/XQWpxtnejhEXyhU1f8CWqoTaQfq2rfBydLJfFs/NDEYfBKBOx697MOWM7VPAHjLvVAENqS57EQ==", "license": "Apache-2.0", "dependencies": { - "@cardano-sdk/core": "^0.46.11", - "@cardano-sdk/crypto": "^0.4.4", - "@cardano-sdk/input-selection": "^0.14.27", - "@cardano-sdk/util": "^0.17.1", + "@cardano-sdk/core": "0.46.12", + "@cardano-sdk/crypto": "0.4.5", + "@cardano-sdk/input-selection": "0.14.28", + "@cardano-sdk/util": "0.17.1", "@harmoniclabs/cbor": "1.6.0", "@harmoniclabs/pair": "^1.0.0", "@harmoniclabs/plutus-data": "1.2.6", "@harmoniclabs/uplc": "1.4.1", - "@meshsdk/common": "1.9.0-beta.99", + "@meshsdk/common": "1.9.1", "@types/base32-encoding": "^1.0.2", "base32-encoding": "^1.0.0", "bech32": "^2.0.0", "blakejs": "^1.2.1", "bn.js": "^5.2.0", - "hash.js": "^1.1.7", - "libsodium-wrappers-sumo": "0.7.15", - "scalus": "^0.14.2" + "hash.js": "^1.1.7" + } + }, + "node_modules/@meshsdk/core-cst/node_modules/@cardano-sdk/core": { + "version": "0.46.12", + "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.46.12.tgz", + "integrity": "sha512-yUA/xBUQMiMqIWiZPvIhM911pL3jNKg4PkZQ8qP9R7yU3NQ5x4RQkZ+zFDlVLxUt+gJiwIW2es0iPd8ObIKCxA==", + "license": "Apache-2.0", + "dependencies": { + "@biglup/is-cid": "^1.0.3", + "@cardano-ogmios/client": "6.9.0", + "@cardano-ogmios/schema": "6.9.0", + "@cardano-sdk/crypto": "~0.4.5", + "@cardano-sdk/util": "~0.17.1", + "@foxglove/crc": "^0.0.3", + "@scure/base": "^1.1.1", + "fraction.js": "4.0.1", + "ip-address": "^9.0.5", + "lodash": "^4.17.21", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4", + "web-encoding": "^1.1.5" + }, + "engines": { + "node": ">=16.20.2" + }, + "peerDependencies": { + "rxjs": "^7.4.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "node_modules/@meshsdk/core-cst/node_modules/@cardano-sdk/crypto": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.5.tgz", + "integrity": "sha512-ymliqxdmen5dGVaiMVQ0VnhrwaYUjbPD3sHoMj8NI6MTuxrREp3pLJASREtWhwmv9k+QzDT6CoyuIXnlEQiWZQ==", + "license": "Apache-2.0", + "dependencies": { + "@cardano-sdk/util": "~0.17.1", + "blake2b": "^2.1.4", + "i": "^0.3.7", + "libsodium-wrappers-sumo": "0.7.10", + "lodash": "^4.17.21", + "pbkdf2": "^3.1.3", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4" + }, + "engines": { + "node": ">=16.20.2" + }, + "peerDependencies": { + "@dcspark/cardano-multiplatform-lib-asmjs": "^3.1.1", + "@dcspark/cardano-multiplatform-lib-browser": "^3.1.1", + "@dcspark/cardano-multiplatform-lib-nodejs": "^3.1.1" + }, + "peerDependenciesMeta": { + "@dcspark/cardano-multiplatform-lib-asmjs": { + "optional": true + }, + "@dcspark/cardano-multiplatform-lib-browser": { + "optional": true + }, + "@dcspark/cardano-multiplatform-lib-nodejs": { + "optional": true + } } }, "node_modules/@meshsdk/core-cst/node_modules/@harmoniclabs/cbor": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@harmoniclabs/cbor/-/cbor-1.6.0.tgz", "integrity": "sha512-KI25p8pHI1rmFZC9NYSxATwlCZ+KJdjydpptKebHcw03Iy7M+E8mF+hSnN5dTbS45xw5ZyKUgPLRgLo1sTuIoQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@harmoniclabs/bytestring": "^1.0.0", @@ -2751,10 +2857,9 @@ } }, "node_modules/@meshsdk/core-cst/node_modules/@meshsdk/common": { - "version": "1.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@meshsdk/common/-/common-1.9.0-beta.99.tgz", - "integrity": "sha512-ySo/s+rXJn3agNEJ6Rtmoo8KtgmohW81/9GU/QtJb1nqmjwRB8CJfJUZypxZ+CtdRSRGf9OupwBL+9mbTIPlrA==", - "dev": true, + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@meshsdk/common/-/common-1.9.1.tgz", + "integrity": "sha512-66l7vcY+h9w71auVaE7RLJYs87DmO+JTEmQ2xknd6F8fAcR73nMxLZunMRbygPoV9gHDBTMYfHoMBZqWt7N6EA==", "license": "Apache-2.0", "dependencies": { "bech32": "^2.0.0", @@ -2764,41 +2869,146 @@ } }, "node_modules/@meshsdk/core-cst/node_modules/libsodium-wrappers-sumo": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", - "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", - "dev": true, + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.10.tgz", + "integrity": "sha512-1noz8Mcl/LUzO/iSO/FJzoJyIaPwxl+/+E4CoTIXtsPiEEXQx2sxalmrVWxteLpynqgX0ASo28ChB9NEVRh0Pg==", "license": "ISC", "dependencies": { - "libsodium-sumo": "^0.7.15" + "libsodium-sumo": "^0.7.0" } }, "node_modules/@meshsdk/provider": { - "version": "1.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@meshsdk/provider/-/provider-1.9.0-beta.99.tgz", - "integrity": "sha512-Aa5vlByjDeUYJMfhqnuJ53VfoSFmD+k5P42HVIQPuyrA02txzdh6NFwh0NIxFDL+pMJer9vo27D7Jsc+ShAuoA==", + "version": "1.9.0-beta.102", + "resolved": "https://registry.npmjs.org/@meshsdk/provider/-/provider-1.9.0-beta.102.tgz", + "integrity": "sha512-A+iGVouE2Hp/lUwM3OvmSom9FEgjLQdaQ6dvPZzi4Htlg1FDjdS6eKIEbWwdiv5Z7Scx4hINq29urvD6XISCMA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.99", - "@meshsdk/core-cst": "1.9.0-beta.99", + "@meshsdk/common": "1.9.0-beta.100", + "@meshsdk/core-cst": "1.9.0-beta.100", "@utxorpc/sdk": "^0.6.7", "@utxorpc/spec": "^0.16.0", "axios": "^1.7.2", "cbor": "^10.0.9" } }, - "node_modules/@meshsdk/provider/node_modules/@meshsdk/common": { - "version": "1.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@meshsdk/common/-/common-1.9.0-beta.99.tgz", - "integrity": "sha512-ySo/s+rXJn3agNEJ6Rtmoo8KtgmohW81/9GU/QtJb1nqmjwRB8CJfJUZypxZ+CtdRSRGf9OupwBL+9mbTIPlrA==", + "node_modules/@meshsdk/provider/node_modules/@cardano-sdk/core": { + "version": "0.46.12", + "resolved": "https://registry.npmjs.org/@cardano-sdk/core/-/core-0.46.12.tgz", + "integrity": "sha512-yUA/xBUQMiMqIWiZPvIhM911pL3jNKg4PkZQ8qP9R7yU3NQ5x4RQkZ+zFDlVLxUt+gJiwIW2es0iPd8ObIKCxA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "bech32": "^2.0.0", - "bip39": "3.1.0", + "@biglup/is-cid": "^1.0.3", + "@cardano-ogmios/client": "6.9.0", + "@cardano-ogmios/schema": "6.9.0", + "@cardano-sdk/crypto": "~0.4.5", + "@cardano-sdk/util": "~0.17.1", + "@foxglove/crc": "^0.0.3", + "@scure/base": "^1.1.1", + "fraction.js": "4.0.1", + "ip-address": "^9.0.5", + "lodash": "^4.17.21", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4", + "web-encoding": "^1.1.5" + }, + "engines": { + "node": ">=16.20.2" + }, + "peerDependencies": { + "rxjs": "^7.4.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "node_modules/@meshsdk/provider/node_modules/@cardano-sdk/crypto": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@cardano-sdk/crypto/-/crypto-0.4.5.tgz", + "integrity": "sha512-ymliqxdmen5dGVaiMVQ0VnhrwaYUjbPD3sHoMj8NI6MTuxrREp3pLJASREtWhwmv9k+QzDT6CoyuIXnlEQiWZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cardano-sdk/util": "~0.17.1", "blake2b": "^2.1.4", - "blakejs": "^1.2.1" + "i": "^0.3.7", + "libsodium-wrappers-sumo": "0.7.10", + "lodash": "^4.17.21", + "pbkdf2": "^3.1.3", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4" + }, + "engines": { + "node": ">=16.20.2" + }, + "peerDependencies": { + "@dcspark/cardano-multiplatform-lib-asmjs": "^3.1.1", + "@dcspark/cardano-multiplatform-lib-browser": "^3.1.1", + "@dcspark/cardano-multiplatform-lib-nodejs": "^3.1.1" + }, + "peerDependenciesMeta": { + "@dcspark/cardano-multiplatform-lib-asmjs": { + "optional": true + }, + "@dcspark/cardano-multiplatform-lib-browser": { + "optional": true + }, + "@dcspark/cardano-multiplatform-lib-nodejs": { + "optional": true + } + } + }, + "node_modules/@meshsdk/provider/node_modules/@harmoniclabs/cbor": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@harmoniclabs/cbor/-/cbor-1.6.0.tgz", + "integrity": "sha512-KI25p8pHI1rmFZC9NYSxATwlCZ+KJdjydpptKebHcw03Iy7M+E8mF+hSnN5dTbS45xw5ZyKUgPLRgLo1sTuIoQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@harmoniclabs/bytestring": "^1.0.0", + "@harmoniclabs/obj-utils": "^1.0.0", + "@harmoniclabs/uint8array-utils": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/HarmonicLabs" + } + }, + "node_modules/@meshsdk/provider/node_modules/@meshsdk/core-cst": { + "version": "1.9.0-beta.100", + "resolved": "https://registry.npmjs.org/@meshsdk/core-cst/-/core-cst-1.9.0-beta.100.tgz", + "integrity": "sha512-gXC7c81puzv12C3xJ6vhH/KIEc/P6ScuXsgmLlqFMpDv0SuoMg+42HgdyWi0WrccVwi8cdepsn5YhtCaYVn0nw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cardano-sdk/core": "0.46.12", + "@cardano-sdk/crypto": "0.4.5", + "@cardano-sdk/input-selection": "0.14.28", + "@cardano-sdk/util": "0.17.1", + "@harmoniclabs/cbor": "1.6.0", + "@harmoniclabs/pair": "^1.0.0", + "@harmoniclabs/plutus-data": "1.2.6", + "@harmoniclabs/uplc": "1.4.1", + "@meshsdk/common": "1.9.0-beta.100", + "@types/base32-encoding": "^1.0.2", + "base32-encoding": "^1.0.0", + "bech32": "^2.0.0", + "blakejs": "^1.2.1", + "bn.js": "^5.2.0", + "hash.js": "^1.1.7", + "scalus": "^0.14.2" + } + }, + "node_modules/@meshsdk/provider/node_modules/libsodium-wrappers-sumo": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.10.tgz", + "integrity": "sha512-1noz8Mcl/LUzO/iSO/FJzoJyIaPwxl+/+E4CoTIXtsPiEEXQx2sxalmrVWxteLpynqgX0ASo28ChB9NEVRh0Pg==", + "dev": true, + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.7.0" } }, "node_modules/@multiformats/dns": { @@ -4038,7 +4248,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4148,10 +4357,9 @@ "license": "MIT" }, "node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", - "dev": true, + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -4428,7 +4636,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/chacha/-/chacha-2.1.0.tgz", "integrity": "sha512-FhVtqaZOiHlOKUkAWfDlJ+oe/O8iPQbCC0pFXJqphr4YQBCZPXa8Mv3j35+W4eWFWCoTUcW2u5IWDDkknygvVA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -4442,7 +4649,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/chacha-native/-/chacha-native-2.0.3.tgz", "integrity": "sha512-93h+osfjhR2sMHAaapTLlL/COoBPEZ6upicPBQ4GfUyadoMb8t9/M0PKK8kC+F+DEA/Oy3Kg9w3HzY3J1foP3g==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4977,8 +5183,7 @@ "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", - "dev": true + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, "node_modules/dotenv": { "version": "16.6.1", @@ -5505,7 +5710,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, "license": "MIT", "optional": true }, @@ -5771,7 +5975,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/get-random-values/-/get-random-values-2.1.0.tgz", "integrity": "sha512-q2yOLpLyA8f9unfv2LV8KVRUFeOIrQVS5cnqpbv6N+ea9j1rmW5dFKj/2Q7CK3juEfDYQgPxGt941VJcmw0jKg==", - "dev": true, "license": "MIT", "dependencies": { "global": "^4.4.0" @@ -5832,7 +6035,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dev": true, "license": "MIT", "dependencies": { "min-document": "^2.19.0", @@ -6013,7 +6215,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -8104,7 +8305,6 @@ "version": "2.19.2", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", - "dev": true, "license": "MIT", "dependencies": { "dom-walk": "^0.1.0" @@ -8114,7 +8314,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, "license": "ISC" }, "node_modules/minimatch": { @@ -8179,10 +8378,9 @@ } }, "node_modules/nan": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", - "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", - "dev": true, + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", "license": "MIT", "optional": true }, @@ -8854,7 +9052,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -8998,7 +9195,6 @@ "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -9011,7 +9207,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, "license": "MIT" }, "node_modules/readdirp": { @@ -9250,7 +9445,6 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -9511,7 +9705,6 @@ "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, "license": "MIT" }, "node_modules/string-length": { @@ -10034,7 +10227,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, "license": "0BSD" }, "node_modules/tsup": { @@ -10588,7 +10780,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.8.0.tgz", "integrity": "sha512-a19+DzlT6Kp9/UI+mF9XQopeZ+n2ussjhxHJ4/pmIGge9ijCDz7Gn93mNnjpZAk95T4Tae8iHZ6sSf869txqiQ==", - "dev": true, "license": "MPL-2.0" }, "node_modules/webidl-conversions": { diff --git a/package.json b/package.json index 29b9ccd..e368f75 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.4.1", "@meshsdk/common": "1.9.0-beta.100", - "@meshsdk/provider": "1.9.0-beta.99", + "@meshsdk/provider": "^1.9.0-beta.102", "@types/jest": "^29.5.14", "@types/mocha": "^10.0.10", "dotenv": "^16.4.5", @@ -50,6 +50,7 @@ "@cardano-sdk/crypto": "0.4.4", "@cardano-sdk/util": "0.17.1", "@harmoniclabs/cbor": "^1.6.6", + "@meshsdk/core-cst": "^1.9.1", "@simplewebauthn/browser": "^13.0.0", "@types/base32-encoding": "^1.0.2", "@types/bn.js": "^5.1.5", diff --git a/src/bitcoin/interfaces/bitcoin-provider.ts b/src/bitcoin/interfaces/bitcoin-provider.ts index d246a8c..8490db3 100644 --- a/src/bitcoin/interfaces/bitcoin-provider.ts +++ b/src/bitcoin/interfaces/bitcoin-provider.ts @@ -1,35 +1,5 @@ -import { AddressInfo } from "../types/address-info"; -import { ScriptInfo } from "../types/script-info"; -import { TransactionsInfo } from "../types/transactions-info"; -import { TransactionsStatus } from "../types/transactions-status"; -import { UTxO } from "../types/utxo"; - -/** - * Read-only chain data queries for Bitcoin addresses and scripts. - * Mirrors the Mesh `IFetcher` interface shape for Cardano, adapted to Bitcoin. - * Only the methods relevant to Bitcoin are included; script variants are optional - * because P2SH/P2WSH usage is uncommon in most dApp workflows. - */ -export interface IBitcoinFetcher { - fetchAddressInfo(address: string): Promise; - fetchAddressUTxOs(address: string): Promise; - fetchUTxO(txid: string, vout?: number): Promise; - fetchAddressTxs( - address: string, - lastSeenTxid?: string, - ): Promise; - fetchTxInfo(txid: string): Promise; - fetchFeeEstimates(blocks: number): Promise; - fetchScriptInfo?(hash: string): Promise; - fetchScriptUTxOs?(hash: string): Promise; - fetchScriptTxs?( - hash: string, - lastSeenTxid?: string, - ): Promise; -} - -export interface IBitcoinSubmitter { - submitTx(tx: string): Promise; -} - -export interface IBitcoinProvider extends IBitcoinFetcher, IBitcoinSubmitter {} +export type { + IBitcoinFetcher, + IBitcoinSubmitter, + IBitcoinProvider, +} from "@meshsdk/provider"; diff --git a/src/bitcoin/utils/coin-selection.ts b/src/bitcoin/utils/coin-selection.ts new file mode 100644 index 0000000..5a7f1b9 --- /dev/null +++ b/src/bitcoin/utils/coin-selection.ts @@ -0,0 +1,95 @@ +/** + * Coin selection for Bitcoin transactions. + * + * Operates on the minimal outpoint shape (`txid`/`vout`/`value`) so it accepts + * both full `UTxO` objects returned by an `IBitcoinProvider` and any + * caller-constructed input set. + */ + +/** + * Minimal UTxO shape required for coin selection. + */ +export type SelectableUTxO = { + txid: string; + vout: number; + value: number; +}; + +export type CoinSelectionResult = { + selectedUtxos: T[]; + change: number; +}; + +export type CoinSelectionStrategy = ( + utxos: T[], + targetAmount: number, + feeRate: number, + numRecipients: number, +) => CoinSelectionResult; + +/** + * P2WPKH dust threshold under default relay policy (Bitcoin Core ~0.21+): + * outputs below this value are non-standard and the tx will fail to relay. + * 546 sats matches `GetDustThreshold` for a P2WPKH output at the default + * 3000 sat/kvB dust feerate. Absorb anything below this into the miner fee. + */ +export const DUST_THRESHOLD_P2WPKH = 546; + +/** + * Largest-first UTXO selection with P2WPKH-realistic vbyte estimates. + * + * vbyte math (BIP-141 / BIP-144): + * - overhead: 10.5 vB (version 4 + locktime 4 + segwit marker/flag 0.5 + 2× varint <253) + * - per P2WPKH input: 68 vB (outpoint 41 + script_len 1 + sequence 0 + witness ~27) + * - per output: 31 vB (value 8 + script_len 1 + scriptPubKey 22) + * + * Tries with-change first; if change would be sub-dust, retries without a + * change output (one fewer 31 vB output) so the dust is consumed as fee. + * + * @param utxos - Candidate UTxOs (e.g. from `IBitcoinProvider.fetchAddressUTxOs`). + * @param targetAmount - Total amount to send across all recipients, in satoshis. + * @param feeRate - Fee rate in satoshis per vByte. + * @param numRecipients - Number of recipient outputs in the transaction. + * @returns The selected UTxOs and the change value (0 when no change output). + * @throws When the UTxO set cannot cover `targetAmount` plus fees. + */ +export function selectUtxosLargestFirst( + utxos: T[], + targetAmount: number, + feeRate: number, + numRecipients: number, +): CoinSelectionResult { + const VB_OVERHEAD = 11; + const VB_INPUT_P2WPKH = 68; + const VB_OUTPUT = 31; + const recipientsVb = numRecipients * VB_OUTPUT; + + const sorted = [...utxos].sort((a, b) => b.value - a.value); + let selectedValue = 0; + const selected: T[] = []; + + for (const utxo of sorted) { + selected.push(utxo); + selectedValue += utxo.value; + const inputsVb = selected.length * VB_INPUT_P2WPKH; + const vbytesWithChange = VB_OVERHEAD + inputsVb + recipientsVb + VB_OUTPUT; + const vbytesNoChange = VB_OVERHEAD + inputsVb + recipientsVb; + const feeWithChange = Math.ceil(vbytesWithChange * feeRate); + const feeNoChange = Math.ceil(vbytesNoChange * feeRate); + + if (selectedValue >= targetAmount + feeWithChange) { + const change = selectedValue - targetAmount - feeWithChange; + if (change >= DUST_THRESHOLD_P2WPKH) { + return { selectedUtxos: selected, change }; + } + // Change would be dust — drop the change output, absorb dust as fee. + if (selectedValue >= targetAmount + feeNoChange) { + return { selectedUtxos: selected, change: 0 }; + } + } else if (selectedValue >= targetAmount + feeNoChange) { + // Just enough for a no-change tx. + return { selectedUtxos: selected, change: 0 }; + } + } + throw new Error("[CoinSelection] Insufficient funds for transaction"); +} diff --git a/src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts b/src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts index a5fe419..647b37a 100644 --- a/src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts +++ b/src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts @@ -8,7 +8,7 @@ import { toXOnly, } from "../../address/bitcoin-address"; import { BitcoinAddressManager } from "../../address/bitcoin-address-manager"; -import { IBitcoinProvider } from "../../interfaces/bitcoin-provider"; +import type { IBitcoinProvider } from "@meshsdk/provider"; import { AddressPurpose, BitcoinAccount, @@ -22,6 +22,10 @@ import { import { RecoveryId } from "../../types/recoveryId"; import { TransactionsInfo } from "../../types/transactions-info"; import { UTxO } from "../../types/utxo"; +import { + CoinSelectionStrategy, + selectUtxosLargestFirst, +} from "../../utils/coin-selection"; import { bip32, bip39, bitcoin, ecc, ECPair } from "../core/bitcoin-core"; export interface BitcoinHeadlessWalletConfig { @@ -29,6 +33,7 @@ export interface BitcoinHeadlessWalletConfig { provider?: IBitcoinProvider; password?: string; account?: number; + coinSelection?: CoinSelectionStrategy; } interface InternalConfig { @@ -38,6 +43,7 @@ interface InternalConfig { manager: BitcoinAddressManager; provider?: IBitcoinProvider; account: number; + coinSelection: CoinSelectionStrategy; } /** @@ -55,6 +61,7 @@ export class BitcoinHeadlessWallet implements IBitcoinWallet { protected readonly manager: BitcoinAddressManager; protected readonly provider?: IBitcoinProvider; protected readonly account: number; + protected readonly coinSelection: CoinSelectionStrategy; protected constructor(cfg: InternalConfig) { this.networkName = cfg.network; @@ -63,6 +70,7 @@ export class BitcoinHeadlessWallet implements IBitcoinWallet { this.manager = cfg.manager; this.provider = cfg.provider; this.account = cfg.account; + this.coinSelection = cfg.coinSelection; } /** @@ -84,6 +92,7 @@ export class BitcoinHeadlessWallet implements IBitcoinWallet { manager, provider: config.provider, account: config.account ?? 0, + coinSelection: config.coinSelection ?? selectUtxosLargestFirst, }); } @@ -448,7 +457,7 @@ export class BitcoinHeadlessWallet implements IBitcoinWallet { } const targetAmount = recipients.reduce((sum, r) => sum + r.amount, 0); - const { selectedUtxos, change } = selectUtxosLargestFirst( + const { selectedUtxos, change } = this.coinSelection( utxos, targetAmount, feeRate, @@ -655,14 +664,6 @@ export class BitcoinHeadlessWallet implements IBitcoinWallet { // Helpers (module-scope, not exported as public API) // ----------------------------------------------------------------------------- -/** - * P2WPKH dust threshold under default relay policy (Bitcoin Core ~0.21+): - * outputs below this value are non-standard and the tx will fail to relay. - * 546 sats matches `GetDustThreshold` for a P2WPKH output at the default - * 3000 sat/kvB dust feerate. Absorb anything below this into the miner fee. - */ -const DUST_THRESHOLD_P2WPKH = 546; - /** * Build per-address sign-target index lists by inspecting each PSBT input's * witnessUtxo script type. Each input is assigned to the single signer that @@ -710,58 +711,6 @@ function buildDefaultSignTargets( return out; } -/** - * Largest-first UTXO selection with P2WPKH-realistic vbyte estimates. - * - * vbyte math (BIP-141 / BIP-144): - * - overhead: 10.5 vB (version 4 + locktime 4 + segwit marker/flag 0.5 + 2× varint <253) - * - per P2WPKH input: 68 vB (outpoint 41 + script_len 1 + sequence 0 + witness ~27) - * - per output: 31 vB (value 8 + script_len 1 + scriptPubKey 22) - * - * Tries with-change first; if change would be sub-dust, retries without a - * change output (one fewer 31 vB output) so the dust is consumed as fee. - */ -function selectUtxosLargestFirst( - utxos: { txid: string; vout: number; value: number }[], - targetAmount: number, - feeRate: number, - numRecipients: number, -): { selectedUtxos: typeof utxos; change: number } { - const VB_OVERHEAD = 11; - const VB_INPUT_P2WPKH = 68; - const VB_OUTPUT = 31; - const recipientsVb = numRecipients * VB_OUTPUT; - - const sorted = [...utxos].sort((a, b) => b.value - a.value); - let selectedValue = 0; - const selected: typeof utxos = []; - - for (const utxo of sorted) { - selected.push(utxo); - selectedValue += utxo.value; - const inputsVb = selected.length * VB_INPUT_P2WPKH; - const vbytesWithChange = VB_OVERHEAD + inputsVb + recipientsVb + VB_OUTPUT; - const vbytesNoChange = VB_OVERHEAD + inputsVb + recipientsVb; - const feeWithChange = Math.ceil(vbytesWithChange * feeRate); - const feeNoChange = Math.ceil(vbytesNoChange * feeRate); - - if (selectedValue >= targetAmount + feeWithChange) { - const change = selectedValue - targetAmount - feeWithChange; - if (change >= DUST_THRESHOLD_P2WPKH) { - return { selectedUtxos: selected, change }; - } - // Change would be dust — drop the change output, absorb dust as fee. - if (selectedValue >= targetAmount + feeNoChange) { - return { selectedUtxos: selected, change: 0 }; - } - } else if (selectedValue >= targetAmount + feeNoChange) { - // Just enough for a no-change tx. - return { selectedUtxos: selected, change: 0 }; - } - } - throw new Error("[BitcoinHeadlessWallet] Insufficient funds for transaction"); -} - /** * Recover the recoveryId (0 or 1) for an ECDSA signature given the message hash and * the expected compressed public key. Returns the id whose point recovery matches diff --git a/src/index.ts b/src/index.ts index 58bdf9f..cc99c4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ export type { } from "./bitcoin/interfaces/bitcoin-wallet"; export * from "./bitcoin/interfaces/bitcoin-provider"; export * from "./bitcoin/types"; +export * from "./bitcoin/utils/coin-selection"; export * from "./bitcoin/address/bitcoin-address"; export * from "./bitcoin/address/bitcoin-address-manager"; export * from "./bitcoin/wallet/mesh/bitcoin-headless-wallet"; diff --git a/test/bitcoin/coin-selection.test.ts b/test/bitcoin/coin-selection.test.ts new file mode 100644 index 0000000..b76ae37 --- /dev/null +++ b/test/bitcoin/coin-selection.test.ts @@ -0,0 +1,137 @@ +import { + DUST_THRESHOLD_P2WPKH, + selectUtxosLargestFirst, +} from "../../src/bitcoin/utils/coin-selection"; + +const makeUtxo = (value: number, i = 0) => ({ + txid: `${i}`.padStart(64, "0"), + vout: i, + value, +}); + +/** + * vbyte model used by the selector (see coin-selection.ts): + * overhead 11 + 68/input + 31/output. + * 1 input, 1 recipient, with change: 11 + 68 + 31 + 31 = 141 vB. + * 1 input, 1 recipient, no change: 11 + 68 + 31 = 110 vB. + */ +describe("selectUtxosLargestFirst", () => { + it("selects the largest UTxO first", () => { + const utxos = [ + makeUtxo(10_000, 0), + makeUtxo(90_000, 1), + makeUtxo(50_000, 2), + ]; + const { selectedUtxos } = selectUtxosLargestFirst(utxos, 20_000, 1, 1); + expect(selectedUtxos).toHaveLength(1); + expect(selectedUtxos[0]!.value).toBe(90_000); + }); + + it("accumulates multiple UTxOs until target plus fee is covered", () => { + const utxos = [ + makeUtxo(30_000, 0), + makeUtxo(30_000, 1), + makeUtxo(30_000, 2), + ]; + const { selectedUtxos, change } = selectUtxosLargestFirst( + utxos, + 55_000, + 1, + 1, + ); + expect(selectedUtxos).toHaveLength(2); + // 2 inputs, with change: 11 + 2*68 + 31 + 31 = 209 vB → fee 209 sats. + expect(change).toBe(60_000 - 55_000 - 209); + }); + + it("returns exact change matching the vbyte fee model", () => { + const { change } = selectUtxosLargestFirst( + [makeUtxo(100_000)], + 10_000, + 1, + 1, + ); + // 1 input, with change: 141 vB → fee 141 sats. + expect(change).toBe(100_000 - 10_000 - 141); + }); + + it("scales fee with the fee rate", () => { + const { change } = selectUtxosLargestFirst( + [makeUtxo(100_000)], + 10_000, + 10, + 1, + ); + expect(change).toBe(100_000 - 10_000 - 1_410); + }); + + it("accounts for extra recipient outputs in the fee", () => { + const { change } = selectUtxosLargestFirst( + [makeUtxo(100_000)], + 10_000, + 1, + 3, + ); + // 1 input, 3 recipients, with change: 11 + 68 + 3*31 + 31 = 203 vB. + expect(change).toBe(100_000 - 10_000 - 203); + }); + + it("drops sub-dust change and absorbs it as fee", () => { + // 10_700 - 10_500 - 141 = 59 → below dust; no-change fee 110 still covered. + const { selectedUtxos, change } = selectUtxosLargestFirst( + [makeUtxo(10_700)], + 10_500, + 1, + 1, + ); + expect(selectedUtxos).toHaveLength(1); + expect(change).toBe(0); + }); + + it("keeps change exactly at the dust threshold", () => { + // Choose value so change is exactly 546: 10_000 + 141 + 546 = 10_687. + const { change } = selectUtxosLargestFirst( + [makeUtxo(10_687)], + 10_000, + 1, + 1, + ); + expect(change).toBe(DUST_THRESHOLD_P2WPKH); + }); + + it("selects a no-change tx when value only covers the smaller fee", () => { + // Covers target + no-change fee (110) but not with-change fee (141). + const { change } = selectUtxosLargestFirst( + [makeUtxo(10_120)], + 10_000, + 1, + 1, + ); + expect(change).toBe(0); + }); + + it("throws when the UTxO set cannot cover target plus fee", () => { + expect(() => + selectUtxosLargestFirst([makeUtxo(10_000)], 10_000, 1, 1), + ).toThrow(/Insufficient funds/); + }); + + it("throws on an empty UTxO set", () => { + expect(() => selectUtxosLargestFirst([], 1_000, 1, 1)).toThrow( + /Insufficient funds/, + ); + }); + + it("does not mutate the input array", () => { + const utxos = [makeUtxo(1_000, 0), makeUtxo(90_000, 1)]; + const snapshot = [...utxos]; + selectUtxosLargestFirst(utxos, 10_000, 1, 1); + expect(utxos).toEqual(snapshot); + }); + + it("preserves extra properties on selected UTxOs (generic passthrough)", () => { + const utxos = [{ ...makeUtxo(90_000), address: "tb1q..." }]; + const { selectedUtxos } = selectUtxosLargestFirst(utxos, 10_000, 1, 1); + expect(selectedUtxos[0]!.address).toBe("tb1q..."); + }); +}); diff --git a/test/setup-sodium.ts b/test/setup-sodium.ts new file mode 100644 index 0000000..816614f --- /dev/null +++ b/test/setup-sodium.ts @@ -0,0 +1,7 @@ +import sodium from "libsodium-wrappers-sumo"; + +// libsodium-wrappers-sumo initializes asynchronously; ensure it is ready +// before any dependency (e.g. @meshsdk/provider) loads a duplicate copy. +beforeAll(async () => { + await sodium.ready; +});