Skip to content

feat(cli): name the wallets that could pay a BOLT11 invoice - #73

Merged
thebrandonlucas merged 1 commit into
masterfrom
feat/cli-invoice-wallet-shortlist
Oct 21, 2026
Merged

thebrandonlucas merged 1 commit into
masterfrom
feat/cli-invoice-wallet-shortlist

Conversation

@thebrandonlucas

@thebrandonlucas thebrandonlucas commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Stack

One stack, merged bottom up:

  1. This PR: wallets that could pay a BOLT11 invoice (base master)
  2. fix(cli): name each candidate's environment when --env is not given #77: tee8z's review fixes for feat(cli): name the wallets that could pay a BOLT11 invoice #73 (base feat(cli): name the wallets that could pay a BOLT11 invoice #73)
  3. feat(cli): list wallets in every organization with --all-orgs #74: wallets list --all-orgs (base fix(cli): name each candidate's environment when --env is not given #77)
  4. feat(cli): offer a pinned profile after login on a terminal #75: offer a pinned profile after login (base feat(cli): list wallets in every organization with --all-orgs #74)

Problem

Off a terminal (--json, --no-input, a script), payments send --invoice without --wallet only reported the missing flag. An agent then had to list wallets in every scope and guess which one could pay the invoice (#34).

Behavior

Without a picker, a BOLT11 send without --wallet lists the wallets in the current scope once, with the same GET /organizations/{org}/wallets call the picker uses, and sorts them:

Check, in order Excluded as
active is not false inactive
wallet network pays on the invoice's network wrong_network
a balance in --currency, or BTC or USD without it wrong_currency
that balance's funds, meaning signed available plus the wallet's limit, are positive no_balance
a BTC balance's funds cover the invoice's amount (msats) insufficient_balance

Funds count the credit limit. In backend origin/master (c4ca979), available is total - (holds + pending payouts) and excludes the credit line (crates/voltage_eventing/src/wallet/entity.rs:245-284; LimitAllocated only calls update_limit, projection.rs:241). A payment's hold is allowed while available - amount >= -limit (Wallet::has_sufficient_balance, entity.rs:99-123, called from crates/wallets/src/domain/wallet/handlers/wallet_command.rs:257). So a credit wallet with available 0 or negative can still pay, and the filter uses available + limit. BTC amounts are in msats and USD amounts in cents (crates/voltage_util/src/amount.rs:462-466).

A USD candidate pays through a quote, so its amount is not compared with the invoice. It has needs_quote: true and a quote command, voltage quotes create --credit-line LOC --network NET --amount N --unit sats --to usd <scope> --execute (msats when the amount is not whole sats). The hint tells you to create the quote and add --quote QUOTE_ID. When a wallet has both a BTC and a USD balance that qualify, BTC is preferred.

The invoice prefix gives the network: lnbc mainnet, lntb testnet, lntbs signet (which includes the API's mutinynet), and lnbcrt regtest, for which the API has no wallets. The amount comes from the human-readable part.

  • The error stays --wallet is required for payment creation (exit 2). Its hint gives --wallet ID plus the scope flags for each candidate, or explains why each wallet does not qualify.
  • --json puts {invoice, candidates, excluded, basis} in error.detail. Human output omits the details: block, because the hint already shows the same content.
  • The wording says this is a shortlist, not a guarantee. Only the API knows fees and line-of-credit rules.
  • docs/concepts/safety-model.md lists this as the second exception to "a description authenticates nothing", and says that --wallet keeps it offline.
  • No change on a terminal (the picker), with --wallet, or for on-chain sends.
  • Local flag mistakes and an invoice without a BOLT11 prefix fail before any credential is resolved. If no organization is set, the existing error reports it. A credential or listing failure adds "Or pass --wallet ID to skip listing wallets."

Out of scope: wallets list --all-orgs, which is the next PR in this stack, and the JSON negative balance, which was closed by decision on #34.

Refs #34

Checks

  • voltage-pr-review self-review against master, with the Rust style checklist.
  • Every early return and error path is tested: a non-BOLT11 invoice, a local flag error, no organization, an on-chain send, a missing credential, a 500 on the listing, no wallets, no qualifying wallet (each reason, including insufficient_balance), candidates found, --currency filtering, credit-limit funds (available 0 or negative within the limit, and a limit fully drawn), a USD candidate's quote command (whole sats, msats, no amount, no line_of_credit_id), and preferring BTC when both qualify. Unit tests cover the BOLT11 prefixes, the amount multipliers, and malformed invoices.
  • Gate in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked, and python3 scripts/check-coverage.py all pass.
  • CRAP (outside nix), on the feat(cli): offer a pinned profile after login on a terminal #75 tip ff8b488: 0 violations. The highest score is 19.60 for wait_for_payment, unchanged by this PR.

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early review of draft fe8bc0a against master 384111d. This is not an approval, because the PR is still a draft. The design is sound: one listing call, the same can_ask gate as the picker, local checks before any credential is read, and every early return tested. One finding needs a fix (P2, inline), plus three smaller ones.

P2: without --env, the candidate flags omit the environment, so the pasted command fails again. I reproduced this against a mock API. payments send --json --org ORG --invoice lntbs10n1qqq with VOLTAGE_API_KEY set lists every wallet in the organization, and returns "flags": "--wallet WALLET_ID --org ORG". Adding those flags (with --currency btc) exits 2 with Exactly one --env is required for this operation. That error's hint points to voltage environments list, which an API key cannot run. Each listed wallet carries a required environment_id, so when scope.envs is empty, add --env {environment_id} to that candidate's flags. Every integration test passes --env, so add a case without it.

Smaller:

  • The invoice amount is parsed and reported, but it is not used to filter. A BTC balance is in msats, as amount_msats is, so a wallet whose available BTC is less than the invoice amount is still shortlisted, even though it cannot pay. Consider an insufficient_balance reason for BTC balances. A USD balance cannot be compared without a quote, so it can stay as it is. If you keep the current rule, please say in the docs that the amount is not checked.
  • Invoice::parse also returns None for a valid prefix with a malformed amount, such as lnbc1p1qqq. The hint then says that the invoice "does not start with a BOLT11 prefix", which is wrong for that input. Something like "is not a BOLT11 invoice the CLI can read" covers both cases.
  • The no-candidate hint lists every excluded wallet, so a large organization makes a very long hint: line. Consider limiting it to a few wallets plus a count, because --json already has the full excluded list.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (123 unit, 105 integration) and python3 scripts/check-coverage.py passed. CRAP: 578 measured functions, none at or above 20, highest wait_for_payment at 19.60, which this PR does not change. 154 functions were unmeasured. CI is green, except the changelog check, which was pending.

Comment thread src/wallet_shortlist.rs Resolved
Comment thread src/wallet_shortlist.rs Resolved
Comment thread src/wallet_shortlist.rs Resolved
Without a picker, `payments send --invoice` without `--wallet` lists the
wallets in scope once and names those that are active, on the invoice's
network, and hold a usable currency with funds: the available balance
plus the credit limit, as the API's hold check counts them. A BTC
wallet's funds must also cover the invoice's amount. A USD wallet is
marked `needs_quote` and comes with the `quotes create` command for the
invoice's amount. The scope flags are ready to paste. When none
qualifies, the hint says why for each wallet. `--json` reports the
shortlist in `error.detail`.

The list is a shortlist, not a guarantee: fees and line-of-credit rules
are checked only when the payment is sent.

Refs #34
@thebrandonlucas
thebrandonlucas force-pushed the feat/cli-invoice-wallet-shortlist branch from fe8bc0a to ed53d14 Compare October 21, 2026 17:42
@thebrandonlucas
thebrandonlucas marked this pull request as ready for review October 21, 2026 17:52
@thebrandonlucas
thebrandonlucas added this pull request to stack #76 October 21, 2026 17:59

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on ed53d14. The new funds check is right. It mirrors backend Wallet::has_sufficient_balance (available - amount >= -limit, with the wallet's single limit), and I checked that against backend origin/master c4ca979. A BTC balance must now cover amount_msats. A USD candidate gets needs_quote and a quotes create --to usd command, which matches the receive and pay guidance from #61.

P2, still open from my review of fe8bc0a: without --env, the flags omit the environment. I reproduced it again on this head. With VOLTAGE_API_KEY and --org but no --env, payments send --json --invoice lntbs10n1qqq returns "flags": "--wallet WALLET_ID --org ORG". Pasting those flags exits 2 with Exactly one --env is required for this operation, and that error's hint names voltage environments list, which an API key cannot run. The new quote command has the same gap, because a quote is created under /organizations/{org}/environments/{env}/quotes. Both use scope_flags(scope). When scope.envs is empty, append --env {environment_id} from the listed wallet (a required field of Wallet) to both. Please also add an integration case without --env.

Still open, nonblocking: the "does not start with a BOLT11 prefix" text for a malformed amount, and the uncapped list of excluded wallets in the hint.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (125 unit, 105 integration) and python3 scripts/check-coverage.py passed. CRAP: 582 measured functions, none at or above 20, highest wait_for_payment at 19.60; 154 functions were unmeasured. CI is green.

Comment thread src/wallet_shortlist.rs Resolved
Comment thread src/wallet_shortlist.rs Resolved
@thebrandonlucas
thebrandonlucas force-pushed the feat/cli-invoice-wallet-shortlist branch 2 times, most recently from dc6b548 to ed53d14 Compare October 21, 2026 19:31
@thebrandonlucas
thebrandonlucas removed this pull request from stack #76 October 21, 2026 19:32
@thebrandonlucas
thebrandonlucas added this pull request to stack #78 October 21, 2026 19:32
@thebrandonlucas

Copy link
Copy Markdown
Contributor Author

Addressed tee8z's review in #77 (ad2fbcb), stacked directly on this PR. This PR is unchanged at ed53d14, because amending the fixes here would take it to about 1,140 changed lines, over the 1,000-line limit.

  • P2, missing --env: the candidate flags and the quote command now name the wallet's own environment when the scope has none.
  • Malformed amount: the hint now says "not a BOLT11 invoice the CLI can read".
  • Excluded list: the hint names at most five wallets and counts the rest. --json still lists them all.
  • insufficient_balance: already in this PR.

The stack is now #73 → #77 → #74 → #75 (GitHub stack #78).

@thebrandonlucas
thebrandonlucas requested a review from tee8z October 21, 2026 19:33
Comment thread src/wallet_shortlist.rs Resolved
@thebrandonlucas
thebrandonlucas merged commit 2c16984 into master Oct 21, 2026
14 checks passed
@thebrandonlucas
thebrandonlucas deleted the feat/cli-invoice-wallet-shortlist branch October 21, 2026 21:49
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.

2 participants