Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 105 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -18,6 +19,7 @@ npm install @meshsdk/wallet
- [Exported Classes](#exported-classes)
- [Headless Wallet (Server-Side)](#headless-wallet-server-side)
- [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)
Expand All @@ -38,16 +40,16 @@ 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 |
| `InMemoryBip32` | 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 |
| 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 |
| `InMemoryBip32` | 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 |

---

Expand All @@ -56,7 +58,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(" "),
Expand All @@ -71,10 +73,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({
Expand Down Expand Up @@ -106,7 +112,7 @@ import { InMemoryBip32 } from "@meshsdk/wallet";

const HARDENED_OFFSET = 0x80000000;
const bip32 = await InMemoryBip32.fromMnemonic(
"globe cupboard camera ...".split(" ")
"globe cupboard camera ...".split(" "),
);

const paymentSigner = await bip32.getSigner([
Expand Down Expand Up @@ -154,11 +160,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)
Expand All @@ -170,6 +176,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: "<your-maestro-api-key>",
});

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

### InMemoryBip32
Expand Down Expand Up @@ -203,12 +279,12 @@ Both `MeshCardanoHeadlessWallet` and `MeshCardanoBrowserWallet` provide CIP-30 c

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

Expand All @@ -220,9 +296,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.
41 changes: 41 additions & 0 deletions bitcoin-milestone.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions src/bitcoin/providers/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Shared HTTP plumbing for Bitcoin data providers.
*
* Providers use the platform `fetch` (Node 18+ / browsers) so the package does
* not carry an HTTP client dependency. All non-2xx responses are normalized
* into a `ProviderHttpError` carrying the response details.
*/

export type HttpErrorDetails = {
status?: number;
statusText?: string;
body?: unknown;
url?: string;
};

/**
* Error thrown by provider HTTP helpers. Carries the response details so
* callers can branch on `status` when needed.
*/
export class ProviderHttpError extends Error {
readonly details: HttpErrorDetails;

constructor(details: HttpErrorDetails) {
super(JSON.stringify(details));
this.name = "ProviderHttpError";
this.details = details;
}
}

/**
* Perform a request against `baseUrl + path` and parse the response.
* Returns parsed JSON when the response body is JSON, otherwise the raw text
* (e.g. the plain-text txid returned by Esplora's POST /tx).
* Throws `ProviderHttpError` on any non-2xx status.
*/
export async function requestJson<T>(
baseUrl: string,
path: string,
init: RequestInit & { headers: Record<string, string> },
): Promise<T> {
const url = `${baseUrl}${path}`;
const response = await fetch(url, init);

const text = await response.text();
let body: unknown = text;
try {
body = text.length ? JSON.parse(text) : undefined;
} catch {
// Non-JSON body — keep the raw text.
}

if (!response.ok) {
throw new ProviderHttpError({
status: response.status,
statusText: response.statusText,
body,
url,
});
}

return body as T;
}
Loading