mostro 0.17.5

Lightning Network peer-to-peer nostr platform
# Lightning Operations

Core interactions with LND via `fedimint_tonic_lnd`.

## Connector
- Source: `src/lightning/mod.rs`
- Type: `LndConnector { client: Client }`
- Construct: `LndConnector::new()` using host, cert, macaroon from settings.

## Hold Invoices
- Create: `create_hold_invoice(description, amount)``(AddHoldInvoiceResp, preimage, hash)`.
- Subscribe: `subscribe_invoice(r_hash, sender)` streams `InvoiceState` updates.
- Settle: `settle_hold_invoice(preimage)`.
- Cancel: `cancel_hold_invoice(hash)`.

## Outgoing Payments
- `send_payment(invoice, amount, sender)`
  - Validates invoice amount or supplies `amt`.
  - Caps max fee: 1% for amounts ≤1000 sats; `Settings::get_mostro().max_routing_fee` for larger amounts.
  - Streams router updates to caller.

## Node Status
- `get_node_info()`; mapped to `LnStatus` and stored in `config::LN_STATUS`.

## Invoice Validation

Source: `src/lightning/invoice.rs`

The invoice validation module provides comprehensive validation for Lightning invoices, Lightning Addresses, and LNURL-pay requests.

### Functions

#### decode_invoice
```rust
pub fn decode_invoice(payment_request: &str) -> Result<Bolt11Invoice, MostroError>
```
Decodes a BOLT11 invoice string into a structured invoice object.

**Parameters**:
- `payment_request`: BOLT11 invoice string

**Returns**: `Bolt11Invoice` or `MostroError::InvalidInvoice`

**Entry**: `src/lightning/invoice.rs:33`

#### is_valid_invoice
```rust
pub async fn is_valid_invoice(
    amount: i64,
    fee: i64,
    payment_request: &str,
) -> Result<String, MostroError>
```
Comprehensive validation supporting:
- BOLT11 Lightning invoices
- Lightning Addresses (user@domain.com)
- LNURL-pay requests

**Validation checks**:
1. Invoice format and decoding
2. Amount matching (with fee deduction): `expected_amount = amount - fee`
3. Minimum payment amount enforcement (`mostro_settings.min_payment_amount`)
4. Expiration validation (`invoice.is_expired()`)
5. Expiration window compliance: `expires_at > now + invoice_expiration_window`
6. Lightning Address resolution via LNURL
7. LNURL callback with amount parameter

**Entry**: `src/lightning/invoice.rs:181`

**Error cases**:
- `InvalidInvoice`: Decoding fails or format invalid
- `WrongAmountError`: Invoice amount doesn't match expected (after fee deduction)
- `MinAmountError`: Amount below minimum threshold
- `InvoiceExpiredError`: Invoice already expired
- `ExpirationWindowTooShort`: Expires before required window

## Payment Retry System

Source: `src/scheduler.rs:172` (job_retry_failed_payments)

Failed outgoing payments are automatically retried via the scheduler.

### Configuration

**Settings** (`src/config/types.rs:28-46`):
```rust
pub struct LightningSettings {
    // ... other fields ...
    pub payment_attempts: u32,        // Max retry attempts (default: 3)
    pub payment_retries_interval: u32, // Seconds between retries (default: 60)
}
```

### Retry Job

**Function**: `job_retry_failed_payments()`
- Queries database for failed payments with retry attempts remaining
- Respects `payment_attempts` limit
- Scheduled at `payment_retries_interval` frequency
- Automatically invokes `send_payment()` for each retry
- Updates payment status and attempt count in database

### Workflow
1. Payment fails initially → marked as failed in DB
2. Scheduler runs retry job every N seconds (payment_retries_interval)
3. Job finds failed payments with `attempts < payment_attempts`
4. Invokes send_payment() again
5. Increments attempt counter
6. Continues until success or max attempts reached

## Payment Error Handling

**Pre-flight Checks** (lines 183-201):
Before attempting payment, `send_payment()` uses `track_payment_v2` to detect duplicate attempts:

```rust
// Check if payment was previously attempted
match ln_client.router().track_payment_v2(track_req).await {
    Ok(_) => {
        error!("Aborting paying invoice with hash {hash} to buyer");
        return Err(MostroError::TrackError);
    }
    Err(_) => {
        // Payment not found, safe to proceed
    }
}
```

**Amount Validation** (lines 210-220):
```rust
if let Some(amt_msat) = invoice.amount_milli_satoshis() {
    let invoice_amount_sats = amt_msat / 1000;
    if invoice_amount_sats != amount as u64 * 1000 {
        error!("Aborting paying invoice with wrong amount to buyer");
        return Err(MostroError::WrongAmountError);
    }
}
```

**Zero-Amount Invoice Handling** (lines 222-228):
If invoice has no amount, the `amt` field is populated in SendPaymentRequest:
```rust
if invoice.amount_milli_satoshis().is_none() {
    req.amt = amount;
}
```

**Fee Limit Enforcement**:
```rust
let max_fee = match amount.cmp(&1000) {
    // For small amounts, use 1% but ensure minimum of 10 sats
    Ordering::Less | Ordering::Equal => (amount as f64 * 0.01).max(10.0),
    Ordering::Greater => amount as f64 * mostro_settings.max_routing_fee,
};
req.fee_limit_sat = max_fee as i64;
```

**Timeout**: 60 seconds (line 205)

## Node Information

### get_node_info
```rust
pub async fn get_node_info(&mut self) -> Result<GetInfoResponse, MostroError>
```

Retrieves LND node information including:
- Node version
- Public key
- Node alias
- Active chains (e.g., bitcoin mainnet/testnet)
- Network information
- Block height sync status

**Entry**: `src/lightning/mod.rs:260`

**Usage**: Called during startup to populate `config::LN_STATUS` (src/main.rs:86)

## Anti-Abuse Bond Operations

The optional anti-abuse bond (`[anti_abuse_bond]`, off by default) puts a
**second** hold invoice on a trade, owned by the maker and/or taker. It is
released on normal completion and on cancels before a waiting-state
timeout; it is slashed only on an explicit solver `BondResolution`
directive or a waiting-state timeout (when `slash_on_waiting_timeout =
true`). Full design: `docs/ANTI_ABUSE_BOND.md`. This section is the
operator runbook.

### Where the state lives

Every bond is one row in the `bonds` table (`src/app/bond/db.rs`,
`model.rs`). Inspect it directly:

```sql
SELECT id, order_id, role, state, amount_sats,
       parent_bond_id, child_order_id, slashed_share_sats,
       node_share_sats, slashed_reason,
       payout_attempts, invoice_request_attempts, slashed_at
  FROM bonds ORDER BY created_at;
```

`state` (string-backed, `src/app/bond/types.rs`) walks:

```text
requested → locked ─┬→ released                       (happy / cancel before timeout)
                    └→ pending-payout ─┬→ slashed      (counterparty paid their share)
                                       ├→ forfeited    (counterparty never claimed in window)
                                       └→ failed       (send_payment exhausted)
```

- **`pending-payout`** — a slash already fired. The bond HTLC was
  **settled** (claimed into Mostro's wallet) at slash time; the scheduler
  is now driving the counterparty payout. The split is frozen here:
  `node_share_sats` is the node's retained share, `amount_sats -
  node_share_sats` is owed to the winning counterparty.
- **`slashed`** — terminal success; the counterparty share was paid.
- **`forfeited`** — designed-in long-stop: the counterparty never sent a
  payout invoice within `payout_claim_window_days`. The node keeps
  `amount_sats` in full. **No operator action needed.**
- **`failed`**`send_payment` exhausted `payout_max_retries` against a
  delivered invoice. **User-recoverable** while inside the claim window: a
  fresh `Action::AddBondInvoice` from the recipient flips the row back to
  `pending-payout`. Only past the window does it need operator attention
  (see below).
- `slashed_reason` is `lost-dispute` (solver directive) or `timeout`
  (waiting-state timeout). A cancel before the timeout is never a slash.

For range-order maker bonds the parent row stays `locked` while child
rows (`parent_bond_id` set, `child_order_id` = the taken slice) carry the
proportional per-slice slashes; the single settle happens at range close.

### Scheduler jobs

Run from `src/scheduler.rs` (see `run_jobs`):

- `job_process_bond_payouts` — drives every `pending-payout` row: requests
  a payout bolt11 from the winner (`Action::AddBondInvoice`, cadenced by
  `payout_invoice_window_seconds`), runs `send_payment`, retries up to
  `payout_max_retries`, and reconciles against LND on entry so a daemon
  restart never double-pays.
- `job_reconcile_stranded_maker_bonds` — settles and distributes a range
  maker bond at range close (per-slice counterparty shares + maker
  refund); the 5-minute sweep is the backstop if the inline close failed.

### Reading what happened in the logs

Bond transitions log through `tracing` (`bond payout: …` lines in
`src/app/bond/payout.rs`, plus slash/release lines in `flow.rs` /
`slash.rs`). To follow a solver decision, look for the `BondResolution`
on the inbound `admin-settle` / `admin-cancel` message — its wire shape is:

```json
{ "order": { "version": 1, "id": "<order-id>", "action": "admin-cancel",
  "payload": { "bond_resolution": { "slash_seller": true, "slash_buyer": false } } } }
```

`slash_seller` / `slash_buyer` are resolved to a maker- or taker-bond row
by the order kind (sell → maker is seller; buy → maker is buyer). A
`payload: null` (or absent) means **release both bonds** — no slash. A
slash directed at a side with no `locked` bond is rejected with
`CantDo(InvalidPayload)` and the trade resolution does not run.

### Resolving a `failed` bond manually

A `failed` row means the bond was slashed (sats are already in Mostro's
wallet), but Mostro could not route the counterparty's share and the
claim window has since elapsed, so the auto-recovery path no longer
re-arms it. There is no slash to undo and no funds at risk on the
counterparty's side — the value is held by the node. To make the
counterparty whole, pay them out-of-band (the amount owed is
`amount_sats - node_share_sats`) and keep the row as the audit record.
Before the window elapses, prefer the built-in path: have the
counterparty resend their payout invoice, which flips the row back to
`pending-payout` automatically.

### Public exposure

The node advertises its bond policy in the kind-38385 info event
(`src/nip33.rs::info_to_tags`) so clients can warn users before they
trade: `bond_enabled` (always emitted), and when enabled `bond_apply_to`,
`bond_amount_pct`, `bond_base_amount_sats`, `bond_slash_on_waiting_timeout`,
`bond_slash_node_share_pct`, and `bond_payout_claim_window_days`.

## Diagrams
```mermaid
flowchart TD
  A[Create Hold Invoice] --> B[Subscribe Single Invoice]
  B -->|Settled| C[Settle Hold]
  B -->|Cancel/Expire| D[Cancel Hold]
  E[Send Payment] --> F[Track/Stream Updates]
```