Skip to content

On-chain bitcoin payments#180

Merged
v0l merged 8 commits into
masterfrom
onchain-payments
Jul 20, 2026
Merged

On-chain bitcoin payments#180
v0l merged 8 commits into
masterfrom
onchain-payments

Conversation

@v0l

@v0l v0l commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #109

Adds on-chain bitcoin payments using the payments-rs OnChainProvider (v0.4.1 from crates.io), backed by the same LND wallet as the Lightning node.

Changes

DB (lnvps_db)

  • PaymentMethod::OnChain and ProviderConfig::OnChain(LndConfig) (reuses the existing LND config)
  • Migration adds a unique index on subscription_payment.external_id — the txid is stored here and uniqueness backs the stream de-duplication
  • New list_subscription_payments_by_method() for address correlation (external_data is encrypted, so matching happens in memory)

Payment creation

  • Settings::get_onchain() mirrors get_node(); the provider is required and injected into SubscriptionHandler exactly like the Lightning node
  • On-chain payments are BTC-only with a 1h quote expiry; the derived receive address is stored in external_data, external_id stays empty until the deposit is seen

Chain watcher (payments/onchain.rs)

  • Subscribes to provider chain events; de-dupes by txid (at-least-once stream → exactly-once accounting)
  • Deposits are never rejected: partial, late and over-payments are pro-rated at the quoted rate (amount/tax/time_value scaled by received/expected)
  • A deposit to an already-settled address automatically inserts a new pro-rated renewal payment (per issue On-chain payments #109)

API

  • onchain payment method selectable wherever methods are chosen; payment data returns { "onchain": { "address": "bc1…" } }
  • Admin payment-method configs accept the onchain provider type (sanitized like LND)

Fixes

  • MockDb::update_subscription_payment now mirrors all columns the MySQL impl writes (previously dropped external_id, amounts, etc.)

Testing

  • 12 new tests (watcher settle/pro-rate/replay/address-reuse, DB enum round-trips, renewal address derivation, pro-rate math); 474 passing overall
  • Migration verified against a real MySQL instance (applies cleanly, index present)
  • Pre-existing e2e harness failures are unrelated (identical on master)

v0l added 4 commits July 20, 2026 17:03
Support on-chain bitcoin payments (issue #109) using the payments-rs
OnChainProvider backed by the same LND wallet as the Lightning node.

- lnvps_db: PaymentMethod::OnChain, ProviderConfig::OnChain(LndConfig),
  list_subscription_payments_by_method, unique index on
  subscription_payment.external_id (txid de-dupe)
- Settings::get_onchain() mirrors get_node(); provider is required and
  injected into SubscriptionHandler like the Lightning node
- Payment creation derives a fresh receive address (external_data);
  external_id stays empty until the deposit txid is known
- OnChainPaymentHandler watcher: correlates deposits by address,
  de-dupes by txid (at-least-once stream -> exactly-once), pro-rates
  partial/late/over-payments and inserts new pro-rated renewal payments
  for deposits to already-settled addresses
- API: onchain payment method, ApiPaymentData::OnChain { address },
  admin payment method config support
- MockDb::update_subscription_payment now mirrors all MySQL columns
The quote only fixes the price in the subscription's currency, never the
BTC rate. When a deposit is discovered, time credited is pro-rated by the
value received at the current rate:

  time_value * received_msat * rate_now / (expected_msat * rate_quoted)

For BTC-denominated subscriptions this reduces to the plain msat ratio.
The current rate is recorded on the settled payment, so address-reuse
renewals also price against the latest settle. PricingEngine::get_ticker
is now public for the watcher.
…utpoint

Per review:

- Discard the quote when a deposit is discovered (Detected, 0-conf) and
  re-generate time/tax/fee/rate from the received amount through
  PricingEngine::get_cost_by_amount - the same path as LNURL top-ups.
  Confirmed then just settles, so time-to-confirm never matters.
  Subscriptions without a VM (no amount->cost API) fall back to scaling
  the original quote by value at the current rate.
- Key deposits by the standard outpoint {txid}:{vout} instead of txid
  alone: one tx can pay several watched addresses. Requires payments-rs
  0.5.0 which exposes vout on ChainPaymentUpdate.
- PricingEngine::get_ticker made public for the fallback path.
Deposits to addresses of deleted VMs cannot be credited automatically.
Database-wise they are ignored - nothing is settled or recorded; a
SendAdminNotification job is queued (VM id, user, outpoint, amount,
receive address) and the sender is expected to contact support so it is
resolved out of band. An in-memory seen-set stops stream replays from
re-notifying within one process.

Also fixes MockDb::get_vm_by_subscription to match the MySQL impl, which
does not filter deleted VMs.
v0l added 3 commits July 20, 2026 19:34
- New OnChainProviderConfig (url/cert/macaroon + address_type, account,
  min_confirmations with serde defaults) replaces the reused LndConfig in
  ProviderConfig::OnChain; serde tag fixed to 'onchain' to match
  provider_type() (rename_all would have produced 'on_chain')
- PaymentMethodFactory: create_onchain_provider +
  get_onchain_provider_for_company, mapping OnChainAddressType to
  LndAddressType and passing account/min_confirmations through
- Startup data migration seeds an OnChain config from the YAML LND
  section so it is manageable via the admin API
- Admin API: SanitizedOnChainConfig exposes the new fields

Note: runtime still builds the provider from YAML settings (like the
Lightning node); completing the factory->runtime integration is #182.

Cleanup: collapse nested ifs (let-chains), document that the pro_rate
helpers only serve the no-VM fallback pending #181.
Lifecycle test now exercises the full on-chain flow with real nodes:

- 14d happy path: renew?method=onchain derives a real address from LND,
  lnd-payer sends coins, a block is mined and the API's chain watcher
  settles the payment; expiry advances and the settled payment records
  the real txid:vout outpoint
- 14e partial payment: half the quoted sats settles with exactly the
  received msats recorded and ~half an interval credited
- 14f address reuse: a further deposit to the settled address
  auto-creates a new paid renewal payment
- Falls back to admin-complete when the docker stack is unavailable

Fixes a latent bug: the compose-file path in the docker helpers was
relative but cargo runs tests with the crate dir as CWD, so pay_invoice
had been silently falling back to admin-complete and lightning e2e never
actually paid through LND. Both lightning and on-chain now settle via
the real payer node (path resolved from CARGO_MANIFEST_DIR).
Drop the finished onchain-payments work file and all other work files
marked complete (basic-firewall, dns-db-refactor, e2e-integration-tests,
fw-inkernel-src-rate, referral-program-apis,
vat-snapshot-and-vm-payment-retirement); in-progress files remain.
@v0l
v0l merged commit 8d071c5 into master Jul 20, 2026
8 checks passed
@v0l
v0l deleted the onchain-payments branch July 20, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

On-chain payments

1 participant