beignet 0.7.5 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +427 -586
- package/dist/cli/beignet-node.js +3 -1
- package/dist/cli/beignet-node.js.map +1 -1
- package/dist/lightning/offer/offer-manager.js +6 -0
- package/dist/lightning/offer/offer-manager.js.map +1 -1
- package/dist/types/lightning/offer/offer-manager.d.ts +4 -0
- package/docs/RECOVERY-PROTOCOL.md +131 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,205 +1,208 @@
|
|
|
1
1
|
# Beignet
|
|
2
2
|
|
|
3
|
-
A self-custodial Bitcoin wallet library for JavaScript/TypeScript
|
|
3
|
+
A self-custodial Bitcoin wallet library for JavaScript/TypeScript with a **full Lightning Network implementation**. Beignet implements the Lightning protocol and channel state machine in TypeScript rather than wrapping LND, CLN or LDK: it speaks BOLT 8 over a real TCP socket and runs its own BOLT 2 state machine.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Two layers, one mnemonic:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **On-chain wallet:** HD keys, address generation, UTXO tracking, transaction building, PSBT/hardware signing, multisig, watch-only, Electrum connectivity.
|
|
8
|
+
- **Lightning:** channel lifecycle, onion-routed payments, BOLT 11 invoices, BOLT 12 offers, gossip and pathfinding, anchors, splicing, taproot channels, watchtower client. Interop-tested against LND, Core Lightning and Eclair on regtest.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
- **Lightning Network** — A complete BOLT-compliant Lightning implementation in TypeScript, supporting channel management, onion-routed payments, BOLT 11 invoices, gossip-based routing, and real TCP transport. Tested against LND, CLN, and Eclair on regtest.
|
|
10
|
+
Requires **Node.js 18+**. MIT licensed.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**Jump to:** [Install](#install) · [Examples](#try-the-examples) · [On-chain wallet](#on-chain-wallet) · [Lightning](#lightning) · [Daemon & CLI](#http-daemon--cli) · [Protocol layer](#protocol-layer-advanced) · [Tests](#tests) · [Status & limitations](#status--limitations)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
2. [On-Chain Wallet](#on-chain-wallet)
|
|
16
|
-
- [Leveled Logging](#leveled-logging)
|
|
17
|
-
3. [Lightning Network](#lightning-network)
|
|
18
|
-
- [Lightning Quick Start (BeignetNode)](#lightning-quick-start-beignetnode)
|
|
19
|
-
- [Decision-Support APIs](#decision-support-apis)
|
|
20
|
-
- [HTTP Daemon](#http-daemon)
|
|
21
|
-
- [Advanced API (LightningNode)](#advanced-api-lightningnode)
|
|
22
|
-
- [Architecture](#architecture)
|
|
23
|
-
- [BOLT Coverage](#bolt-coverage)
|
|
24
|
-
- [Module Reference](#module-reference)
|
|
25
|
-
4. [Running Tests](#running-tests)
|
|
26
|
-
5. [Interop Testing](#interop-testing)
|
|
27
|
-
6. [React Native](#react-native)
|
|
28
|
-
7. [Documentation](#documentation)
|
|
29
|
-
8. [Support](#support)
|
|
30
|
-
|
|
31
|
-
## Getting Started
|
|
14
|
+
## Install
|
|
32
15
|
|
|
33
16
|
```bash
|
|
34
|
-
#
|
|
35
|
-
|
|
17
|
+
npm install beignet # or: yarn add beignet
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Smallest thing that works. `net` and `tls` are injected so the same code runs on Node and React Native:
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
```typescript
|
|
23
|
+
import net from 'net';
|
|
24
|
+
import tls from 'tls';
|
|
25
|
+
import { Wallet, generateMnemonic } from 'beignet';
|
|
26
|
+
|
|
27
|
+
const result = await Wallet.create({
|
|
28
|
+
mnemonic: generateMnemonic(),
|
|
29
|
+
electrumOptions: { net, tls }
|
|
30
|
+
});
|
|
31
|
+
if (result.isErr()) throw result.error;
|
|
32
|
+
const wallet = result.value;
|
|
33
|
+
|
|
34
|
+
console.log(await wallet.getAddress());
|
|
35
|
+
console.log(wallet.getBalance());
|
|
39
36
|
```
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
From here: [the on-chain wallet](#on-chain-wallet) for sending, PSBTs, multisig and watch-only, or [Lightning](#lightning) for channels and payments.
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
## Try the examples
|
|
41
|
+
|
|
42
|
+
The fastest way to understand the whole system is to run the two REPL examples against a live wallet and a live node. Both are checked-in TypeScript you can read and edit.
|
|
44
43
|
|
|
45
44
|
```bash
|
|
46
45
|
git clone git@github.com:coreyphillips/beignet.git && cd beignet
|
|
47
|
-
npm install
|
|
46
|
+
npm install
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
###
|
|
51
|
-
|
|
52
|
-
Both examples launch an interactive REPL with a live wallet/node instance:
|
|
49
|
+
### 1. On-chain wallet REPL
|
|
53
50
|
|
|
54
51
|
```bash
|
|
55
|
-
# On-chain wallet REPL
|
|
56
52
|
npm run example
|
|
53
|
+
```
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
Creates a mainnet wallet (a fresh mnemonic unless you pass one), syncs it against a public Electrum server, prints the balance and a receive address, then drops you at a `>` prompt with the wallet bound to `wallet`. Type `help()` for the command list.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
> wallet.getBalance()
|
|
59
|
+
> await wallet.getAddress()
|
|
60
|
+
> await wallet.refreshWallet()
|
|
61
|
+
> await wallet.send({ address: 'bc1q...', amount: 10000, satsPerByte: 2 })
|
|
62
|
+
```
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
npm run example:lightning -- --low-level
|
|
64
|
+
State persists as JSON under `example/walletData/`. Pass a mnemonic as the first argument to reuse a wallet: `npm run example -- "abandon abandon ... about"`.
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
npm run example:lightning -- abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about --alias mynode
|
|
66
|
+
### 2. Lightning node REPL
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
npm run example:lightning
|
|
68
|
+
```bash
|
|
69
|
+
npm run example:lightning
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
Boots a real Lightning node (`BeignetNode`) with an auto-created wallet, storage and funding provider, waits for it to become operational, prints info/balance/health, and drops you at a `beignet>` prompt with the node bound to `node`. Type `help()` for the command list. Top-level `await` works.
|
|
72
73
|
|
|
73
|
-
```
|
|
74
|
-
|
|
74
|
+
```js
|
|
75
|
+
beignet> await node.getNewAddress() // fund this on-chain, then:
|
|
76
|
+
beignet> await node.connectAndOpenChannel(pubkey, host, port, 200000)
|
|
77
|
+
beignet> node.createInvoice(1000, 'coffee').bolt11
|
|
78
|
+
beignet> await node.payInvoice('lnbc...')
|
|
79
|
+
beignet> node.getLiquiditySnapshot()
|
|
80
|
+
```
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
The flags you will actually reach for (everything after `--`):
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
| Flag | Effect |
|
|
85
|
+
|------|--------|
|
|
86
|
+
| `mainnet` \| `testnet` \| `regtest` | Network, as a bare positional arg (default `mainnet`) |
|
|
87
|
+
| `<12 or 24 words>` | Reuse a mnemonic, as bare positional args (default generates one) |
|
|
88
|
+
| `--electrum-host <h>` `--electrum-port <p>` | Point at your own Electrum server |
|
|
89
|
+
| `--alias <name>` | Node alias in `node_announcement` |
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
```bash
|
|
92
|
+
# named regtest node against a local Electrum server
|
|
93
|
+
npm run example:lightning -- regtest --electrum-host 127.0.0.1 --electrum-port 60001 --alias mynode
|
|
94
|
+
```
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
const balance = wallet.getBalance();
|
|
96
|
+
Tor, full-graph gossip, the low-level `LightningNode` variant and the non-interactive payment-API walkthrough have flags too: see [the flag reference](example/REPL_TESTING.md#repl-flags).
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
const sendRes = await wallet.send({
|
|
90
|
-
address: 'bc1q...',
|
|
91
|
-
amount: 50000,
|
|
92
|
-
satPerByte: 2,
|
|
93
|
-
});
|
|
98
|
+
Node state lives in a SQLite DB under `~/.beignet/data/<hash-of-mnemonic>/` (the `--low-level` example uses `example/lightningData/node.db`).
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
Both examples run straight off the TypeScript sources through `ts-node`, so no build step is needed. Use `npm run build` when you want the compiled `dist/`.
|
|
101
|
+
|
|
102
|
+
**→ [example/REPL_TESTING.md](example/REPL_TESTING.md) is a copy-pasteable walkthrough** of the whole lifecycle in the REPL: funding, peers, channels, invoices, payments, keysend, offers, splicing, closing, backup.
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
## Which entry point?
|
|
105
|
+
|
|
106
|
+
| Import | Contains | Use when |
|
|
107
|
+
|--------|----------|----------|
|
|
108
|
+
| `beignet` | `Wallet`, `generateMnemonic`, types | You want the on-chain wallet |
|
|
109
|
+
| `beignet/cli` | `BeignetNode`, `startDaemon`, error helpers | **You want Lightning.** Sats-denominated, string IDs, structured errors |
|
|
110
|
+
| `beignet/lightning` | Namespaced protocol modules (`node`, `channel`, `onion`, ...) | You need the raw BOLT layer: bigint msat, Buffer IDs, wire messages |
|
|
111
|
+
|
|
112
|
+
## On-chain wallet
|
|
100
113
|
|
|
101
114
|
```typescript
|
|
102
|
-
import { Wallet, generateMnemonic } from 'beignet';
|
|
103
115
|
import net from 'net';
|
|
104
116
|
import tls from 'tls';
|
|
117
|
+
import { Wallet, generateMnemonic } from 'beignet';
|
|
105
118
|
|
|
106
|
-
const
|
|
119
|
+
const res = await Wallet.create({
|
|
107
120
|
mnemonic: generateMnemonic(),
|
|
108
|
-
|
|
109
|
-
electrumOptions: {
|
|
110
|
-
servers: { host: '127.0.0.1', ssl: 50002, tcp: 50001, protocol: 'ssl' },
|
|
111
|
-
net,
|
|
112
|
-
tls,
|
|
113
|
-
},
|
|
114
|
-
network: 'mainnet',
|
|
115
|
-
addressType: 'p2wpkh',
|
|
116
|
-
coinSelectPreference: 'consolidate',
|
|
121
|
+
electrumOptions: { net, tls } // required: inject the socket implementations
|
|
117
122
|
});
|
|
123
|
+
if (res.isErr()) throw res.error;
|
|
124
|
+
const wallet = res.value;
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
txs: [
|
|
122
|
-
{ address: 'addr1', amount: 1000 },
|
|
123
|
-
{ address: 'addr2', amount: 2000 },
|
|
124
|
-
],
|
|
125
|
-
});
|
|
126
|
+
const address = await wallet.getAddress();
|
|
127
|
+
const balance = wallet.getBalance();
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
await wallet.
|
|
129
|
-
|
|
130
|
-
toAddress: 'bc1q...',
|
|
131
|
-
satsPerByte: 5,
|
|
132
|
-
});
|
|
129
|
+
await wallet.send({ address: 'bc1q...', amount: 50_000, satsPerByte: 2 });
|
|
130
|
+
await wallet.sendMany({ txs: [{ address: 'bc1q...', amount: 1000 }] });
|
|
131
|
+
await wallet.refreshWallet();
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
const
|
|
133
|
+
const utxos = wallet.listUtxos();
|
|
134
|
+
const history = await wallet.getAddressHistory('bc1q...');
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Every fallible call returns a `Result<T>`: check `isErr()` before reading `.value`. Amounts are always satoshis.
|
|
138
|
+
|
|
139
|
+
Options worth knowing on `Wallet.create`: `network` (`EAvailableNetworks.mainnet` | `testnet` | `regtest` | `signet`), `addressType` (`p2wpkh` default, `p2sh-p2wpkh`, `p2pkh`, `p2tr`), `passphrase`, `account`, `storage`, `logger`, `coinSelectPreference`, `feeEstimationSource`, `gapLimitOptions`.
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary><b>Custom Electrum servers, failover, fee sources, BIP21</b></summary>
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
```typescript
|
|
145
|
+
import { EAvailableNetworks, EProtocol, Wallet } from 'beignet';
|
|
146
|
+
|
|
147
|
+
const res = await Wallet.create({
|
|
148
|
+
mnemonic,
|
|
149
|
+
network: EAvailableNetworks.mainnet,
|
|
150
|
+
feeEstimationSource: 'electrum', // 'electrum' | 'http' | 'auto' (default)
|
|
151
|
+
electrumOptions: {
|
|
152
|
+
net,
|
|
153
|
+
tls,
|
|
154
|
+
servers: [
|
|
155
|
+
{ host: 'bitcoin.lu.ke', ssl: 50002, tcp: 50001, protocol: EProtocol.ssl },
|
|
156
|
+
{ host: 'mempool.space', ssl: 60602, tcp: 60601, protocol: EProtocol.ssl }
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
});
|
|
139
160
|
```
|
|
140
161
|
|
|
141
|
-
|
|
162
|
+
- **Failover:** with multiple servers the wallet rotates through them in order on connect/reconnect failure, then through hardcoded fallback peers for the network, with a per-server cooldown so dead servers are not hammered. Inspect `wallet.electrum.currentServer` and `wallet.electrum.rotationCount`.
|
|
163
|
+
- **Fee source:** `'electrum'` queries only the connected server via `blockchain.estimatefee`, so fee lookups never leak to mempool.space/blocktank over clearnet. `'auto'` prefers Electrum and falls back to HTTP. All remote rates are clamped to 5000 sat/vB.
|
|
164
|
+
- **Networks:** mainnet, testnet, regtest and signet work end to end (wallet, Electrum, CLI/daemon `--network signet`, Lightning chain hash and `tbs` invoice prefix). Signet shares testnet address formats and coin type 1.
|
|
165
|
+
- **BIP21:** `encodeBip21({ address, amountSats?, label?, message? })` builds a `bitcoin:` URI.
|
|
142
166
|
|
|
143
|
-
|
|
144
|
-
(xpub/ypub/zpub for mainnet, tpub/upub/vpub for testnet/regtest) instead of a
|
|
145
|
-
mnemonic. The key is assumed to sit at the account level
|
|
146
|
-
(m/purpose'/coin'/account', e.g. m/84'/0'/0' for p2wpkh), so receive and
|
|
147
|
-
change addresses derive publicly as xpub/0/i and xpub/1/i. SLIP-132 version
|
|
148
|
-
bytes are normalized automatically: a zpub/vpub implies p2wpkh and a
|
|
149
|
-
ypub/upub implies p2sh-p2wpkh; a plain xpub/tpub uses the `addressType`
|
|
150
|
-
option (default p2wpkh). Because one account xpub yields exactly one address
|
|
151
|
-
type, a watch-only wallet monitors only that type.
|
|
167
|
+
</details>
|
|
152
168
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
subscriptions. Anything that requires private keys
|
|
156
|
-
(send/sendMax/sendMany/sweepPrivateKey/getPrivateKey and the internal signing
|
|
157
|
-
paths) fails with the typed `WatchOnlySigningError`
|
|
158
|
-
(`code: 'WATCH_ONLY_CANNOT_SIGN'`, message `watch-only wallet cannot sign`).
|
|
169
|
+
<details>
|
|
170
|
+
<summary><b>Watch-only wallets (account xpub)</b></summary>
|
|
159
171
|
|
|
160
|
-
|
|
161
|
-
with a mnemonic.
|
|
172
|
+
Built from an account-level extended public key instead of a mnemonic. The key is assumed to sit at `m/purpose'/coin'/account'` (e.g. `m/84'/0'/0'`), so addresses derive as `xpub/0/i` and `xpub/1/i`. SLIP-132 version bytes are normalized: `zpub`/`vpub` implies p2wpkh, `ypub`/`upub` implies p2sh-p2wpkh, a plain `xpub`/`tpub` uses `addressType` (default p2wpkh). One account xpub yields exactly one address type, so a watch-only wallet monitors only that type.
|
|
162
173
|
|
|
163
174
|
```typescript
|
|
164
|
-
import { Wallet } from 'beignet';
|
|
165
|
-
|
|
166
175
|
const res = await Wallet.createWatchOnly({
|
|
167
176
|
xpub: 'zpub6r...',
|
|
168
|
-
network:
|
|
169
|
-
electrumOptions: { net, tls }
|
|
177
|
+
network: EAvailableNetworks.mainnet,
|
|
178
|
+
electrumOptions: { net, tls }
|
|
170
179
|
});
|
|
171
180
|
if (res.isErr()) return;
|
|
172
181
|
const watchOnly = res.value;
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
await watchOnly.getAddress(); // works
|
|
184
|
+
watchOnly.getBalance(); // works
|
|
185
|
+
|
|
186
|
+
const send = await watchOnly.send({ address: 'bc1q...', amount: 1000 });
|
|
187
|
+
// send.isErr() === true, message: 'watch-only wallet cannot sign'
|
|
178
188
|
```
|
|
179
189
|
|
|
180
|
-
|
|
190
|
+
The full read-only surface works: address generation, gap-limit scanning, Electrum refresh, balances, history, UTXOs, fee estimates, address subscriptions. Anything needing private keys (`send`/`sendMax`/`sendMany`/`sweepPrivateKey`/`getPrivateKey`) fails with the typed `WatchOnlySigningError` (`code: 'WATCH_ONLY_CANNOT_SIGN'`). Library-only for now: the HTTP daemon always runs with a mnemonic.
|
|
191
|
+
|
|
192
|
+
</details>
|
|
193
|
+
|
|
194
|
+
<details>
|
|
195
|
+
<summary><b>Hardware wallets and external signers (PSBT)</b></summary>
|
|
181
196
|
|
|
182
|
-
`buildPsbt` runs the normal
|
|
183
|
-
but stops before signing and returns a base64 PSBT populated with everything
|
|
184
|
-
a hardware signer needs: `witnessUtxo` (or `nonWitnessUtxo` for legacy
|
|
185
|
-
p2pkh), `redeemScript` for p2sh-p2wpkh, `tapInternalKey` plus
|
|
186
|
-
`tapBip32Derivation` for p2tr, and `bip32Derivation` (fingerprint + path +
|
|
187
|
-
pubkey) on every wallet input. It works on both full and watch-only wallets.
|
|
188
|
-
Note for watch-only wallets: the true master fingerprint is unknowable from
|
|
189
|
-
an account xpub, so the xpub's parent fingerprint is used; signers should
|
|
190
|
-
locate keys by derivation path.
|
|
197
|
+
`buildPsbt` runs the normal setup (coin selection, change, fee) but stops before signing, returning a base64 PSBT populated with what a hardware signer needs: `witnessUtxo` (or `nonWitnessUtxo` for legacy p2pkh), `redeemScript` for p2sh-p2wpkh, `tapInternalKey` plus `tapBip32Derivation` for p2tr, and `bip32Derivation` on every wallet input. Works on full and watch-only wallets.
|
|
191
198
|
|
|
192
199
|
```typescript
|
|
193
|
-
// 1. Build (
|
|
194
|
-
const build = await wallet.buildPsbt({
|
|
195
|
-
address: 'bc1q...',
|
|
196
|
-
amount: 50000,
|
|
197
|
-
satsPerByte: 4,
|
|
198
|
-
});
|
|
200
|
+
// 1. Build (never touches private keys)
|
|
201
|
+
const build = await wallet.buildPsbt({ address: 'bc1q...', amount: 50_000, satsPerByte: 4 });
|
|
199
202
|
if (build.isErr()) return;
|
|
200
203
|
const { psbtBase64, fee, vsizeEstimate } = build.value;
|
|
201
204
|
|
|
202
|
-
// 2. Sign externally (hardware wallet
|
|
205
|
+
// 2. Sign externally (hardware wallet, HWI, another machine)
|
|
203
206
|
const signedBase64 = await myHardwareWallet.signPsbt(psbtBase64);
|
|
204
207
|
|
|
205
208
|
// 3. Import: validates a signature on EVERY input, finalizes, does NOT broadcast
|
|
@@ -210,211 +213,157 @@ const { txHex, txid } = imported.value;
|
|
|
210
213
|
// 4. Broadcast when ready
|
|
211
214
|
await wallet.broadcastTransaction(txHex);
|
|
212
215
|
|
|
213
|
-
// Multi-party
|
|
216
|
+
// Multi-party: merge partially signed copies of the same PSBT
|
|
214
217
|
const combined = wallet.combinePsbts([copyA, copyB]);
|
|
215
218
|
```
|
|
216
219
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
the
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
script type 2 (`m/48'/coin'/account'/2'`, receive `/0/*`, change `/1/*`) and
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Cosigners are supplied as account-level extended public keys (`xpub`/`tpub`,
|
|
232
|
-
or the SLIP-132 multisig encodings `Zpub`/`Vpub`, normalized automatically).
|
|
233
|
-
When a mnemonic is provided, this wallet IS one of the cosigners: its BIP 48
|
|
234
|
-
account xpub is derived and included automatically (pass `ourXpub` to assert
|
|
235
|
-
it explicitly; a mismatch is rejected). Omit the mnemonic for a watch-only
|
|
236
|
-
multisig coordinator: the full read-only surface (scanning, balances,
|
|
237
|
-
history, subscriptions) works, signing does not.
|
|
238
|
-
|
|
239
|
-
Spending is PSBT-only. Direct spends (`send`/`sendMany`/`sendMax`) fail with
|
|
240
|
-
the typed `MultisigSpendError` (`code: 'MULTISIG_REQUIRES_PSBT'`). `buildPsbt`
|
|
241
|
-
attaches the `witnessScript` and one `bip32Derivation` entry per cosigner to
|
|
242
|
-
every input; `signPsbtWithOurKey` adds this cosigner's partial signature
|
|
243
|
-
without finalizing; `importSignedPsbt` counts the VALID partial signatures on
|
|
244
|
-
each input against the witnessScript threshold and refuses to finalize below
|
|
245
|
-
it (the error names how many signatures it has and needs).
|
|
246
|
-
`exportDescriptors()` emits the checksummed `wsh(sortedmulti(...))` receive
|
|
247
|
-
and change descriptors for import into Bitcoin Core/Sparrow/Specter; our key
|
|
248
|
-
carries its full key origin, cosigners known only as xpubs carry a
|
|
249
|
-
fingerprint-only origin. Multisig is a library-only feature for now: the
|
|
250
|
-
HTTP daemon wallet stays single-sig.
|
|
251
|
-
|
|
252
|
-
Full 2-of-3 walkthrough:
|
|
220
|
+
For watch-only wallets the true master fingerprint is unknowable from an account xpub, so the xpub's parent fingerprint is used: signers should locate keys by derivation path.
|
|
221
|
+
|
|
222
|
+
Also on the daemon (`POST /psbt/build`, `/psbt/import-signed`, `/psbt/combine`) and the CLI (`beignet psbt build|import-signed|combine`).
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
<details>
|
|
227
|
+
<summary><b>Multisig (P2WSH sortedmulti)</b></summary>
|
|
228
|
+
|
|
229
|
+
`Wallet.createMultisig` creates a descriptor-based sorted-multisig wallet, `wsh(sortedmulti(threshold, key1, key2, ...))`: the interoperable standard used by Bitcoin Core, Sparrow and Specter. Derivation follows BIP 48 script type 2 (`m/48'/coin'/account'/2'`, receive `/0/*`, change `/1/*`) and keys are BIP 67 ordered at every index, so any wallet built from the same account xpubs produces identical addresses regardless of cosigner order.
|
|
230
|
+
|
|
231
|
+
Cosigners are account-level extended public keys (`xpub`/`tpub`, or SLIP-132 `Zpub`/`Vpub`, normalized automatically). With a mnemonic, this wallet IS one of the cosigners: its BIP 48 account xpub is derived and included automatically (pass `ourXpub` to assert it; a mismatch is rejected). Omit the mnemonic for a watch-only coordinator.
|
|
232
|
+
|
|
233
|
+
Spending is PSBT-only. `send`/`sendMany`/`sendMax` fail with `MultisigSpendError` (`code: 'MULTISIG_REQUIRES_PSBT'`).
|
|
253
234
|
|
|
254
235
|
```typescript
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
mnemonic: mnemonicA, // we are one cosigner; our xpub is added automatically
|
|
265
|
-
cosigners: [xpubB, xpubC],
|
|
266
|
-
network: 'bitcoin',
|
|
267
|
-
electrumOptions: { net, tls },
|
|
268
|
-
})
|
|
269
|
-
).value;
|
|
270
|
-
|
|
271
|
-
// Cosigner B does the same in their own instance/machine.
|
|
272
|
-
const walletB = (
|
|
273
|
-
await Wallet.createMultisig({
|
|
274
|
-
threshold: 2,
|
|
275
|
-
mnemonic: mnemonicB,
|
|
276
|
-
cosigners: [xpubA, xpubC],
|
|
277
|
-
network: 'bitcoin',
|
|
278
|
-
electrumOptions: { net, tls },
|
|
279
|
-
})
|
|
280
|
-
).value;
|
|
236
|
+
// 1. Each cosigner builds the same quorum from the others' BIP 48 account xpubs.
|
|
237
|
+
const a = await Wallet.createMultisig({
|
|
238
|
+
threshold: 2,
|
|
239
|
+
mnemonic: mnemonicA, // we are one cosigner; our xpub is added automatically
|
|
240
|
+
cosigners: [xpubB, xpubC],
|
|
241
|
+
network: EAvailableNetworks.mainnet,
|
|
242
|
+
electrumOptions: { net, tls }
|
|
243
|
+
});
|
|
244
|
+
const b = await Wallet.createMultisig({ threshold: 2, mnemonic: mnemonicB, cosigners: [xpubA, xpubC], /* ... */ });
|
|
281
245
|
|
|
282
246
|
// An optional watch-only coordinator holds no keys at all.
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// 3. Build the unsigned PSBT (works on any instance, coordinator included).
|
|
296
|
-
const built = await walletA.buildPsbt({
|
|
297
|
-
address: 'bc1q...',
|
|
298
|
-
amount: 50000,
|
|
299
|
-
satsPerByte: 4,
|
|
300
|
-
});
|
|
247
|
+
const c = await Wallet.createMultisig({ threshold: 2, cosigners: [xpubA, xpubB, xpubC], /* ... */ });
|
|
248
|
+
|
|
249
|
+
if (a.isErr() || b.isErr() || c.isErr()) return;
|
|
250
|
+
const [walletA, walletB, coordinator] = [a.value, b.value, c.value];
|
|
251
|
+
|
|
252
|
+
// 2. Fund it: every instance derives the same addresses.
|
|
253
|
+
const deposit = await walletA.getAddress();
|
|
254
|
+
|
|
255
|
+
// 3. Build the unsigned PSBT (any instance, coordinator included).
|
|
256
|
+
const built = await walletA.buildPsbt({ address: 'bc1q...', amount: 50_000, satsPerByte: 4 });
|
|
257
|
+
if (built.isErr()) return;
|
|
301
258
|
const unsigned = built.value.psbtBase64;
|
|
302
259
|
|
|
303
|
-
// 4. Each cosigner signs their own copy (below threshold
|
|
304
|
-
const signedA = walletA.signPsbtWithOurKey(unsigned)
|
|
305
|
-
const signedB = walletB.signPsbtWithOurKey(unsigned)
|
|
260
|
+
// 4. Each cosigner signs their own copy (nothing finalizes below threshold).
|
|
261
|
+
const signedA = walletA.signPsbtWithOurKey(unsigned);
|
|
262
|
+
const signedB = walletB.signPsbtWithOurKey(unsigned);
|
|
263
|
+
if (signedA.isErr() || signedB.isErr()) return;
|
|
306
264
|
|
|
307
|
-
// 5. Combine
|
|
308
|
-
const combined = coordinator.combinePsbts([signedA, signedB])
|
|
309
|
-
|
|
310
|
-
|
|
265
|
+
// 5. Combine, finalize at threshold, broadcast.
|
|
266
|
+
const combined = coordinator.combinePsbts([signedA.value, signedB.value]);
|
|
267
|
+
if (combined.isErr()) return;
|
|
268
|
+
const finalized = coordinator.importSignedPsbt(combined.value); // 2-of-3 met
|
|
269
|
+
if (finalized.isErr()) return;
|
|
270
|
+
await coordinator.broadcastTransaction(finalized.value.txHex);
|
|
311
271
|
|
|
312
|
-
//
|
|
272
|
+
// Below threshold it fails loudly:
|
|
313
273
|
// 'Input 0 is below the multisig threshold: have 1 signature(s), need 2.'
|
|
314
274
|
|
|
315
|
-
// Interop: import
|
|
316
|
-
|
|
275
|
+
// Interop: import into Bitcoin Core / Sparrow / Specter.
|
|
276
|
+
coordinator.exportDescriptors();
|
|
317
277
|
// wsh(sortedmulti(2,[fp/48h/0h/0h/2h]xpub.../0/*,[fp]xpub.../0/*,...))#checksum
|
|
318
278
|
```
|
|
319
279
|
|
|
320
|
-
|
|
280
|
+
`buildPsbt` attaches the `witnessScript` and one `bip32Derivation` per cosigner to every input. `importSignedPsbt` counts VALID partial signatures per input against the witnessScript threshold and refuses to finalize below it. Library-only for now: the daemon wallet stays single-sig.
|
|
321
281
|
|
|
322
|
-
|
|
323
|
-
- **Fee estimation source:** `Wallet.create({ feeEstimationSource })` accepts `'electrum' | 'http' | 'auto'` (default `'auto'`). `'electrum'` queries only the connected Electrum server via `blockchain.estimatefee`, so fee lookups never leak to mempool.space/blocktank over clearnet; `'auto'` prefers Electrum and falls back to HTTP only when Electrum is unavailable or returns unusable values. All remote-sourced rates are clamped to at most 5000 sat/vB. The daemon exposes the same option as `feeEstimationSource` / `--fee-source` / `BEIGNET_FEE_SOURCE`.
|
|
324
|
-
- **Electrum failover:** when multiple `electrumOptions.servers` are provided, the wallet rotates through them in order on connect/reconnect failure (then through hardcoded fallback peers for the network), with a per-server cooldown so dead servers are not hammered. `wallet.electrum.currentServer` and `wallet.electrum.rotationCount` expose the current server and rotation history.
|
|
325
|
-
- **BIP21:** `encodeBip21({ address, amountSats?, label?, message? })` builds a `bitcoin:` payment URI; the daemon's `POST /address/new` accepts `{ bip21: true, amountSats?, label?, message? }` and the CLI supports `address --bip21 [--amount <sats>] [--label L] [--message M]`.
|
|
282
|
+
</details>
|
|
326
283
|
|
|
327
|
-
|
|
284
|
+
<details>
|
|
285
|
+
<summary><b>Encrypted storage and leveled logging</b></summary>
|
|
328
286
|
|
|
329
|
-
The wallet persists
|
|
287
|
+
The wallet persists through the host-injected `TStorage` interface (`storage: { getData, setData }`), and values are handed over as-is, so by default they are stored in plaintext. Persisted data is addresses, indexes, UTXOs, transactions, balance and fee estimates: no private keys and no mnemonic are ever written, so exposure is a privacy concern (full wallet history), not fund loss.
|
|
330
288
|
|
|
331
|
-
|
|
289
|
+
Wrap any `TStorage` with `createEncryptedStorage` to encrypt at rest with AES-256-GCM under an HKDF-derived key from the seed. Pre-existing plaintext values pass through unchanged and migrate lazily as they are rewritten.
|
|
332
290
|
|
|
333
291
|
```typescript
|
|
334
|
-
import { createEncryptedStorage, Wallet } from 'beignet';
|
|
335
292
|
import * as bip39 from 'bip39';
|
|
293
|
+
import { createConsoleLogger, createEncryptedStorage, Wallet } from 'beignet';
|
|
336
294
|
|
|
337
295
|
const seed = bip39.mnemonicToSeedSync(mnemonic);
|
|
338
296
|
const wallet = await Wallet.create({
|
|
339
297
|
mnemonic,
|
|
340
298
|
storage: createEncryptedStorage({ getData, setData }, seed),
|
|
341
|
-
// ...
|
|
342
|
-
});
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Leveled Logging
|
|
346
|
-
|
|
347
|
-
Diagnostic output (debug/info/warn/error) flows through a small injectable logger, kept separate from the Lightning node's persisted structured action log (`getActionLog`). The `ILogger` interface is four methods, `debug`/`info`/`warn`/`error`(`message: string, meta?: unknown`), with level filtering `debug < info < warn < error` plus `'silent'`:
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
import { Wallet, createConsoleLogger, noopLogger } from 'beignet';
|
|
351
|
-
|
|
352
|
-
const wallet = await Wallet.create({
|
|
353
|
-
mnemonic,
|
|
354
299
|
logger: createConsoleLogger('warn'), // only warn + error reach the console
|
|
355
|
-
|
|
356
|
-
// logger: myLogger, // any ILogger: route into your own stack
|
|
357
|
-
// ...
|
|
300
|
+
electrumOptions: { net, tls }
|
|
358
301
|
});
|
|
359
302
|
```
|
|
360
303
|
|
|
361
|
-
|
|
362
|
-
- **`LightningNode`** accepts `logger` in `INodeConfig` / `fromMnemonic` options and defaults to `noopLogger` (the node prints nothing, as before). Every structured action-log entry is additionally mirrored to `logger.debug('category:action', data)`.
|
|
363
|
-
- **`BeignetNode.create({ logger, logLevel })`**: log entries that pass `logLevel` are forwarded to the injected logger (in addition to the `'log'` event), and the logger is injected into the underlying `Wallet` and `LightningNode`. Without `logger`, behavior is unchanged (events only).
|
|
364
|
-
- **Daemon:** `beignet start --log-level <debug|info|warn|error|silent>` (or `BEIGNET_LOG_LEVEL`, or `logLevel` in `~/.beignet/config.json`) prints leveled diagnostics to stderr. Unset keeps the daemon silent (the default); stdout stays reserved for command output.
|
|
304
|
+
Diagnostics flow through a small injectable `ILogger` (`debug`/`info`/`warn`/`error`, each `(message, meta?)`), with filtering `debug < info < warn < error` plus `'silent'`. This is separate from the Lightning node's persisted structured action log (`getActionLog`).
|
|
365
305
|
|
|
366
|
-
|
|
306
|
+
- `Wallet.create({ logger })` defaults to `createConsoleLogger('info')`, preserving historical console output. `disableMessages` is independent: it only gates `onMessage` callbacks.
|
|
307
|
+
- `LightningNode` defaults to `noopLogger` (silent). Every action-log entry is also mirrored to `logger.debug('category:action', data)`.
|
|
308
|
+
- `BeignetNode.create({ logger, logLevel })` forwards passing entries to the logger (in addition to the `'log'` event) and injects it into the underlying `Wallet` and `LightningNode`.
|
|
309
|
+
- Daemon: `beignet start --log-level <debug|info|warn|error|silent>` (or `BEIGNET_LOG_LEVEL`, or `logLevel` in `~/.beignet/config.json`) prints to stderr. Unset keeps the daemon silent; stdout stays reserved for command output.
|
|
367
310
|
|
|
368
|
-
|
|
311
|
+
</details>
|
|
369
312
|
|
|
370
|
-
|
|
313
|
+
## Lightning
|
|
371
314
|
|
|
372
|
-
|
|
315
|
+
> **Beignet is under active development.** Evaluate it on regtest, signet, or with small
|
|
316
|
+
> amounts you can afford to lose. Read [Status & limitations](#status--limitations) before
|
|
317
|
+
> putting meaningful mainnet funds behind it: this is a self-custodial Lightning
|
|
318
|
+
> implementation, and channel funds are only as safe as the node watching them.
|
|
319
|
+
|
|
320
|
+
`BeignetNode` from `beignet/cli` is the recommended API: it wraps the protocol layer with satoshi amounts, string channel IDs and structured error codes.
|
|
373
321
|
|
|
374
322
|
```typescript
|
|
375
323
|
import { BeignetNode, isRetryableError } from 'beignet/cli';
|
|
376
324
|
|
|
377
|
-
//
|
|
325
|
+
// Creates the wallet, storage and funding provider for you
|
|
378
326
|
const node = await BeignetNode.create({
|
|
379
|
-
mnemonic: 'abandon abandon
|
|
327
|
+
mnemonic: 'abandon abandon ... about',
|
|
380
328
|
network: 'regtest',
|
|
381
329
|
electrumHost: '127.0.0.1',
|
|
382
|
-
electrumPort: 60001
|
|
330
|
+
electrumPort: 60001
|
|
383
331
|
});
|
|
384
332
|
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
console.log(node.isReady()); // true when node has active channels
|
|
333
|
+
node.getInfo(); // { nodeId, network, alias, ... }
|
|
334
|
+
node.getHealth(); // { status: 'ready', peers, channels, ... }
|
|
335
|
+
node.isReady(); // true once the node has active channels
|
|
389
336
|
|
|
390
|
-
// Create an invoice
|
|
391
337
|
const inv = node.createInvoice(1000, 'coffee');
|
|
392
338
|
console.log(inv.bolt11);
|
|
393
339
|
|
|
394
|
-
// Pay an invoice with automatic retry logic
|
|
395
340
|
try {
|
|
396
341
|
const payment = await node.payInvoice('lnbcrt10n1...');
|
|
397
342
|
console.log(payment.status); // 'COMPLETED'
|
|
398
343
|
} catch (err) {
|
|
399
344
|
if (isRetryableError(err)) {
|
|
400
|
-
//
|
|
345
|
+
// transient: no route, timeout. Safe to retry
|
|
401
346
|
} else {
|
|
402
|
-
//
|
|
347
|
+
// permanent: invalid invoice, expired. Do not retry
|
|
403
348
|
}
|
|
404
349
|
}
|
|
405
350
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
console.log(node.listInvoices());
|
|
351
|
+
node.listChannels();
|
|
352
|
+
node.listPayments();
|
|
353
|
+
node.listInvoices();
|
|
410
354
|
|
|
411
|
-
// Clean shutdown
|
|
412
355
|
await node.destroy();
|
|
413
356
|
```
|
|
414
357
|
|
|
415
|
-
|
|
358
|
+
Events: `node:ready`, `channel:ready`, `channel:closed`, `peer:connect`, `peer:disconnect`, `peer:error`, `payment:sent`, `payment:received`, `node:error`, `log`.
|
|
359
|
+
|
|
360
|
+
Useful variants: `payInvoiceSafe` (never throws), `payInvoiceWithRetry({ maxRetries, backoffMs, maxFeeSats })`, `sendPaymentAsync` (returns the hash immediately), `connectAndOpenChannel`, `openChannelAndWait`, `sendKeysend`, `createOffer`/`payOffer`, `spliceIn`/`spliceOut`, `backup`, `gracefulShutdown`.
|
|
361
|
+
|
|
362
|
+
**→ [docs/AI_AGENT_GUIDE.md](docs/AI_AGENT_GUIDE.md)** covers deployment in depth: channel strategy, liquidity management, monitoring and Prometheus metrics, pre-flight validation, safety rails, retry/backoff patterns, idempotency keys, spend limits, drain mode, backup and recovery, mainnet checklist.
|
|
363
|
+
|
|
364
|
+
### Decision-support APIs
|
|
416
365
|
|
|
417
|
-
|
|
366
|
+
Built-in advisors, not usually found in a Lightning library:
|
|
418
367
|
|
|
419
368
|
```typescript
|
|
420
369
|
// Channel balance analysis with actionable recommendations
|
|
@@ -424,420 +373,306 @@ for (const rec of liquidity.recommendations) {
|
|
|
424
373
|
console.log(`[${rec.priority}] ${rec.type}: ${rec.reason}`);
|
|
425
374
|
}
|
|
426
375
|
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
//
|
|
431
|
-
const fees = node.getFeeSnapshot();
|
|
432
|
-
|
|
433
|
-
// Payment success probability + estimated fee before sending
|
|
434
|
-
const estimate = node.estimatePayment(bolt11);
|
|
435
|
-
|
|
436
|
-
// 11-check mainnet readiness report with weighted score
|
|
437
|
-
const readiness = node.getMainnetReadiness();
|
|
438
|
-
console.log('Score:', readiness.score + '/100', 'Ready:', readiness.ready);
|
|
376
|
+
node.getChannelSuggestions(3); // graph-based peer suggestions for opens
|
|
377
|
+
node.getFeeSnapshot(); // on-chain fee trend: OPEN_NOW / WAIT / NEUTRAL
|
|
378
|
+
node.estimatePayment(bolt11); // success probability + estimated fee, pre-send
|
|
379
|
+
node.getMainnetReadiness(); // 11-check weighted readiness report
|
|
439
380
|
```
|
|
440
381
|
|
|
441
|
-
|
|
382
|
+
<details>
|
|
383
|
+
<summary><b>Advisor execution: circular rebalancing and fee auto-tuning</b></summary>
|
|
442
384
|
|
|
443
|
-
The advisor can
|
|
444
|
-
default** and only run when explicitly enabled in the node options.
|
|
385
|
+
The advisor can act, not just recommend. Both features are **off by default**.
|
|
445
386
|
|
|
446
387
|
```typescript
|
|
447
|
-
// One-shot circular rebalance: self-payment out over
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
fromChannelId, toChannelId, 50_000, /* maxFeeSats: */ 50
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
// Inspect what the executor would do (read-only)
|
|
454
|
-
const recs = node.getAdvisorRecommendations(); // analyze() + rebalancePlan[]
|
|
388
|
+
// One-shot circular rebalance: self-payment out over `from` and back in over `to`.
|
|
389
|
+
// Aborts WITHOUT paying if the route fee exceeds maxFeeSats.
|
|
390
|
+
await node.rebalanceChannel(fromChannelId, toChannelId, 50_000, /* maxFeeSats */ 50);
|
|
455
391
|
|
|
456
|
-
//
|
|
457
|
-
|
|
392
|
+
node.getAdvisorRecommendations(); // read-only: analyze() + rebalancePlan[]
|
|
393
|
+
await node.executeRebalances(/* budgetSatsPerDay */ 500);
|
|
458
394
|
```
|
|
459
395
|
|
|
460
|
-
Automatic modes
|
|
396
|
+
Automatic modes, opt-in via `BeignetNodeOptions` / `INodeConfig`:
|
|
461
397
|
|
|
462
398
|
```typescript
|
|
463
399
|
const node = await BeignetNode.create({
|
|
464
400
|
mnemonic,
|
|
465
|
-
// Periodically executes the rebalance plan. Routing fees spent on
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// resets at midnight UTC.
|
|
401
|
+
// Periodically executes the rebalance plan. Routing fees spent on rebalances
|
|
402
|
+
// are capped per UTC day and the running spend is persisted, so restarts
|
|
403
|
+
// never overspend the same day. Resets at midnight UTC.
|
|
469
404
|
autoRebalance: { enabled: true, budgetSatsPerDay: 500, minImbalancePct: 20 },
|
|
470
405
|
// Every intervalMs (default 6h) nudges each channel's proportional fee:
|
|
471
|
-
// +25% when outbound is depleted (<20% local) but still forwarding,
|
|
472
|
-
//
|
|
473
|
-
//
|
|
406
|
+
// +25% when outbound is depleted (<20% local) but still forwarding, -25% when
|
|
407
|
+
// the channel saw no forwards in the window, clamped to [floorPpm, ceilPpm].
|
|
408
|
+
// One adjustment per channel per interval.
|
|
474
409
|
autoTuneFees: { enabled: true, floorPpm: 1, ceilPpm: 5_000 }
|
|
475
410
|
});
|
|
476
411
|
```
|
|
477
412
|
|
|
478
|
-
Daemon
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if a counterparty broadcasts a revoked commitment while you are offline, nobody
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
pre-signed to_local penalty) and ships it to one or more remote towers over the
|
|
490
|
-
standard BOLT 8 Noise transport. When a tower later sees the breach transaction on
|
|
491
|
-
chain, it decrypts the kit and broadcasts the penalty on your behalf — reclaiming
|
|
492
|
-
the channel even though you never came back online.
|
|
493
|
-
|
|
494
|
-
- **Altruist only.** Sessions use `reward = 0`; towers take no cut. There is no
|
|
495
|
-
server mode (beignet is a tower *client*, not a tower).
|
|
496
|
-
- **LND-tower compatible.** Implements LND's `wtwire` protocol (Init/CreateSession/
|
|
497
|
-
StateUpdate/DeleteSession, message types 600-607) and the version-0 justice blob
|
|
498
|
-
(XChaCha20-Poly1305, breach hint = `SHA256(txid)[:16]`, key = `SHA256(txid‖txid)`),
|
|
499
|
-
so it interoperates with existing public LND altruist towers.
|
|
500
|
-
- **Legacy + anchor channels.** The to_local revocation penalty (the fund-critical
|
|
501
|
-
breach punishment) is packed for both; taproot channels are not yet backed up.
|
|
502
|
-
- **Durable.** Per-tower session state and the un-acked update backlog are persisted
|
|
503
|
-
(encrypted at rest) and drained with exponential backoff on reconnect. An un-acked
|
|
504
|
-
update is never dropped silently.
|
|
505
|
-
|
|
506
|
-
Configure towers as `pubkey@host:port` URIs (off when empty):
|
|
507
|
-
|
|
508
|
-
```ts
|
|
413
|
+
Daemon: `POST /rebalance`, `GET /advisor/recommendations`, `POST /advisor/execute-rebalances`.
|
|
414
|
+
CLI: `beignet rebalance <from> <to> <sats> --max-fee <sats>`, `beignet advisor recommendations`, `beignet advisor execute-rebalances [--budget <sats>]`.
|
|
415
|
+
|
|
416
|
+
</details>
|
|
417
|
+
|
|
418
|
+
<details>
|
|
419
|
+
<summary><b>Watchtowers (altruist client)</b></summary>
|
|
420
|
+
|
|
421
|
+
Penalty enforcement normally needs this node's chain monitor to be online: if a counterparty broadcasts a revoked commitment while you are offline, nobody sweeps the breach. The watchtower client closes that gap. At every revocation it builds an encrypted justice kit (the revoked commitment's breach hint plus a pre-signed to_local penalty) and ships it to remote towers over BOLT 8. When a tower later sees the breach on chain it decrypts the kit and broadcasts the penalty for you.
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
509
424
|
const node = await BeignetNode.create({
|
|
510
425
|
mnemonic,
|
|
511
|
-
watchtowers: ['03abc...@tower.example.com:9911']
|
|
426
|
+
watchtowers: ['03abc...@tower.example.com:9911'] // off when empty
|
|
512
427
|
});
|
|
513
428
|
```
|
|
514
429
|
|
|
515
|
-
|
|
516
|
-
`
|
|
517
|
-
|
|
518
|
-
(
|
|
430
|
+
- **Altruist only.** Sessions use `reward = 0`. There is no server mode: beignet is a tower client, not a tower.
|
|
431
|
+
- **LND-tower compatible.** Implements LND's `wtwire` protocol (Init/CreateSession/StateUpdate/DeleteSession, message types 600-607) and the version-0 justice blob (XChaCha20-Poly1305, breach hint `SHA256(txid)[:16]`, key `SHA256(txid‖txid)`), so it works with existing public LND altruist towers.
|
|
432
|
+
- **Legacy + anchor channels.** The to_local revocation penalty (the fund-critical punishment) is packed for both. Taproot channels are not yet backed up.
|
|
433
|
+
- **Durable.** Per-tower session state and the un-acked backlog are persisted (encrypted at rest) and drained with exponential backoff on reconnect. An un-acked update is never dropped silently.
|
|
434
|
+
|
|
435
|
+
Daemon: `GET /watchtowers`, `POST /watchtower/add`, `DELETE /watchtower/remove`.
|
|
436
|
+
CLI: `beignet watchtower list|add <pubkey@host:port>|remove <uri>`, daemon flag `--watchtower` (repeatable) or `BEIGNET_WATCHTOWERS`.
|
|
519
437
|
|
|
520
|
-
|
|
438
|
+
</details>
|
|
521
439
|
|
|
522
|
-
|
|
440
|
+
## HTTP daemon & CLI
|
|
441
|
+
|
|
442
|
+
The same node runs as an HTTP/SSE daemon for language-agnostic integrations, driven by a JSON CLI.
|
|
523
443
|
|
|
524
444
|
```bash
|
|
525
|
-
#
|
|
526
|
-
npx beignet
|
|
445
|
+
# 1. Generate a mnemonic + ~/.beignet/config.json
|
|
446
|
+
npx beignet init --network regtest
|
|
447
|
+
|
|
448
|
+
# 2. Start the daemon (add --daemon to background it)
|
|
449
|
+
BEIGNET_ELECTRUM_HOST=127.0.0.1 BEIGNET_ELECTRUM_PORT=60001 BEIGNET_ELECTRUM_TLS=false \
|
|
450
|
+
npx beignet start --network regtest --api-token mytoken
|
|
451
|
+
|
|
452
|
+
# 3. Drive it with the CLI (thin HTTP client, JSON out)
|
|
453
|
+
npx beignet info --pretty
|
|
454
|
+
npx beignet address
|
|
455
|
+
npx beignet channel connect-and-open <pubkey> <host> <port> 200000
|
|
456
|
+
npx beignet invoice create 1000 "coffee"
|
|
457
|
+
npx beignet invoice pay <bolt11>
|
|
458
|
+
```
|
|
527
459
|
|
|
528
|
-
|
|
529
|
-
|
|
460
|
+
Electrum and most other settings come from `~/.beignet/config.json` or the environment (`BEIGNET_MNEMONIC`, `BEIGNET_ELECTRUM_HOST`, `BEIGNET_ELECTRUM_PORT`, `BEIGNET_NETWORK`, ...). Run `npx beignet help` for the full command and flag list.
|
|
461
|
+
|
|
462
|
+
Or over HTTP directly:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
curl -X POST http://localhost:2112/invoice/create -H 'Authorization: Bearer mytoken' \
|
|
530
466
|
-H 'Content-Type: application/json' -d '{"amountSats": 1000, "description": "coffee"}'
|
|
531
467
|
|
|
532
|
-
|
|
533
|
-
curl -X POST http://localhost:2112/invoice/pay -H 'Authorization: Bearer <token>' \
|
|
468
|
+
curl -X POST http://localhost:2112/invoice/pay -H 'Authorization: Bearer mytoken' \
|
|
534
469
|
-H 'Content-Type: application/json' -d '{"bolt11": "lnbcrt10n1..."}'
|
|
535
470
|
|
|
536
|
-
|
|
537
|
-
curl
|
|
538
|
-
|
|
539
|
-
# Simple readiness check (auth-exempt, for load balancers)
|
|
540
|
-
curl http://localhost:2112/ready
|
|
471
|
+
curl -N http://localhost:2112/events -H 'Authorization: Bearer mytoken' # SSE stream
|
|
472
|
+
curl http://localhost:2112/ready # load-balancer probe
|
|
541
473
|
```
|
|
542
474
|
|
|
543
|
-
|
|
475
|
+
- Responses are `{ "ok": true, "result": {...} }` or `{ "ok": false, "error": { "code": "...", "message": "..." } }`.
|
|
476
|
+
- Full spec at `GET /openapi.json`.
|
|
477
|
+
- `GET /health`, `/ready`, `/openapi.json` and `/metrics` are auth-exempt; everything else requires the bearer token **when one is configured**. Auth is off unless you set `apiToken` or `apiKeys` (named keys with `readonly`/`invoice`/`admin` scopes), so configure a token before exposing the daemon anywhere. It binds `127.0.0.1` by default.
|
|
478
|
+
- Embed it instead of shelling out: `import { startDaemon } from 'beignet/cli'`.
|
|
544
479
|
|
|
545
|
-
|
|
546
|
-
Full spec: `GET /openapi.json` (no auth required).
|
|
480
|
+
## Protocol layer (advanced)
|
|
547
481
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
Most users should use `BeignetNode` above. Use `LightningNode` only if you need direct access to the protocol layer (bigint amounts, Buffer IDs, raw BOLT messages).
|
|
482
|
+
Use this only if you need the BOLT layer directly: bigint msat, Buffer IDs, raw wire messages. `beignet/lightning` exports **namespaces**, not flat symbols.
|
|
551
483
|
|
|
552
484
|
```typescript
|
|
553
|
-
import { Wallet, generateMnemonic } from 'beignet';
|
|
554
|
-
import { LightningNode, WalletFundingProvider, Network } from 'beignet/lightning';
|
|
555
485
|
import net from 'net';
|
|
556
486
|
import tls from 'tls';
|
|
487
|
+
import { Wallet, generateMnemonic } from 'beignet';
|
|
488
|
+
import { invoice, node as ln, wallet as lnWallet } from 'beignet/lightning';
|
|
557
489
|
|
|
558
490
|
const mnemonic = generateMnemonic();
|
|
559
491
|
|
|
560
|
-
// 1.
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
electrumOptions: { net, tls },
|
|
564
|
-
})).value;
|
|
492
|
+
// 1. On-chain wallet (the same mnemonic funds both layers)
|
|
493
|
+
const res = await Wallet.create({ mnemonic, electrumOptions: { net, tls } });
|
|
494
|
+
if (res.isErr()) throw res.error;
|
|
565
495
|
|
|
566
|
-
// 2.
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
network: Network.REGTEST,
|
|
496
|
+
// 2. Lightning node with auto-funding from the wallet
|
|
497
|
+
const node = ln.LightningNode.fromMnemonic(mnemonic, {
|
|
498
|
+
network: invoice.Network.REGTEST,
|
|
570
499
|
enableNetworking: true,
|
|
571
|
-
fundingProvider
|
|
500
|
+
fundingProvider: new lnWallet.WalletFundingProvider(res.value)
|
|
572
501
|
});
|
|
573
502
|
|
|
574
|
-
// 3. Connect
|
|
503
|
+
// 3. Connect and open: fully automatic with a funding provider
|
|
575
504
|
await node.connectPeer('03...pubkey', '127.0.0.1', 9735);
|
|
576
505
|
node.openChannel('03...pubkey', 100_000n);
|
|
577
506
|
|
|
578
|
-
// 4.
|
|
579
|
-
|
|
580
|
-
amountMsat: 50_000n,
|
|
581
|
-
description: 'Payment for coffee',
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// 5. Pay a BOLT 11 invoice
|
|
507
|
+
// 4. Invoice and payment
|
|
508
|
+
node.createInvoice({ amountMsat: 50_000n, description: 'coffee' });
|
|
585
509
|
node.sendPayment(invoiceString);
|
|
586
510
|
|
|
587
|
-
//
|
|
588
|
-
node.on('channel:ready', (channelId) =>
|
|
589
|
-
|
|
590
|
-
});
|
|
591
|
-
node.on('payment:received', (payment) => {
|
|
592
|
-
console.log('Received:', payment.amountMsat, 'msat');
|
|
593
|
-
});
|
|
594
|
-
node.on('node:error', (err) => {
|
|
595
|
-
console.error(`[${err.code}]`, err.message);
|
|
596
|
-
});
|
|
511
|
+
// 5. Events. Channel-scoped events carry an object, not a bare id
|
|
512
|
+
node.on('channel:ready', ({ channelId }) => console.log(channelId.toString('hex')));
|
|
513
|
+
node.on('payment:received', (p) => console.log(p.amountMsat, 'msat'));
|
|
514
|
+
node.on('node:error', (err) => console.error(`[${err.code}]`, err.message));
|
|
597
515
|
```
|
|
598
516
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
const node = LightningNode.fromMnemonic(mnemonic, {
|
|
603
|
-
network: Network.REGTEST,
|
|
604
|
-
enableNetworking: true,
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
const channel = node.openChannel('03...pubkey', 100_000n);
|
|
608
|
-
// Build your own funding tx, then:
|
|
609
|
-
const channelId = node.createFunding(channel, fundingTxid, outputIndex, signature);
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### Architecture
|
|
613
|
-
|
|
614
|
-
Beignet's Lightning implementation follows a layered, transport-agnostic design:
|
|
517
|
+
Without a `fundingProvider`, build the funding transaction yourself and call `node.createFunding(channel, fundingTxid, outputIndex, signature)` after `openChannel`.
|
|
615
518
|
|
|
616
519
|
```
|
|
617
|
-
LightningNode
|
|
618
|
-
├── ChannelManager
|
|
619
|
-
│ └── Channel
|
|
620
|
-
├── PeerManager
|
|
621
|
-
│ └── Peer
|
|
622
|
-
├── NetworkGraph
|
|
623
|
-
├── InvoiceManager
|
|
624
|
-
├── ChainMonitor
|
|
625
|
-
└── FundingProvider?
|
|
520
|
+
LightningNode High-level API (EventEmitter)
|
|
521
|
+
├── ChannelManager Multiplexes messages to Channel instances
|
|
522
|
+
│ └── Channel BOLT 2 state machine (returns ChannelAction[])
|
|
523
|
+
├── PeerManager TCP connections + Noise_XK encrypted transport
|
|
524
|
+
│ └── Peer Per-connection BOLT 8 handshake + message framing
|
|
525
|
+
├── NetworkGraph BOLT 7 gossip topology + Dijkstra pathfinding
|
|
526
|
+
├── InvoiceManager BOLT 11 encode/decode/sign
|
|
527
|
+
├── ChainMonitor BOLT 5 force-close detection + sweep
|
|
528
|
+
└── FundingProvider? Auto-builds + broadcasts funding txs (via Wallet)
|
|
626
529
|
```
|
|
627
530
|
|
|
628
|
-
**Key design principle:**
|
|
531
|
+
**Key design principle:** `Channel` is fully transport-agnostic. Every method returns a `ChannelAction[]` (send message, broadcast tx, watch output, ...) that `ChannelManager` maps to real transport or chain operations, which makes the state machine testable without network I/O.
|
|
629
532
|
|
|
630
|
-
|
|
533
|
+
**→ [src/lightning/README.md](src/lightning/README.md)** documents the protocol layer in detail: data flow, events reference, typed payment errors, channel lifecycle, zero-conf, anchors, dual funding, splicing, offers, onion messages, forwarding, chain monitoring.
|
|
631
534
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
535
|
+
<details>
|
|
536
|
+
<summary><b>BOLT coverage</b></summary>
|
|
537
|
+
|
|
538
|
+
| BOLT | Specification | Implemented |
|
|
539
|
+
|------|--------------|-------------|
|
|
540
|
+
| 1 | Base Protocol | Peer messaging, init, error, ping/pong, feature negotiation, peer storage |
|
|
635
541
|
| 2 | Channel Management | Full state machine: open, fund, normal operation, shutdown, close, reestablish; v2 dual-funded opens (interactive-tx), splicing, quiescence |
|
|
636
542
|
| 3 | Transactions | Commitment txs, HTLC scripts, funding scripts, anchor outputs, fee calculation; simple taproot channels (MuSig2 funding, Schnorr HTLC sigs) |
|
|
637
543
|
| 4 | Onion Routing | Sphinx encryption, TLV hop payloads, payment_secret, failure codes, route blinding, onion messages |
|
|
638
544
|
| 5 | On-Chain | Force-close detection, HTLC sweep, output resolution, chain monitoring, wallet-funded anchor fee bumping (commitment CPFP + zero-fee HTLC fee-attach) |
|
|
639
|
-
| 7 | Gossip | Channel/node announcements, network graph, Dijkstra routing, gossip sync |
|
|
545
|
+
| 7 | Gossip | Channel/node announcements, network graph, Dijkstra routing, gossip sync, Rapid Gossip Sync |
|
|
640
546
|
| 8 | Transport | Noise_XK handshake, encrypted transport, key rotation |
|
|
641
547
|
| 9 | Features | DATA_LOSS_PROTECT, STATIC_REMOTE_KEY, PAYMENT_SECRET, TLV_ONION, BASIC_MPP, CHANNEL_TYPE, GOSSIP_QUERIES, ANCHORS_ZERO_FEE_HTLC_TX (default), ROUTE_BLINDING, ONION_MESSAGES, QUIESCE, SCID_ALIAS, ZERO_CONF, KEYSEND, OPTION_TAPROOT, OPTION_WILL_FUND |
|
|
548
|
+
| 10 | DNS Bootstrap | Seed resolution for discovering initial peers |
|
|
642
549
|
| 11 | Invoices | Encode, decode, sign, verify, amount formatting, hold invoices |
|
|
643
|
-
| 12 | Offers | Offer encode/decode, invoice_request/invoice
|
|
550
|
+
| 12 | Offers | Offer encode/decode, invoice_request/invoice over onion messages, receive-side settlement, async payment offers |
|
|
644
551
|
| bLIP-51 | Liquidity Ads | lease_rates/request_funds/will_fund negotiation, lease fee accounting, CLTV-locked lessor to_local, advisor lease quoting |
|
|
645
552
|
|
|
646
|
-
|
|
553
|
+
</details>
|
|
647
554
|
|
|
648
|
-
|
|
555
|
+
<details>
|
|
556
|
+
<summary><b>Module reference (23 modules under <code>src/lightning/</code>)</b></summary>
|
|
649
557
|
|
|
650
558
|
| Module | Description |
|
|
651
559
|
|--------|-------------|
|
|
652
|
-
| `crypto/` | ChaCha20-Poly1305 AEAD, ECDH, HKDF
|
|
653
|
-
| `message/` | Wire
|
|
560
|
+
| `crypto/` | ChaCha20-Poly1305 AEAD, ECDH, HKDF, MuSig2 (BIP 327) for taproot channels |
|
|
561
|
+
| `message/` | Wire encode/decode for all channel, gossip and control messages |
|
|
654
562
|
| `features/` | Feature flag bitmap management (BOLT 9) |
|
|
655
|
-
| `transport/` | Noise_XK handshake,
|
|
656
|
-
| `keys/` | HD
|
|
657
|
-
| `script/` | Funding
|
|
658
|
-
| `channel/` | Channel state machine, ChannelManager, commitment builder,
|
|
563
|
+
| `transport/` | Noise_XK handshake, transport cipher, TCP/WebSocket peer connections, PeerManager |
|
|
564
|
+
| `keys/` | HD derivation, per-commitment secrets (shachain), signing, wallet keys |
|
|
565
|
+
| `script/` | Funding 2-of-2 multisig, commitment outputs, HTLC scripts, revocation, anchors, taproot scripts |
|
|
566
|
+
| `channel/` | Channel state machine, ChannelManager, commitment builder, actions, validation, liquidity ads |
|
|
659
567
|
| `chain/` | ChainMonitor, ChainWatcher, output resolver, closing tx, sweep tx, Electrum backend |
|
|
660
|
-
| `invoice/` | BOLT 11
|
|
568
|
+
| `invoice/` | BOLT 11 encoding/decoding, bech32 words, signature verification |
|
|
661
569
|
| `gossip/` | NetworkGraph, Dijkstra pathfinding, gossip sync state machine, SCID encoding |
|
|
662
|
-
| `onion/` | Sphinx crypto,
|
|
663
|
-
| `onion-message/` |
|
|
570
|
+
| `onion/` | Sphinx crypto, packet construction/processing, hop payloads, failures, blinded paths |
|
|
571
|
+
| `onion-message/` | Onion message construction/processing (carries BOLT 12 and async-payment messages) |
|
|
664
572
|
| `offer/` | BOLT 12 offers: encode/decode, OfferManager invoice_request/invoice flows |
|
|
665
573
|
| `async-payments/` | Hold invoices and AsyncPaymentManager (LSP held-forward, release_held_htlc, wake) |
|
|
666
574
|
| `interactive-tx/` | Interactive transaction construction for v2 dual-funded opens and splicing |
|
|
667
|
-
| `
|
|
668
|
-
| `
|
|
669
|
-
| `
|
|
670
|
-
| `
|
|
671
|
-
| `
|
|
672
|
-
| `
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
### LightningNode API
|
|
677
|
-
|
|
678
|
-
`LightningNode` is an `EventEmitter` that provides the high-level API:
|
|
679
|
-
|
|
680
|
-
**Peer Management:**
|
|
681
|
-
- `connectPeer(pubkey, host, port)` — Establish encrypted connection
|
|
682
|
-
- `disconnectPeer(pubkey)` — Disconnect from peer
|
|
683
|
-
- `listPeers()` — List connected peers
|
|
684
|
-
- `getNodeId()` — Get this node's public key
|
|
685
|
-
|
|
686
|
-
**Channel Operations:**
|
|
687
|
-
- `openChannel(peerPubkey, fundingSatoshis, pushMsat?)` — Open a channel (auto-funds when `fundingProvider` is set)
|
|
688
|
-
- `createFunding(channel, txid, outputIndex, signature)` — Manual funding (when no `fundingProvider`)
|
|
689
|
-
- `handleFundingConfirmed(channelId)` — Notify funding tx confirmed
|
|
690
|
-
- `closeChannel(channelId, scriptPubkey)` — Cooperative close
|
|
691
|
-
- `forceCloseChannel(channelId, destinationScript)` — Force close (unilateral)
|
|
692
|
-
- `listChannels()` — List all channels
|
|
693
|
-
- `getChannel(channelId)` — Get channel details
|
|
694
|
-
|
|
695
|
-
**Payments:**
|
|
696
|
-
- `createInvoice(options)` — Generate a BOLT 11 invoice
|
|
697
|
-
- `sendPayment(invoiceString)` — Send a payment
|
|
698
|
-
- `sendPaymentToRoute(route, paymentHash, ...)` — Send via explicit route
|
|
699
|
-
|
|
700
|
-
**Chain Events:**
|
|
701
|
-
- `handleNewBlock(height)` — Process new block
|
|
702
|
-
- `handleOutputSpent(txid, index, spendingTx, height)` — Track spent outputs
|
|
703
|
-
|
|
704
|
-
**Events:**
|
|
705
|
-
- `payment:received` — Incoming payment fulfilled
|
|
706
|
-
- `payment:sent` — Outgoing payment succeeded
|
|
707
|
-
- `payment:failed` — Outgoing payment failed
|
|
708
|
-
- `channel:ready` — Channel entered NORMAL state
|
|
709
|
-
- `channel:closed` — Channel closed
|
|
710
|
-
- `peer:connect` / `peer:disconnect` — Peer connection changes
|
|
711
|
-
- `node:error` — Structured error (code, message, channelId, timestamp)
|
|
712
|
-
|
|
713
|
-
## Running Tests
|
|
714
|
-
|
|
715
|
-
```bash
|
|
716
|
-
# Run lightning unit tests (2740+ tests, no infrastructure needed)
|
|
717
|
-
npm run test:lightning
|
|
575
|
+
| `watchtower/` | Altruist watchtower client: wtwire protocol, justice blobs, tower sessions |
|
|
576
|
+
| `backup/` | Static channel backup (SCB) export/import |
|
|
577
|
+
| `node/` | LightningNode orchestrator, the main protocol-layer entry point |
|
|
578
|
+
| `wallet/` | WalletFundingProvider, adapts the on-chain Wallet for auto-funded opens |
|
|
579
|
+
| `bootstrap/` | DNS seed resolution for discovering initial peers |
|
|
580
|
+
| `advisor/` | Liquidity, fee and channel-suggestion advisors |
|
|
581
|
+
| `storage/` | SQLite persistence backend, channel state serialization |
|
|
582
|
+
| `validation/` | Input validation shared across modules |
|
|
718
583
|
|
|
719
|
-
|
|
720
|
-
npm run test:cli
|
|
584
|
+
`beignet/lightning` re-exports each of these as a namespace (`crypto`, `message`, `node`, ...). `async-payments` and `watchtower` are reachable via their source paths.
|
|
721
585
|
|
|
722
|
-
|
|
723
|
-
npm run test:integration
|
|
586
|
+
</details>
|
|
724
587
|
|
|
725
|
-
|
|
726
|
-
npm run test:interop
|
|
588
|
+
## Tests
|
|
727
589
|
|
|
728
|
-
|
|
729
|
-
npm run test:
|
|
590
|
+
```bash
|
|
591
|
+
npm run test:lightning # 4000+ Lightning unit tests, no infrastructure needed
|
|
592
|
+
npm run test:cli # 900+ CLI + daemon unit tests, no infrastructure needed
|
|
593
|
+
npm run test:conformance # 250+ official BOLT vector cases (subset of test:lightning)
|
|
594
|
+
npm run test:integration # daemon/Electrum integration (needs an Electrum server)
|
|
595
|
+
npm run test:interop # 190+ cases vs LND/CLN/Eclair (needs Docker)
|
|
596
|
+
npm run test:all # Lightning + CLI + interop (needs Docker + Electrum)
|
|
730
597
|
```
|
|
731
598
|
|
|
732
|
-
|
|
599
|
+
Counts are floors, not snapshots. Run the suites for exact numbers.
|
|
733
600
|
|
|
734
|
-
The
|
|
601
|
+
The on-chain wallet suites live in `tests/*.test.ts` and connect to **live public
|
|
602
|
+
Electrum servers**, so they need network access and can fail on a server outage
|
|
603
|
+
rather than on your change. Each script runs `yarn build` first, so yarn has to be
|
|
604
|
+
installed:
|
|
735
605
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
| Channel State Machine (BOLT 2) | 161 | Message encode/decode, channel types, validation, Channel, commitment builder, ChannelManager |
|
|
742
|
-
| Chain Monitor (BOLT 5) | 67 | Closing tx, sweep tx, output resolver, chain monitor, force close |
|
|
743
|
-
| Invoices (BOLT 11) | 98 | Types, words, amount, signing, decode, encode |
|
|
744
|
-
| Gossip & Routing (BOLT 7) | 104 | SCID, messages, validation, network graph, pathfinding |
|
|
745
|
-
| Onion & Payments (BOLT 4) | 83 | Sphinx crypto, hop payloads, onion construction/processing, failure handling |
|
|
746
|
-
| Node API | 50 | LightningNode orchestrator, invoice management, payment send/receive, HTLC forwarding |
|
|
747
|
-
| PeerManager Integration | 18 | PeerManager wiring, peer management, event forwarding |
|
|
748
|
-
| Production Hardening | — | Error visibility, input validation, resource management, BOLT 1 error propagation |
|
|
749
|
-
| **Interop (LND/CLN/Eclair)** | **129** | **Multi-implementation interop: TCP handshake, channel lifecycle, bidirectional payments, anchor channels, anchor force-close with wallet-funded CPFP + HTLC-timeout fee-attach, crash recovery against LND v0.20.0, CLN, and Eclair** |
|
|
606
|
+
```bash
|
|
607
|
+
npm run test:wallet # also test:transaction, test:electrum, test:storage,
|
|
608
|
+
# test:derivation, test:receive, test:boost
|
|
609
|
+
npm test # everything: build, on-chain, Lightning, CLI, interop
|
|
610
|
+
```
|
|
750
611
|
|
|
751
|
-
|
|
612
|
+
The on-chain files without a dedicated script (multisig, PSBT, watch-only,
|
|
613
|
+
descriptors, signet and others) run through mocha directly:
|
|
752
614
|
|
|
753
|
-
|
|
615
|
+
```bash
|
|
616
|
+
npx mocha --exit -r ts-node/register 'tests/multisig.test.ts'
|
|
617
|
+
```
|
|
754
618
|
|
|
755
|
-
|
|
619
|
+
`test:conformance` runs the official BOLT test vectors (BOLT 1 bigsize/TLV, BOLT 3 commitments and anchors and per-commitment secrets, BOLT 4 onion/route-blinding/onion-errors, BOLT 7 extended queries, BOLT 8 transport, BOLT 11 invoices, BOLT 12 offers/signatures) under `tests/lightning/conformance/`.
|
|
756
620
|
|
|
757
|
-
|
|
758
|
-
|
|
621
|
+
<details>
|
|
622
|
+
<summary><b>Interop testing against real implementations</b></summary>
|
|
759
623
|
|
|
760
|
-
|
|
624
|
+
The interop suite drives beignet against real nodes on Bitcoin regtest.
|
|
761
625
|
|
|
762
626
|
```bash
|
|
763
|
-
|
|
764
|
-
docker compose -f docker/docker-compose.yml up -d
|
|
765
|
-
|
|
766
|
-
# Wait for nodes to sync (~30 seconds)
|
|
767
|
-
|
|
768
|
-
# Run interop tests
|
|
627
|
+
docker compose -f docker/docker-compose.yml up -d # wait ~30s for nodes to sync
|
|
769
628
|
npm run test:interop
|
|
770
629
|
```
|
|
771
630
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
#### LND (43 tests)
|
|
775
|
-
|
|
776
|
-
| Tier | Tests | Validates |
|
|
777
|
-
|------|-------|-----------|
|
|
778
|
-
| **1: TCP & Init** | 5 | BOLT 8 Noise_XK handshake, BOLT 1 init exchange, feature negotiation, disconnect/reconnect, ping/pong survival |
|
|
779
|
-
| **2: Channel Open** | 3 | LND opens channel to beignet, balance verification, error-free lifecycle |
|
|
780
|
-
| **3: LND pays beignet** | 3 | Receive payment from LND, payment_secret validation, multiple sequential payments |
|
|
781
|
-
| **4: Beignet pays LND** | 3 | Pay LND invoice, outbound payment_secret, graceful failure handling |
|
|
782
|
-
| **5-9: Advanced** | 11 | Channel close, reestablish, gossip sync, MPP payments, SCID aliases |
|
|
783
|
-
| **10: Inbound connections** | 4 | LND connects to beignet listener, channel open from inbound peer |
|
|
784
|
-
| **11-13: Anchor & Recovery** | 14 | Anchor channels, beignet-funded opens, crash recovery |
|
|
785
|
-
|
|
786
|
-
#### CLN (42 tests)
|
|
787
|
-
|
|
788
|
-
| Tier | Tests | Validates |
|
|
789
|
-
|------|-------|-----------|
|
|
790
|
-
| **1: TCP & Init** | 5 | BOLT 8 handshake, init exchange, feature negotiation |
|
|
791
|
-
| **2-9: Channel & Payments** | 23 | Channel lifecycle, bidirectional payments, close, reestablish, gossip, MPP, SCID aliases |
|
|
792
|
-
| **10: Inbound** | 4 | Inbound connections from CLN |
|
|
793
|
-
| **12-14: Anchor & Recovery** | 10 | Anchor channels, beignet-funded opens, crash recovery |
|
|
794
|
-
|
|
795
|
-
#### Eclair (42 tests)
|
|
631
|
+
Services in `docker/docker-compose.yml`:
|
|
796
632
|
|
|
797
|
-
|
|
|
798
|
-
|
|
799
|
-
|
|
|
800
|
-
|
|
|
801
|
-
|
|
|
802
|
-
|
|
|
633
|
+
| Service | Image | Ports |
|
|
634
|
+
|---------|-------|-------|
|
|
635
|
+
| bitcoind | Bitcoin Core 29.1 (regtest) | RPC 43782, ZMQ 28334/28335/28336 |
|
|
636
|
+
| lnd | `lightninglabs/lnd:v0.20.0-beta` | P2P 9735, REST 8081 |
|
|
637
|
+
| cln | `elementsproject/lightningd:v26.06.1` | CLNRest 3010 |
|
|
638
|
+
| eclair | `polarlightning/eclair:0.13.1` | HTTP API 8082 |
|
|
639
|
+
| electrs | `getumbrel/electrs:v0.10.10` | Electrum 60001 |
|
|
803
640
|
|
|
804
|
-
|
|
641
|
+
Covered per implementation: BOLT 8 handshake and BOLT 1 init/feature negotiation, disconnect/reconnect and ping/pong survival, channel open in both directions, bidirectional payments and payment_secret validation, MPP, SCID aliases, cooperative close, reestablish, gossip sync, inbound connections, anchor channels, anchor force-close with wallet-funded CPFP and HTLC-timeout fee-attach, and crash recovery. Beyond the shared matrix: taproot channel lifecycle vs LND (open, pay both directions, reestablish, coop and force close, penalty, SCB recovery), splice matrix and lease/liquidity-ads flows vs CLN, `simple_close` vs Eclair, blinded-path payments, and the watchtower client vs an LND tower.
|
|
805
642
|
|
|
806
|
-
|
|
643
|
+
Interop tests are excluded from `npm run test:lightning`.
|
|
807
644
|
|
|
808
|
-
|
|
809
|
-
- **bitcoind** — Bitcoin Core regtest node (RPC port 43782, ZMQ on 28334/28335)
|
|
810
|
-
- **LND** — Lightning Network Daemon v0.20.0-beta (P2P port 9735, REST port 8081)
|
|
811
|
-
- **CLN** — Core Lightning (CLNRest API on port 3010)
|
|
812
|
-
- **Eclair** — ACINQ Eclair (HTTP API on port 8082)
|
|
645
|
+
</details>
|
|
813
646
|
|
|
814
|
-
##
|
|
647
|
+
## Status & limitations
|
|
815
648
|
|
|
816
|
-
Beignet is under active development.
|
|
649
|
+
Beignet is under active development. Known gaps and caveats:
|
|
817
650
|
|
|
818
|
-
| Feature | Status |
|
|
651
|
+
| Feature | Status | Detail |
|
|
819
652
|
|---------|--------|--------|
|
|
820
|
-
|
|
|
821
|
-
|
|
|
822
|
-
|
|
|
823
|
-
|
|
|
824
|
-
|
|
|
825
|
-
|
|
|
826
|
-
|
|
|
827
|
-
|
|
|
828
|
-
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
-
|
|
833
|
-
-
|
|
834
|
-
-
|
|
835
|
-
-
|
|
836
|
-
-
|
|
653
|
+
| Mainnet battle-testing | Limited | Interop-tested on regtest, with some flows validated live on mainnet. Exercise caution with large balances. |
|
|
654
|
+
| Watchtowers | Client only (altruist) | Punishes breaches while you are offline via remote LND altruist towers. Legacy + anchor channels only: taproot channels are not backed up. No server mode. |
|
|
655
|
+
| LSP / LSPS protocols | Not implemented | No automated inbound liquidity via LSPS0/1/2. Liquidity ads (bLIP-51) cover negotiated leases; otherwise open channels manually. |
|
|
656
|
+
| Trampoline routing | Not implemented | All route computation is local. |
|
|
657
|
+
| BOLT 12 offers | Newer | Offers, invoice_request/invoice over onion messages and receive-side settlement work, but the surface is less battle-tested than BOLT 11. Prefer BOLT 11 in production. |
|
|
658
|
+
| Async payments | LSP-dependent | Hold invoices plus AsyncPaymentManager let an offline receiver be paid, but the receiver's LSP must run the held-forward/wake flow. |
|
|
659
|
+
| Simple taproot channels | Experimental | Full lifecycle validated against LND v0.20 on regtest, but the feature bit is still in staging upstream. Not recommended for mainnet balances. |
|
|
660
|
+
| Splicing / dual funding | Partial | Splice-out and splice-in validated live against CLN; v2 dual-funded opens implemented both as initiator and acceptor. CLN-initiated splices, repeat splices and multi-UTXO splice-ins are untested. |
|
|
661
|
+
| Mobile background | Limited | Works on React Native but has no background sync or push-notification support. |
|
|
662
|
+
|
|
663
|
+
Recommended safeguards in production:
|
|
664
|
+
|
|
665
|
+
- Cap exposure with `maxPaymentSats` and `dailySpendLimitSats`.
|
|
666
|
+
- Call `validatePayment()` before every send.
|
|
667
|
+
- Set `backupPath` for automated database backups, and keep an SCB (`beignet backup scb`).
|
|
668
|
+
- Pass multiple `electrumServers` for connection redundancy.
|
|
669
|
+
- Configure watchtowers so breaches are punished while you are offline.
|
|
670
|
+
- Monitor `node:error` events and the `/health` endpoint.
|
|
671
|
+
- Start with small channels and increase gradually.
|
|
837
672
|
|
|
838
673
|
## React Native
|
|
839
674
|
|
|
840
|
-
|
|
675
|
+
`react-native-tcp-socket` is a drop-in replacement for `net` and `tls`:
|
|
841
676
|
|
|
842
677
|
```json
|
|
843
678
|
{
|
|
@@ -850,12 +685,18 @@ You can use `react-native-tcp-socket` as a drop-in replacement for `net` & `tls`
|
|
|
850
685
|
|
|
851
686
|
## Documentation
|
|
852
687
|
|
|
853
|
-
|
|
854
|
-
|
|
688
|
+
| Document | Contents |
|
|
689
|
+
|----------|----------|
|
|
690
|
+
| [example/REPL_TESTING.md](example/REPL_TESTING.md) | Copy-pasteable REPL walkthrough of the full node lifecycle |
|
|
691
|
+
| [docs/AI_AGENT_GUIDE.md](docs/AI_AGENT_GUIDE.md) | Deployment, monitoring, safety rails, HTTP daemon patterns |
|
|
692
|
+
| [src/lightning/README.md](src/lightning/README.md) | Protocol-layer reference and usage guide |
|
|
693
|
+
| [docs/ROADMAP.md](docs/ROADMAP.md) | Feature roadmap and progress |
|
|
694
|
+
| [docs/RECOVERY-PROTOCOL.md](docs/RECOVERY-PROTOCOL.md) | Proposed replicated state-continuity design |
|
|
695
|
+
| [API reference](docs/markdown/classes/Wallet.md) | Generated typedoc ([HTML](docs/html/classes/Wallet.html)) |
|
|
855
696
|
|
|
856
697
|
## Support
|
|
857
698
|
|
|
858
|
-
|
|
699
|
+
Open an issue, or reach out on [Telegram](https://t.me/bitkitchat).
|
|
859
700
|
|
|
860
701
|
## License
|
|
861
702
|
|