Commit 336c8b5
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
- src
- services
- lightning
- tools
- utils
- tests/services
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
217 | 251 | | |
218 | 252 | | |
219 | 253 | | |
| |||
0 commit comments