Skip to content

Commit 6eba965

Browse files
authored
Merge pull request #10 from MeshJS/feature/bitcoin-maestro-provider
feat(bitcoin): Maestro data provider and first-class coin selection
2 parents 284b473 + 6c4a5ca commit 6eba965

11 files changed

Lines changed: 781 additions & 310 deletions

File tree

‎README.md‎

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ npm install @meshsdk/wallet
77
```
88

99
> **Migrating from v1 (`MeshWallet` or `BrowserWallet`)?** This version has breaking changes. See:
10+
>
1011
> - [`mesh-wallet-migration.md`](./mesh-wallet-migration.md) — for `MeshWallet` to `MeshCardanoHeadlessWallet`
1112
> - [`browser-wallet-migration.md`](./browser-wallet-migration.md) — for `BrowserWallet` to `MeshCardanoBrowserWallet`
1213
@@ -19,6 +20,7 @@ npm install @meshsdk/wallet
1920
- [Headless Wallet (Server-Side)](#headless-wallet-server-side)
2021
- [Simulated CIP-30 API (headless)](#simulated-cip-30-api-headless)
2122
- [Browser Wallet (Client-Side)](#browser-wallet-client-side)
23+
- [Bitcoin Headless Wallet](#bitcoin-headless-wallet)
2224
- [Low-Level Components](#low-level-components)
2325
- [CIP-30 Compatibility](#cip-30-compatibility)
2426
- [CardanoHeadlessWallet vs MeshCardanoHeadlessWallet](#cardanoheadlesswallet-vs-meshcardanoheadlesswallet)
@@ -39,20 +41,20 @@ This package uses a two-tier class hierarchy for both headless and browser walle
3941

4042
## Exported Classes
4143

42-
| Class | Purpose | Use When |
43-
|-------|---------|----------|
44-
| `MeshCardanoHeadlessWallet` | Full-featured headless wallet with convenience methods | Server-side signing, backend transaction building, testing |
45-
| `CardanoHeadlessWallet` | CIP-30 strict headless wallet (raw hex/CBOR returns) | You need raw CIP-30 output without conversion |
46-
| `MeshCardanoBrowserWallet` | Full-featured browser wallet wrapper with convenience methods | dApp frontend integration with browser wallets (Eternl, Nami, etc.) |
47-
| `CardanoBrowserWallet` | CIP-30 strict browser wallet wrapper (raw hex/CBOR returns) | You need raw CIP-30 passthrough from browser wallets |
48-
| `CardanoInMemoryBip32` | BIP32 key derivation from mnemonic (keys stored in memory) | Deriving payment/stake/DRep keys from a mnemonic |
49-
| `BaseSigner` | Ed25519 signer from raw private keys | Signing with raw private keys (normal or extended) |
50-
| `CardanoAddress` | Cardano address construction and utilities | Building addresses from credentials |
51-
| `ICardanoWallet` | Interface definition for Cardano wallets | Type-checking and implementing custom wallets |
52-
| `createCip30Wallet` | Wraps an `ICardanoWallet` as a simulated CIP-30 initial API (`window.cardano.<name>`-shaped), with `enable()` extension negotiation | dApp integration tests / server-side agents that need a real `window.cardano`-shaped API, no browser |
53-
| `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 |
54-
| `ICip30Api` / `ICip30InitialApi` | Interface definitions for the CIP-30 API surface | Type-checking and implementing custom CIP-30 adapters |
55-
| `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` |
44+
| Class | Purpose | Use When |
45+
| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
46+
| `MeshCardanoHeadlessWallet` | Full-featured headless wallet with convenience methods | Server-side signing, backend transaction building, testing |
47+
| `CardanoHeadlessWallet` | CIP-30 strict headless wallet (raw hex/CBOR returns) | You need raw CIP-30 output without conversion |
48+
| `MeshCardanoBrowserWallet` | Full-featured browser wallet wrapper with convenience methods | dApp frontend integration with browser wallets (Eternl, Nami, etc.) |
49+
| `CardanoBrowserWallet` | CIP-30 strict browser wallet wrapper (raw hex/CBOR returns) | You need raw CIP-30 passthrough from browser wallets |
50+
| `CardanoInMemoryBip32` | BIP32 key derivation from mnemonic (keys stored in memory) | Deriving payment/stake/DRep keys from a mnemonic |
51+
| `BaseSigner` | Ed25519 signer from raw private keys | Signing with raw private keys (normal or extended) |
52+
| `CardanoAddress` | Cardano address construction and utilities | Building addresses from credentials |
53+
| `ICardanoWallet` | Interface definition for Cardano wallets | Type-checking and implementing custom wallets |
54+
| `createCip30Wallet` | Wraps an `ICardanoWallet` as a simulated CIP-30 initial API (`window.cardano.<name>`-shaped), with `enable()` extension negotiation | dApp integration tests / server-side agents that need a real `window.cardano`-shaped API, no browser |
55+
| `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 |
56+
| `ICip30Api` / `ICip30InitialApi` | Interface definitions for the CIP-30 API surface | Type-checking and implementing custom CIP-30 adapters |
57+
| `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` |
5658

5759
---
5860

@@ -61,7 +63,7 @@ This package uses a two-tier class hierarchy for both headless and browser walle
6163
### Create from Mnemonic
6264

6365
```typescript
64-
import { MeshCardanoHeadlessWallet, AddressType } from "@meshsdk/wallet";
66+
import { AddressType, MeshCardanoHeadlessWallet } from "@meshsdk/wallet";
6567

6668
const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({
6769
mnemonic: "globe cupboard camera ...".split(" "),
@@ -76,10 +78,14 @@ The `fetcher` is needed for signing transactions — the wallet uses it to look
7678
### Create from Raw Private Key
7779

7880
```typescript
79-
import { MeshCardanoHeadlessWallet, AddressType, BaseSigner } from "@meshsdk/wallet";
81+
import {
82+
AddressType,
83+
BaseSigner,
84+
MeshCardanoHeadlessWallet,
85+
} from "@meshsdk/wallet";
8086

8187
const paymentSigner = BaseSigner.fromNormalKeyHex(
82-
"d4ffb1e83d44b66849b4f16183cbf2ba1358c491cfeb39f0b66b5f811a88f182"
88+
"d4ffb1e83d44b66849b4f16183cbf2ba1358c491cfeb39f0b66b5f811a88f182",
8389
);
8490

8591
const wallet = await MeshCardanoHeadlessWallet.fromCredentialSources({
@@ -111,7 +117,7 @@ import { CardanoInMemoryBip32 } from "@meshsdk/wallet";
111117

112118
const HARDENED_OFFSET = 0x80000000;
113119
const bip32 = await CardanoInMemoryBip32.fromMnemonic(
114-
"globe cupboard camera ...".split(" ")
120+
"globe cupboard camera ...".split(" "),
115121
);
116122

117123
const paymentSigner = await bip32.getSigner([
@@ -219,11 +225,11 @@ const wallets = MeshCardanoBrowserWallet.getInstalledWallets();
219225
### Common Operations
220226

221227
```typescript
222-
const balance = await wallet.getBalanceMesh(); // Asset[]
223-
const address = await wallet.getChangeAddressBech32(); // bech32 string
224-
const utxos = await wallet.getUtxosMesh(); // UTxO[]
225-
const collateral = await wallet.getCollateralMesh(); // UTxO[]
226-
const networkId = await wallet.getNetworkId(); // number
228+
const balance = await wallet.getBalanceMesh(); // Asset[]
229+
const address = await wallet.getChangeAddressBech32(); // bech32 string
230+
const utxos = await wallet.getUtxosMesh(); // UTxO[]
231+
const collateral = await wallet.getCollateralMesh(); // UTxO[]
232+
const networkId = await wallet.getNetworkId(); // number
227233
const rewards = await wallet.getRewardAddressesBech32(); // string[]
228234

229235
// Sign and get the full transaction back (ready to submit)
@@ -235,6 +241,76 @@ const signature = await wallet.signData(addressBech32, hexPayload);
235241

236242
---
237243

244+
## Bitcoin Headless Wallet
245+
246+
`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.
247+
248+
### Create from Mnemonic with the Maestro provider
249+
250+
```typescript
251+
import { BitcoinHeadlessWallet, MaestroProvider } from "@meshsdk/wallet";
252+
253+
const provider = new MaestroProvider({
254+
network: "testnet", // or "mainnet"
255+
apiKey: "<your-maestro-api-key>",
256+
});
257+
258+
const wallet = await BitcoinHeadlessWallet.fromMnemonic({
259+
network: "Testnet4", // or "Mainnet"
260+
mnemonic: "muscle urban donkey ...".split(" "),
261+
provider,
262+
});
263+
```
264+
265+
`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.
266+
267+
### Query UTXOs
268+
269+
```typescript
270+
// All UTXOs across the payment (P2WPKH) and ordinals (P2TR) addresses,
271+
// each annotated with the address and purpose it belongs to.
272+
const utxos = await wallet.fetchUTXOs();
273+
274+
// Or query the provider directly for any address.
275+
const addressUtxos = await provider.fetchAddressUTxOs("tb1q...");
276+
```
277+
278+
### Coin selection
279+
280+
`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:
281+
282+
```typescript
283+
import { selectUtxosLargestFirst } from "@meshsdk/wallet";
284+
285+
const { selectedUtxos, change } = selectUtxosLargestFirst(
286+
utxos, // from fetchAddressUTxOs
287+
50_000, // target amount (sats)
288+
2, // fee rate (sats/vB)
289+
1, // number of recipients
290+
);
291+
```
292+
293+
### Send a transfer
294+
295+
```typescript
296+
// Queries UTXOs, selects coins, builds + signs a PSBT (RBF-enabled),
297+
// broadcasts through the provider, and returns the txid.
298+
const txid = await wallet.signTransfer([
299+
{ address: "tb1q...", amount: 10_000 },
300+
]);
301+
```
302+
303+
### Other operations
304+
305+
```typescript
306+
const balance = await wallet.getBalance(); // { confirmed, unconfirmed, total } in sats
307+
const history = await wallet.getTransactionHistory(); // newest first
308+
const signature = await wallet.signMessage("tb1q...", "hello"); // BIP-137 ECDSA
309+
const psbtBase64 = await wallet.signPsbt({ psbt, signInputs }); // P2WPKH + P2TR inputs
310+
```
311+
312+
---
313+
238314
## Low-Level Components
239315

240316
### CardanoInMemoryBip32
@@ -300,12 +376,12 @@ Endpoints implemented by the `createCip30Wallet()` / `createCip30Api()` adapter
300376

301377
`MeshCardanoHeadlessWallet` extends it with convenience methods:
302378

303-
| Need | Base method (hex/CBOR) | Mesh method (parsed) |
304-
|------|----------------------|---------------------|
305-
| Balance | `getBalance()` → CBOR hex | `getBalanceMesh()` → `Asset[]` |
306-
| Address | `getChangeAddress()` → hex | `getChangeAddressBech32()` → bech32 |
307-
| UTxOs | `getUtxos()` → CBOR hex[] | `getUtxosMesh()` → `UTxO[]` |
308-
| Sign tx | `signTx()` → witness set | `signTxReturnFullTx()` → full signed tx |
379+
| Need | Base method (hex/CBOR) | Mesh method (parsed) |
380+
| ------- | -------------------------- | --------------------------------------- |
381+
| Balance | `getBalance()` → CBOR hex | `getBalanceMesh()` → `Asset[]` |
382+
| Address | `getChangeAddress()` → hex | `getChangeAddressBech32()` → bech32 |
383+
| UTxOs | `getUtxos()` → CBOR hex[] | `getUtxosMesh()` → `UTxO[]` |
384+
| Sign tx | `signTx()` → witness set | `signTxReturnFullTx()` → full signed tx |
309385

310386
The same pattern applies to `CardanoBrowserWallet` vs `MeshCardanoBrowserWallet`.
311387

@@ -317,9 +393,9 @@ This package (`@meshsdk/wallet` v2) has breaking changes from the previous `Mesh
317393

318394
**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.
319395

320-
| Migrating from | Migrating to | Guide |
321-
|----------------|-------------|-------|
322-
| `MeshWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoHeadlessWallet` | [`mesh-wallet-migration.md`](./mesh-wallet-migration.md) |
323-
| `BrowserWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoBrowserWallet` | [`browser-wallet-migration.md`](./browser-wallet-migration.md) |
396+
| Migrating from | Migrating to | Guide |
397+
| ----------------------------------------------------------- | --------------------------- | -------------------------------------------------------------- |
398+
| `MeshWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoHeadlessWallet` | [`mesh-wallet-migration.md`](./mesh-wallet-migration.md) |
399+
| `BrowserWallet` (from `@meshsdk/wallet` or `@meshsdk/core`) | `MeshCardanoBrowserWallet` | [`browser-wallet-migration.md`](./browser-wallet-migration.md) |
324400

325401
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.

‎bitcoin-milestone.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Milestone Evidence — Standard Headless Wallet on Bitcoin
2+
3+
This document maps the milestone acceptance criteria to the code in this repository.
4+
5+
**Milestone output A:** Develop and deploy a standard headless wallet on Bitcoin.
6+
7+
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.
8+
9+
## A1 — UTXO querying based on Maestro as the data provider
10+
11+
| What | Where |
12+
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
13+
| Maestro provider (implements `IBitcoinProvider`) | [`src/bitcoin/providers/maestro.ts`](./src/bitcoin/providers/maestro.ts) |
14+
| Shared fetch/error plumbing | [`src/bitcoin/providers/common.ts`](./src/bitcoin/providers/common.ts) |
15+
| Wallet-level UTXO querying (`fetchUTXOs`) | [`src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts`](./src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts) |
16+
| Tests (mocked HTTP, 20 cases) | [`test/bitcoin/providers/maestro.test.ts`](./test/bitcoin/providers/maestro.test.ts) |
17+
18+
`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).
19+
20+
## A2 — Coin selection algorithm on queried Bitcoin UTXOs
21+
22+
| What | Where |
23+
| ------------------------------------------------- | ------------------------------------------------------------------------------ |
24+
| Coin selection module (`selectUtxosLargestFirst`) | [`src/bitcoin/utils/coin-selection.ts`](./src/bitcoin/utils/coin-selection.ts) |
25+
| Tests (fee model, dust handling, 12 cases) | [`test/bitcoin/coin-selection.test.ts`](./test/bitcoin/coin-selection.test.ts) |
26+
27+
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.
28+
29+
## A3 — Send transfer transaction
30+
31+
| What | Where |
32+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
33+
| `signTransfer` (query → select → build → sign → broadcast) | [`src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts`](./src/bitcoin/wallet/mesh/bitcoin-headless-wallet.ts) |
34+
| PSBT signing (P2WPKH ECDSA + P2TR Schnorr) | same file — `signPsbt` |
35+
| Tests (broadcast, fees, dust, RBF) | [`test/bitcoin/bitcoin-headless-wallet.test.ts`](./test/bitcoin/bitcoin-headless-wallet.test.ts) |
36+
37+
`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.
38+
39+
## Usage
40+
41+
See the [Bitcoin Headless Wallet](./README.md#bitcoin-headless-wallet) section of the README for end-to-end examples.

‎jest.config.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ const jestConfig: Config = {
77
testMatch: ["**/test/**/*.test.ts"],
88
testPathIgnorePatterns: ["<rootDir>/_backup/"],
99
setupFiles: ["dotenv/config"],
10+
setupFilesAfterEnv: ["<rootDir>/test/setup-sodium.ts"],
1011
preset: "ts-jest",
1112
moduleNameMapper: {
1213
"^(\\.{1,2}/.*)\\.js$": "$1",
14+
"^libsodium-wrappers-sumo$": "<rootDir>/node_modules/libsodium-wrappers-sumo",
15+
"^libsodium-sumo$": "<rootDir>/node_modules/libsodium-sumo",
1316
},
1417
transform: {
1518
"^.+\\.[jt]s?$": "ts-jest",

0 commit comments

Comments
 (0)