feat(invoices): pay every accepted invoice in one confirmation - #504
Merged
Conversation
Paying the Accepted queue one invoice at a time — create a payment
request, copy the address, send, wait — is about an hour of clicking at
62 invoices. This prepares all the payment requests server-side, hands
the list to the CoinPay wallet extension, and takes a single approval.
Flow:
BulkPayAccepted panel (received tab)
-> POST /api/invoices/bulk-payment-request mint/reuse requests
-> window.coinpay.payBatch(...) one approval, N txs
-> POST /api/invoices/bulk-payment-record store tx hashes
Recording a payment deliberately does NOT mark it paid. A payer-reported
transaction hash is a claim, not a settlement — it can still be dropped
or replaced, and trusting it here would let anyone flip their own
invoice to paid. Confirmation stays with the CoinPay webhook and
syncGigInvoicePaymentStatus. What the record buys us is the ability to
tell "broadcast, not yet confirmed" from "never sent" — the difference
between waiting and paying twice.
`ensureInvoicePaymentRequest` is extracted from the single-invoice route
so both paths produce identical requests; two implementations would
eventually disagree about what "paid" means. It reuses an unexpired
quote rather than minting a second deposit address for the same debt,
which matters on retry.
Every requested id lands in exactly one of `payments` or `skipped`, each
skip carrying a reason — an invoice missing from both would look paid
when it never was.
Partial success is normal here, since each payment is its own on-chain
transaction: failures are listed by name and retried by id, so a retry
cannot re-pay something that already succeeded.
Timing note: CoinPay quotes hold ~15 minutes and the wallet serializes
per account (~4s/payment on EVM/Solana, ~10s on Bitcoin). 62 USDC
payments land around 4 minutes; large single-chain BTC runs can approach
the window, so prefer USDC/SOL rails for payables.
Tests: 1765 pass (56 new, covering both endpoints, the shared lib, and
the component including the retry-only-failures path).
Requires the CoinPay wallet extension — profullstack/coinpayportal
branch feat/extension-bulk-payments.
Docs: docs/bulk-invoice-payments.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Paying the Accepted queue one invoice at a time — create a payment request, copy the address, send, wait — is about an hour of clicking at 62 invoices. This prepares all the payment requests server-side, hands the list to the CoinPay wallet extension, and takes a single approval.
Flow
Recording a payment does NOT mark it paid
A payer-reported transaction hash is a claim, not a settlement — it can still be dropped or replaced, and trusting it here would let anyone flip their own invoice to paid. Confirmation stays with the CoinPay webhook and
syncGigInvoicePaymentStatus, which watch the deposit address.What the record buys us: distinguishing "broadcast, not yet confirmed" from "never sent" — the difference between waiting and paying twice.
Design notes
ensureInvoicePaymentRequestis extracted from the single-invoice route so both paths produce identical requests; two implementations would eventually disagree about what "paid" means.paymentsorskipped, each skip carrying a reason. An invoice missing from both would look paid when it never was.Timing
CoinPay quotes hold ~15 minutes, and the wallet serializes per account (~4s/payment on EVM/Solana, ~10s on Bitcoin). 62 USDC payments land around 4 minutes — comfortably inside the window. Large single-chain Bitcoin runs can approach it, so prefer USDC/SOL rails for payables or split them. The UI tells the user to keep the approval window open, since closing it cancels whatever hasn't gone out.
Tests — 1765 pass (56 new)
Covering both endpoints (ownership, eligibility, skip accounting, provider failure isolation, reuse vs re-quote), the shared lib, and the component — including that the record call never sets
paid, that a webhook-confirmed invoice is not downgraded, and that "retry the failed ones" re-prepares only the failed ids.Full pre-commit suite (lint + type-check + tests + build) passed.
Requires the wallet extension
profullstack/coinpayportal#189 (
feat/extension-bulk-payments). Without it installed, the panel shows an install link instead of the button, and every existing per-invoice flow is untouched.I could not exercise the on-chain path in this environment, so it is covered by unit/component tests rather than a real transaction — worth a small run (2–3 invoices) before turning it loose on all 62.
Docs:
docs/bulk-invoice-payments.md🤖 Generated with Claude Code