feat: pay bolt11 Lightning invoices via /api/v1/send#66
Conversation
Allow the API send endpoint to pay an external bolt11 invoice when the `to` field is a `lnbc...` payment request (optionally `lightning:`-prefixed). - Decode the invoice and take the amount from it; reject amountless invoices - Enforce the same min/max and admin-approval thresholds as internal sends - Idempotency lock + dedup keyed on the invoice payment hash - Balance check reserves ~2% for routing fees; response reports the real fee - Large invoices reuse the existing Telegram admin-approval flow - Response adds `payment_hash` and `fee` (sats) for invoice payments Docs: add provider-facing docs/invoice-payments-api.md and update referral-api.md / README for invoice support.
📝 WalkthroughWalkthroughBolt11 invoices can now be submitted to the Send API, validated and paid externally through LNBits, or routed through Telegram admin approval. Pending transactions and responses persist invoice-specific data, while documentation covers authentication, endpoints, constraints, and integration flows. ChangesBolt11 invoice payments
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Provider
participant SendAPI
participant InvoicePayment
participant LNBits
participant TelegramApproval
Provider->>SendAPI: submit Bolt11 invoice
SendAPI->>InvoicePayment: decode and validate invoice
InvoicePayment->>LNBits: pay immediately or create approval
LNBits-->>InvoicePayment: payment hash and routing fee
InvoicePayment->>TelegramApproval: process approval when required
TelegramApproval-->>Provider: return or publish payment status
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
docs/invoice-payments-api.md (1)
30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSpecify a language for fenced code blocks. Fenced code blocks without a language specifier trigger markdown lint warnings. Add
textto resolve them.
docs/invoice-payments-api.md#L30-L32: changeto```text.docs/invoice-payments-api.md#L41-L43: changeto```text.docs/invoice-payments-api.md#L310-L328: changeto```text.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/invoice-payments-api.md` around lines 30 - 32, Specify the text language for all three fenced code blocks in docs/invoice-payments-api.md: update the fences at lines 30-32, 41-43, and 310-328 from untyped fences to text fences, preserving their contents.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/api/send.go`:
- Around line 538-543: Update the payment failure handling in the Invoice
payment flow to keep logging the detailed err internally, but pass a generic
payment-failed message to RespondError instead of formatting err into the API
response. Preserve the existing ErrorLogger.LogPaymentError call and response
status behavior.
- Around line 559-563: Update the sender confirmation message construction near
senderConfirmationMsg so the provided API memo is included in the Telegram
notification, alongside the existing invoice description when present. Reuse the
existing formatting and escaping conventions, while preserving the current
amount and send behavior.
- Around line 487-488: In the payment flow around GetUserAvailableBalance and
the subsequent invoice payment, acquire an exclusive per-user lock using the
existing MemoCache before checking balance, and hold it through the payment
execution. Ensure the lock is released on every success and error path so
concurrent requests for the same user are serialized while different users
remain independent.
In `@internal/telegram/api_approval.go`:
- Around line 245-292: Update CreateAPIInvoiceApprovalRequest to construct a
fresh local ReplyMarkup for each approval request instead of mutating the shared
apiApprovalConfirmationMenu. Build the approve and cancel buttons and their row
on this per-call markup, then pass it to bot.Telegram.Send so concurrent
requests retain their own callback data.
---
Nitpick comments:
In `@docs/invoice-payments-api.md`:
- Around line 30-32: Specify the text language for all three fenced code blocks
in docs/invoice-payments-api.md: update the fences at lines 30-32, 41-43, and
310-328 from untyped fences to text fences, preserving their contents.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a679854e-370f-4d24-bcb9-55bda467d90b
📒 Files selected for processing (6)
README.mddocs/invoice-payments-api.mddocs/referral-api.mdinternal/api/pending_transaction.gointernal/api/send.gointernal/telegram/api_approval.go
| // Check available balance with a ~2% routing fee reserve | ||
| balance, err := s.Bot.GetUserAvailableBalance(fromUser) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Prevent concurrent payments from bypassing pot reserves.
Currently, concurrent API requests for the same user can all pass the availableBalance check because the LNBits Wallet.Pay backend does not enforce local "pot" limits. This TOCTOU (Time-of-Check to Time-of-Use) race condition allows a user to overdraw their available balance and inadvertently spend funds that were locked in pots.
Consider acquiring a temporary user-level lock (e.g., leveraging the existing MemoCache) before checking the balance and executing the invoice payment.
🔒️ Proposed fix to prevent concurrent balance bypass
// Check available balance with a ~2% routing fee reserve
+
+ // Prevent concurrent balance-draining API calls for the same user
+ userLockKey := fmt.Sprintf("api_send_user_%s", fromUsername)
+ if success := s.MemoCache.SetNX(userLockKey, "locked"); !success {
+ RespondError(w, "Another payment is currently processing. Please wait.")
+ return
+ }
+ defer s.MemoCache.Delete(userLockKey)
+
balance, err := s.Bot.GetUserAvailableBalance(fromUser)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/api/send.go` around lines 487 - 488, In the payment flow around
GetUserAvailableBalance and the subsequent invoice payment, acquire an exclusive
per-user lock using the existing MemoCache before checking balance, and hold it
through the payment execution. Ensure the lock is released on every success and
error path so concurrent requests for the same user are serialized while
different users remain independent.
| senderConfirmationMsg := fmt.Sprintf("✅ Invoice paid successfully!\n\n💸 Amount: %s", thirdparty.FormatSatsWithLKR(amount)) | ||
| if bolt11.Description != "" { | ||
| senderConfirmationMsg += fmt.Sprintf("\n✉️ %s", str.MarkdownEscape(bolt11.Description)) | ||
| } | ||
| if _, err := s.Bot.Telegram.Send(fromUser.Telegram, senderConfirmationMsg); err != nil { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Include the memo in the sender's Telegram confirmation.
The API documentation states that the memo is used for "sender confirmation / logs". However, the Telegram confirmation message currently only includes the invoice description, omitting the provided API memo.
📝 Proposed fix to include the memo
// Send confirmation to sender
senderConfirmationMsg := fmt.Sprintf("✅ Invoice paid successfully!\n\n💸 Amount: %s", thirdparty.FormatSatsWithLKR(amount))
if bolt11.Description != "" {
- senderConfirmationMsg += fmt.Sprintf("\n✉️ %s", str.MarkdownEscape(bolt11.Description))
+ senderConfirmationMsg += fmt.Sprintf("\n📄 Invoice: %s", str.MarkdownEscape(bolt11.Description))
}
+ if memo != "" {
+ senderConfirmationMsg += fmt.Sprintf("\n✉️ Memo: %s", str.MarkdownEscape(memo))
+ }
if _, err := s.Bot.Telegram.Send(fromUser.Telegram, senderConfirmationMsg); err != nil {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| senderConfirmationMsg := fmt.Sprintf("✅ Invoice paid successfully!\n\n💸 Amount: %s", thirdparty.FormatSatsWithLKR(amount)) | |
| if bolt11.Description != "" { | |
| senderConfirmationMsg += fmt.Sprintf("\n✉️ %s", str.MarkdownEscape(bolt11.Description)) | |
| } | |
| if _, err := s.Bot.Telegram.Send(fromUser.Telegram, senderConfirmationMsg); err != nil { | |
| senderConfirmationMsg := fmt.Sprintf("✅ Invoice paid successfully!\n\n💸 Amount: %s", thirdparty.FormatSatsWithLKR(amount)) | |
| if bolt11.Description != "" { | |
| senderConfirmationMsg += fmt.Sprintf("\n📄 Invoice: %s", str.MarkdownEscape(bolt11.Description)) | |
| } | |
| if memo != "" { | |
| senderConfirmationMsg += fmt.Sprintf("\n✉️ Memo: %s", str.MarkdownEscape(memo)) | |
| } | |
| if _, err := s.Bot.Telegram.Send(fromUser.Telegram, senderConfirmationMsg); err != nil { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/api/send.go` around lines 559 - 563, Update the sender confirmation
message construction near senderConfirmationMsg so the provided API memo is
included in the Telegram notification, alongside the existing invoice
description when present. Reuse the existing formatting and escaping
conventions, while preserving the current amount and send behavior.
There was a problem hiding this comment.
I think this is a good suggestion. Add memo?
There was a problem hiding this comment.
@cmnisal, agreed. The sender confirmation should show both the invoice description (when present) and the caller-provided API memo.
- Update
internal/api/send.goinService.sendToInvoice. - Keep the existing amount and invoice-description behavior.
- Append a separately labeled, Markdown-escaped
Memo:line whenmemois non-empty.
🐇✨
Failed to handle agent chat message. Please try again.
Summary
Adds external bolt11 Lightning invoice payments to the existing
/api/v1/sendendpoint. When thetofield is alnbc...payment request (optionallylightning:-prefixed), the bot pays it externally via lnbits instead of doing an internal transfer.What changed
Send()—tomatching a bolt11 invoice routes to the newsendToInvoicehandler.SetNXlock + dedup keyed on the invoice payment hash prevents double-payment from concurrent/retried requests./api/v1/send/statusreflects the outcome.payment_hashandfee(sats) for invoice payments.API contract
POST /api/v1/send{ "from": "BiccoindeepaDSA", "to": "lnbc10u1p...", "memo": "optional" }200→{ "success": true, "payment_hash": "...", "fee": 3, "amount": 1000, ... }202→{ "success": false, "message": "... Transaction ID: ..." }Docs
docs/invoice-payments-api.mddocs/referral-api.mdandREADME.mdfor invoice support.Testing
go build ./...— cleango vet ./internal/api/...— cleanSummary by CodeRabbit