Skip to content

Commit 336c8b5

Browse files
biwasxyzclaude
andauthored
feat(lightning): add L402 payment rail via Spark SDK (#474)
* feat(lightning): add L402 payment rail via Spark SDK Adds embedded Lightning wallet support alongside the existing x402-stacks payment rail. Uses @buildonspark/spark-sdk for a self-custodial wallet that requires no API key and can be funded from the user's existing L1 BTC. - Spark-backed LightningProvider with pay/receive/deposit operations - L402 interceptor in src/services/x402.service.ts (auto-pays LN invoices on HTTP 402 with WWW-Authenticate: L402 ...) - Encrypted session storage at ~/.aibtc/lightning/keystore.json - 7 new tools: lightning_create/import/unlock/lock/status, fund_from_btc, pay_invoice, create_invoice - L1 BTC → LN funding reuses the existing transfer_btc signing path Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lightning): address L402 review feedback (PR #474) - Per-URL L402 retry counter (fixes rejection of second L402 endpoint) - Reject L402 invoices above L402_MAX_SATS_PER_INVOICE cap (default 10000) - Reject amountless L402 invoices for safety - Invalidate cached macaroon+preimage when server rejects retry with 402 - Escape regex special chars in extractParam for future-proofing Does NOT change: - testnet→REGTEST mapping (REGTEST is Spark's official test network per docs.spark.money) - lightning_lock inputSchema omission (matches existing wallet_lock pattern) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lightning): defer rail-preference checks until L402 challenge detected The L402 interceptor was calling getWalletManager().isUnlocked() on every 402 response to decide whether to defer to the x402-stacks chain. Tests that mock the wallet manager without isUnlocked (pre-existing x402-only tests) then fail with a TypeError. Reorder so the interceptor checks for a WWW-Authenticate: L402 challenge first. No challenge → immediate pass-through to x402 chain, no wallet manager access. Rail-preference logic only runs when L402 is actually applicable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lightning): swap bolt11 for light-bolt11-decoder to clear Snyk bolt11 pulls in secp256k1 → elliptic <= 6.6.1 (GHSA-848j-6mx2-7j84), which trips Snyk. We only needed invoice amount decoding, not signature verification, so light-bolt11-decoder is a better fit: 59 KB, single dependency (@scure/base, already in tree), no CVEs. light-bolt11-decoder returns the amount in millisats rather than sats; converted before comparing to L402_MAX_SATS_PER_INVOICE. Amountless invoice detection (no amount section / zero value) still rejects with the same user-facing error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lightning): correct claimDeposit + fee unit handling + cache invalidation - claimDeposit: SDK's claimStaticDepositWithMaxFee returns { transferId } — no creditAmountSats. Now calls getClaimStaticDepositQuote first to capture creditAmountSats, then claims, and returns { creditedSats, transferId }. Adds optional outputIndex parameter. Provider interface updated. - payInvoice: Spark's CurrencyAmount has originalUnit ∈ {SATOSHI, MILLISATOSHI, BITCOIN, MILLIBITCOIN, MICROBITCOIN, NANOBITCOIN, ...}. Previously read originalValue blindly as sats — could mis-report fees by 1000× on MILLISATOSHI. Added internal currencyAmountToSats helper with explicit unit handling; throws on fiat/FUTURE_VALUE. - L402 cache: retry-after-pay now invalidates the macaroon entry on 402, mirroring the cache-hit path. Prevents permanent dead entries when the server rejects a freshly minted preimage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lightning): match Spark docs canonical claim flow claimDeposit was using claimStaticDepositWithMaxFee (a convenience wrapper that internally fetches the SSP quote) AND fetching the quote explicitly to capture creditAmountSats — two redundant quote requests per claim, with a tiny race window between them. Switch to the canonical flow shown at docs.spark.money/wallets/deposit-from-l1: fetch quote once (returns creditAmountSats + signature), then call claimStaticDeposit with the quote's signature. Single quote request, SSP-signed creditAmountSats matches what was actually claimed. Drops the maxFeeSats parameter from the LightningProvider interface since the SSP-signed quote IS the agreement. Callers that need a max-fee guard should fetch and inspect the quote before invoking claimDeposit. claimDeposit is currently dead code (no tool wires it), so this is a safe interface change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lightning): address Copilot review feedback (PR #474) - Validate L402_MAX_SATS_PER_INVOICE env var; NaN/non-finite/<=0 falls back to default (was a security bypass — invalid env disabled the cap) - Reject NETWORK=testnet for Lightning ops with a clear error; Spark's REGTEST cannot interoperate with Bitcoin testnet (lightning_fund_from_btc was silently broken on testnet) - Add lightning_claim_deposit tool to complete the L1->LN funding flow - Initialize Spark provider before persisting keystore in lightning-manager so init failures don't leave orphaned encrypted blobs blocking retries - Invalidate L402 macaroon cache on 401/403 in addition to 402; many servers signal stale credentials with non-402 errors - Use absolute URL (baseURL + url) for L402 attempt counter and macaroon cache keys to prevent cross-service collision when one path is hosted on multiple base URLs - parseL402Challenge: support single-quoted parameter values to match the docstring claim (RFC-7235 quoted-string semantics) - New tests/services/l402-interceptor.test.ts covering header parsing, amount-cap, amountless rejection, cache reuse, cache invalidation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bf67e62 commit 336c8b5

11 files changed

Lines changed: 3292 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,40 @@ Tools for Bitcoin L1 blockchain operations via mempool.space API:
214214
- `get_btc_transaction_status` - Get confirmation status and details for a Bitcoin transaction by txid
215215
- `get_btc_address_txs` - Get recent transaction history for a Bitcoin address (last 25 transactions)
216216

217+
### Lightning Network (L402)
218+
219+
Embedded, self-custodial Lightning wallet backed by the [Spark SDK](https://www.npmjs.com/package/@buildonspark/spark-sdk) (`@buildonspark/spark-sdk`). No API key required — auth is derived from the BIP39 identity key. Works alongside the existing x402-stacks rail: when an endpoint returns `HTTP 402 WWW-Authenticate: L402 macaroon="...", invoice="..."`, the interceptor pays the invoice via Spark and retries with `Authorization: L402 <macaroon>:<preimage>`. Macaroons are cached in-memory per `{method}:{url}` so repeat calls don't re-pay.
220+
221+
**Mainnet only for now.** Spark does not have a public Bitcoin testnet environment, and Spark REGTEST cannot interoperate with Bitcoin testnet (`tb1...` addresses), so all Lightning tools throw a clear error when `NETWORK=testnet`. Use `NETWORK=mainnet` (real BTC) or wait for Spark testnet support.
222+
223+
**Rail preference:** if an endpoint advertises both x402-stacks and L402, the x402-stacks rail is preferred when a Stacks wallet is unlocked. Otherwise, the L402 rail is used if the Lightning wallet is unlocked.
224+
225+
**Storage:** encrypted keystore at `~/.aibtc/lightning/keystore.json` (AES-256-GCM with scrypt KDF — same scheme as the Stacks wallet).
226+
227+
**Configuration:**
228+
- `L402_MAX_SATS_PER_INVOICE` (optional, default `10000`): hard cap on the satoshi amount the L402 auto-pay interceptor will pay without prompting. Invalid (NaN, non-finite, ≤ 0) values fall back to the default with a warning logged to stderr.
229+
230+
**Tools:**
231+
- `lightning_create` - Create a new Lightning wallet with a fresh BIP39 mnemonic (shown once). Returns deposit address + mnemonic.
232+
- `lightning_import` - Import a Lightning wallet from an existing BIP39 mnemonic.
233+
- `lightning_unlock` - Unlock the Lightning wallet for the session. Required before paying / receiving / L402 auto-pay.
234+
- `lightning_lock` - Drop the in-memory Spark session.
235+
- `lightning_status` - Report locked/unlocked state, wallet id, balance, deposit address.
236+
- `lightning_fund_from_btc` - Send L1 BTC from the main wallet to the Spark deposit address. Reuses the same signing path as `transfer_btc` (cardinal UTXOs only on mainnet).
237+
- `lightning_claim_deposit` - Claim a confirmed L1 deposit into the Spark Lightning wallet (after `lightning_fund_from_btc` confirms with 3+ blocks). Returns credited sats and Spark transfer id.
238+
- `lightning_pay_invoice` - Manually pay a BOLT-11 invoice.
239+
- `lightning_create_invoice` - Manually create a BOLT-11 invoice for receiving sats.
240+
241+
**Example Usage:**
242+
| Request | Action |
243+
|---------|--------|
244+
| "Set up a Lightning wallet" | `lightning_create` |
245+
| "Unlock Lightning" | `lightning_unlock` |
246+
| "Fund Lightning with 100000 sats from my BTC" | `lightning_fund_from_btc` with amountSats=100000 |
247+
| "Claim my Lightning deposit" | `lightning_claim_deposit` with transactionId of the L1 funding tx |
248+
| "Pay this invoice: lnbc..." | `lightning_pay_invoice` with bolt11 |
249+
| "Create a Lightning invoice for 500 sats" | `lightning_create_invoice` with amountSats=500 |
250+
217251
### Direct Stacks Transactions
218252
- `transfer_stx` - Transfer STX tokens to a recipient (signs and broadcasts)
219253
- `call_contract` - Call a smart contract function (signs and broadcasts)

0 commit comments

Comments
 (0)