Files
caravel/backend/spec/bitcoin.md
T
Jon Staab c0aff5f7cf
Docker / build-and-push-image (backend, backend, coracle/caravel-backend) (push) Failing after 5m48s
Docker / build-and-push-image (frontend, frontend, coracle/caravel-frontend) (push) Successful in 2m53s
Refactor billing module
2026-05-12 16:32:05 -07:00

63 lines
3.8 KiB
Markdown

# `bitcoin` — Bitcoin / Lightning helpers
Small wrappers around the Bitcoin-facing services the app talks to: Nostr Wallet Connect (NWC) wallets for Lightning invoices/payments, and a fiat↔BTC spot price feed, plus the fiat-minor-units → millisatoshi conversion that ties them together. The billing-specific orchestration (which wallet pays which invoice, double-charge guards, DMs, etc.) lives in `spec/billing.md`.
## `pub struct Bitcoin`
Owns the app's Bitcoin-facing configuration: the system NWC wallet URL (the wallet used to *receive* payments — issue and look up bolt11 invoices) and the HTTP client used for the fiat↔BTC spot price feed.
Members:
- `system_nwc_url: String` - from `NWC_URL`
- `http: reqwest::Client`
### `pub fn from_env() -> Self`
Reads `NWC_URL` (the system / receiving wallet) and constructs a fresh `reqwest::Client`. Unlike the Stripe keys, `NWC_URL` is **optional**: if it's unset, Lightning operations fail at use time with a clear error rather than panicking at startup. This is what `Billing::new` calls.
### `pub fn system_wallet(&self) -> Result<Wallet>`
Returns the system `Wallet` (parsed from `system_nwc_url` with label `"system"`). Errors if `NWC_URL` is unset or malformed.
### `pub async fn fiat_minor_to_msats(&self, amount_due_minor: i64, currency: &str) -> Result<u64>`
Fetches the live BTC spot price (via `btc_spot_price_from_base` against Coinbase) for `currency` and converts `amount_due_minor` to millisatoshis via `fiat_minor_to_msats_from_quote`. `currency` is upper-cased before use.
## `pub struct Wallet`
A handle to a single NWC wallet. Each operation opens a fresh NWC connection and tears it down (`shutdown`) afterwards.
Member:
- `uri: NostrWalletConnectURI` - the parsed `nostr+walletconnect://…` URI
### `pub fn parse(uri: &str, label: &str) -> Result<Self>`
Parses an `nostr+walletconnect://` URI. `label` (e.g. `"system"` / `"tenant"`) only flavours the error message — `invalid {label} NWC URL` — so callers can tell which wallet was misconfigured.
### `pub async fn make_invoice(&self, amount_msats: u64, description: &str) -> Result<String>`
Issues a bolt11 invoice for `amount_msats` with the given description. Returns the bolt11 string.
### `pub async fn pay_invoice(&self, bolt11: String) -> Result<()>`
Pays a bolt11 invoice.
### `pub async fn invoice_settled(&self, bolt11: &str) -> Result<bool>`
Looks up a bolt11 invoice (previously issued by this wallet) and returns whether it has settled — true if the transaction state is `Settled` or `settled_at` is present.
## `pub async fn btc_spot_price_from_base(http: &reqwest::Client, api_base: &str, currency: &str) -> Result<f64>`
Fetches the BTC spot price denominated in `currency` (an ISO-4217 code) from a Coinbase-shaped API at `api_base` (`{api_base}/BTC-{currency}/spot`); production callers reach it via `Bitcoin::fiat_minor_to_msats` with the real Coinbase base, while tests can point it at a stub. Errors if the quote is missing, unparseable, or non-positive.
## `pub fn fiat_minor_to_msats_from_quote(amount_due_minor: i64, currency: &str, btc_price_in_fiat: f64) -> Result<u64>`
Converts a fiat amount expressed in minor units (cents, etc.) to millisatoshis, given a BTC price quote in that currency.
- Errors if `amount_due_minor <= 0` or `btc_price_in_fiat <= 0`
- Converts minor units to a major-unit amount using the currency's decimal exponent (most currencies 2; `JPY`/`KRW`/… 0; `BHD`/`KWD`/… 3 — following Stripe's currency conventions; an unrecognized or malformed code is an error)
- `msats = (amount_fiat / btc_price_in_fiat) * 100_000_000_000`
- Rounds **up** so we never under-charge, but snaps to the nearest integer when within `1e-6` of one to avoid floating-point artifacts at integer boundaries
- Errors if the result is non-finite, non-positive, or exceeds `u64::MAX`