@imikailoby/sats
Version:
Tiny non-custodial Bitcoin SDK (TS) โ keys, addresses, PSBT, provider chain
167 lines (120 loc) โข 5.56 kB
Markdown
# @imikailoby/sats
**Minimalistic, non-custodial Bitcoin SDK in TypeScript.**
Keys and signatures stay local. On-chain address derivation (BIP84/P2WPKH), **PSBT** build/sign/finalize, **Esplora** providers (Blockstream/Mempool), and provider chain with timeouts & backoff.
> **Runtime:** Node 20+ โ global `fetch` and ESM.
> **ESM:** This package is ESM-only. For CommonJS, use dynamic `import()` or bundle accordingly.
## Installation
```bash
npm i @imikailoby/sats
# or
pnpm add @imikailoby/sats
```
## Features (v1.0.0)
- ๐ **Non-custodial**: BIP39 โ BIP32 (BIP84), P2WPKH addresses, keys never leave your process.
- ๐งฉ **PSBT (BIP-174)**: Build, sign, and finalize ready-to-broadcast transactions.
- ๐ **Esplora providers**: Blockstream / Mempool + any custom Esplora-compatible endpoint.
- โ๏ธ **Provider chain**: Sequential retries with timeout and simple backoff.
- ๐งช **Tested**: Jest + ts-jest (ESM).
## Quick Start
### Keys & addresses (testnet)
```ts
import { generateMnemonic, fromMnemonic } from "@imikailoby/sats";
const mnemonic = generateMnemonic(); // BIP39
const wallet = fromMnemonic(mnemonic, { testnet: true }); // BIP84 m/84'/1'/0'
const recv1 = wallet.nextReceive(); // { address, path }
const recv2 = wallet.nextReceive();
console.log(recv1.address, recv2.address); // tb1...
```
### Providers: chain with backoff
```ts
import { providers, createProviderChain } from "@imikailoby/sats";
const p = createProviderChain(
providers.mempool(true, { timeoutMs: 2000 }), // testnet
providers.blockstream(true, { timeoutMs: 2000 }), // fallback
{ timeoutMs: 2000, backoffMs: [0, 50, 100] } // chain options (optional)
);
const utxos = await p.getAddressUtxos("<tb1...>");
const bal = await p.getAddressBalance("<tb1...>");
console.log(utxos.length, bal);
```
### PSBT: build โ sign โ finalize โ (optional) broadcast
```ts
import { fromMnemonic, buildPsbt, signPsbt, finalizePsbt, providers, createProviderChain } from "@imikailoby/sats";
const w = fromMnemonic("<mnemonic>", { testnet: true });
const change = w.nextReceive().address;
// (usually fetched from provider)
const utxos = [{ txid: "<txid>", vout: 0, value: 25_000, scriptPubKey: "0014..." }];
const psbt = buildPsbt({
utxos,
outputs: [{ address: "<recipient tb1...>", value: 20_000 }],
changeAddress: change,
fee: 500,
testnet: true
});
const wif = w.toWIF(0); // WIF for key index 0
signPsbt(psbt, wif, true); // sign
const { hex, txid } = finalizePsbt(psbt); // raw tx + txid
// broadcast (if provider supports /tx):
const chain = createProviderChain(
providers.mempool(true, { timeoutMs: 2000 }),
providers.blockstream(true, { timeoutMs: 2000 }),
{ timeoutMs: 2000, backoffMs: [0, 50, 100] }
);
await chain.broadcast?.(hex);
```
## API Overview
### Network
- `network(testnet?: boolean): Network` โ `true` โ testnet, otherwise mainnet.
### Keys / Wallet
- `generateMnemonic(strength?: 128|160|192|224|256): string`
Generate BIP39 mnemonic.
- `fromMnemonic(mnemonic: string, opts?: { testnet?: boolean; account?: number }): Wallet`
BIP84 wallet (`m/84'/{coin}'/{account}'/change/index`).
**Wallet**:
- `testnet: boolean`
- `nextReceive(): { address: string; path: string }` โ next receive address
- `getKeyAt(index: number, change?: 0|1)` โ derive key at path
- `toWIF(index: number, change?: 0|1): string` โ export private key as WIF
- `deriveKeypair(mnemonic, { account, change, index }, testnet?): { priv: Buffer; pub: Buffer; path: string }`
Direct derivation if needed.
- `p2wpkhAddress(pubkey: Buffer, testnet?): { address: string; scriptPubKey: string }`
### Providers (Esplora)
- `providers.blockstream(testnet?: boolean, opts?: { timeoutMs?: number }): Provider`
- `providers.mempool(testnet?: boolean, opts?: { timeoutMs?: number }): Provider`
- `providers.esplora(baseUrl: string, opts?: { timeoutMs?: number }): Provider`
**Provider**:
- `getAddressUtxos(address): Promise<Utxo[]>`
- `getAddressBalance(address): Promise<{ funded: number; spent: number }>`
- `broadcast?(rawHex): Promise<{ txid: string }>` โ if endpoint available (Mempool/Blockstream have it)
- `createProviderChain(...providers: Provider[], options?: { timeoutMs?: number; backoffMs?: number[] }): Provider`
Sequential retries with timeout and simple backoff. Options override defaults for the chain.
### PSBT
- `buildPsbt({ utxos, outputs, changeAddress, fee, testnet? }): Psbt`
Validates sums, adds inputs/outputs/change.
- `signPsbt(psbt, priv: Buffer|string, testnet?): Psbt`
Sign all inputs with provided key (Buffer or WIF).
- `finalizePsbt(psbt): { hex: string; txid: string }`
Finalize and extract raw transaction.
## Errors
The SDK uses domain-specific errors (names may differ slightly in implementation):
- `SatsError` โ base class.
- `DerivationError`, `AddressError` โ key/address issues.
- `PsbtBuildError`, `InsufficientFundsError` โ transaction build issues.
- `ProviderError`, `TimeoutError`, `BroadcastError` โ network/provider issues.
> These are thrown from exported functions; catch them to retry/change provider/adjust fee.
## Patterns & Safety
- **Non-custodial**: seed/keys remain local.
- **SRP/DRY/SOLID**: layers `core/`, `crypto/`, `psbt/`, `net/`, `providers/`, `utils/`.
- **ESM, tree-shake friendly**: no side effects in root modules.
- **Fetch abstraction**: providers use shared HTTP helper with timeout.
## Tests
```bash
npm test
```