Skip to content

feat: pay bolt11 Lightning invoices via /api/v1/send - #66

Open
xbuddhi wants to merge 2 commits into
mainfrom
feat/api-invoice-payments
Open

feat: pay bolt11 Lightning invoices via /api/v1/send#66
xbuddhi wants to merge 2 commits into
mainfrom
feat/api-invoice-payments

Conversation

@xbuddhi

@xbuddhi xbuddhi commented Jul 20, 2026

Copy link
Copy Markdown

Summary

Adds external bolt11 Lightning invoice payments to the existing /api/v1/send endpoint. When the to field is a lnbc... payment request (optionally lightning:-prefixed), the bot pays it externally via lnbits instead of doing an internal transfer.

What changed

  • Invoice detection in Send()to matching a bolt11 invoice routes to the new sendToInvoice handler.
  • Amount from the invoice — amountless invoices are rejected (they must encode an amount).
  • Same guardrails as internal sends — min/max limits and per-wallet admin-approval threshold are enforced.
  • Idempotency — a SetNX lock + dedup keyed on the invoice payment hash prevents double-payment from concurrent/retried requests.
  • Fee handling — balance check reserves ~2% for routing fees; the response reports the real fee (read back from the settled payment, converted msat→sat).
  • Large invoices reuse the existing Telegram admin-approval flow — a pending transaction is created and an approve/pay button is sent; on approval the invoice is paid and /api/v1/send/status reflects the outcome.
  • Response fields — adds payment_hash and fee (sats) for invoice payments.

API contract

POST /api/v1/send

{ "from": "BiccoindeepaDSA", "to": "lnbc10u1p...", "memo": "optional" }
  • Success 200{ "success": true, "payment_hash": "...", "fee": 3, "amount": 1000, ... }
  • Requires approval 202{ "success": false, "message": "... Transaction ID: ..." }

Docs

  • New provider-facing guide: docs/invoice-payments-api.md
  • Updated docs/referral-api.md and README.md for invoice support.

Testing

  • go build ./... — clean
  • go vet ./internal/api/... — clean

Note: this is a separate feature from the /api/v1/usertransactions work on feat/api-user-transactions; that endpoint is not part of this PR.

Summary by CodeRabbit

  • New Features
    • Added support for sending payments to bolt11 (BOLT11) Lightning invoices via the Send API (including optional lightning: prefix), with invoice-only amount handling.
    • Invoice payments now execute immediately or require admin approval when configured, with approval de-duplication per invoice payment_hash.
    • Successful invoice payments return payment_hash and routing fee (fee), and confirmations include the invoice description and optional memo.
  • Documentation
    • Updated the README and added/expanded provider integration docs (authentication, endpoints, request/response shapes, statuses, errors, limits, and examples).

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.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Bolt11 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.

Changes

Bolt11 invoice payments

Layer / File(s) Summary
Invoice payment contracts and integration guide
README.md, docs/invoice-payments-api.md, docs/referral-api.md
Documents Bolt11 recipients, HMAC authentication, endpoint contracts, validation rules, approval polling, errors, and invoice payment responses.
Pending invoice transaction state
internal/api/pending_transaction.go
Adds invoice transaction fields, payment-hash approval locks, and loading behavior that avoids treating invoices as Telegram recipients.
Invoice detection and immediate payment
internal/api/send.go
Detects, decodes, validates, deduplicates, serializes, and pays Bolt11 invoices while returning the payment hash and routing fee.
Approval-based invoice execution
internal/telegram/api_approval.go
Adds invoice approval persistence, approval routing, available-balance checks, LNBits execution, pending-status updates, and Telegram result messages.

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
Loading

Possibly related PRs

  • CeyLabs/BitcoinDeepaBot#19: Introduces the Telegram API approval structures and handlers extended with invoice-specific execution.

Suggested labels: Review effort 4/5, Possible security concern

Suggested reviewers: cmnisal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding bolt11 Lightning invoice payments to /api/v1/send.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-invoice-payments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@xbuddhi xbuddhi assigned cmnisal and unassigned cmnisal Jul 20, 2026
@xbuddhi
xbuddhi requested a review from cmnisal July 20, 2026 13:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
docs/invoice-payments-api.md (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a language for fenced code blocks. Fenced code blocks without a language specifier trigger markdown lint warnings. Add text to resolve them.

  • docs/invoice-payments-api.md#L30-L32: change to ```text.
  • docs/invoice-payments-api.md#L41-L43: change to ```text.
  • docs/invoice-payments-api.md#L310-L328: change to ```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

📥 Commits

Reviewing files that changed from the base of the PR and between f26fb22 and 9cdc43c.

📒 Files selected for processing (6)
  • README.md
  • docs/invoice-payments-api.md
  • docs/referral-api.md
  • internal/api/pending_transaction.go
  • internal/api/send.go
  • internal/telegram/api_approval.go

Comment thread internal/api/send.go
Comment thread internal/api/send.go
Comment thread internal/api/send.go
Comment thread internal/telegram/api_approval.go
@xbuddhi
xbuddhi requested a review from helloscoopa July 28, 2026 12:38

@helloscoopa helloscoopa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Check these out!

Comment thread internal/telegram/api_approval.go Outdated
fromUserStr := GetUserStr(from.Telegram)

// Re-check balance (with fee reserve) before paying
balance, err := bot.GetUserBalance(from)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

executeApprovedInvoicePayment re-checks balance with GetUserBalance (raw wallet balance) instead of GetUserAvailableBalance (wallet minus pot reservations) used at submission time in send.go.

Comment thread internal/api/send.go
RespondError(w, "This invoice is already being processed")
return
}
defer s.MemoCache.Delete(lockKey)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The invoice idempotency lock (SetNX on payment hash) is released via defer as soon as sendToInvoice returns, including on the 202 pending-approval path, before the payment is actually executed.

Comment thread internal/api/send.go
RespondError(w, fmt.Sprintf("Insufficient balance: %s available, %s required", thirdparty.FormatSatsWithLKR(balance), thirdparty.FormatSatsWithLKR(amount)))
return
}
if float64(amount) > float64(balance)*0.98 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Money-critical fee-reserve comparison uses float64 arithmetic on satoshi integer amounts instead of integer math, contrary to the project's money-math convention.

Balance and approval safety:
- Re-check the *available* balance (wallet minus pot reservations) when an
  API transaction is approved, for both the internal-transfer and bolt11
  paths. Pot balances are DB-side reservations that never leave the lnbits
  wallet, so the raw balance let an approved payment spend pot-reserved sats
  that the submission-time check had excluded.
- Hold a per-sender lock across the balance-check-then-pay window in
  sendToInvoice, so concurrent requests cannot each pass the available
  balance check and together overdraw.

Invoice approval de-duplication:
- Persist the bolt11 payment hash on PendingTransaction.
- Add InvoiceApprovalLock, a Bunt record tying a payment hash to its
  outstanding approval, and reject a second request for an invoice that
  already has one waiting. The existing MemoCache lock only spans 5 minutes
  while an approval stays actionable for 24h, so it could not cover the
  approval window on its own.

Other:
- Return a generic error when an invoice payment fails; the raw error can
  carry the internal lnbits host/port. Details stay in the logs.
- Include the caller-supplied memo in the sender's Telegram confirmation,
  labelled separately from the invoice description.
- Build the API approval keyboard per request instead of mutating a shared
  ReplyMarkup. These requests originate from concurrent HTTP handlers, which
  the Telegram lockInterceptor does not serialise, so concurrent approvals
  could overwrite each other's callback data.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/telegram/api_approval.go (1)

220-235: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize approved invoice payments per sender.

The mutex only serializes one approval ID. Two approvals for the same sender can concurrently pass the available-balance check and call Wallet.Pay, spending funds reserved in pots. Acquire the same shared sender-payment lock used by Service.sendToInvoice before the balance check and hold it through payment completion.

🤖 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/telegram/api_approval.go` around lines 220 - 235, Update
approveAPITransactionHandler to acquire the shared sender-payment lock used by
Service.sendToInvoice before GetUserAvailableBalance, and retain it until
Wallet.Pay completes. Ensure all error and success paths release the lock, while
preserving the existing balance validation and payment behavior.
🤖 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/pending_transaction.go`:
- Around line 148-153: The invoice approval lock created in
internal/api/pending_transaction.go at lines 148-153 must use storage with a
lifetime matching PendingTransactionExpiry instead of the default five-minute
Bunt TTL; update the lock persistence around InvoiceApprovalLock and Set. Apply
the same approval-lifetime persistence change to callback/approval state in
internal/telegram/api_approval.go at lines 292-297, ensuring both records remain
available for the full approval window.
- Around line 123-125: In internal/api/pending_transaction.go:123-125,
distinguish Bunt’s missing-key result from other read errors: treat only an
absent record as no outstanding approval and propagate unexpected storage
failures. In internal/api/send.go:537-539, make approval creation fail when
recording the invoice lock returns an error, and clean up the pending state
before returning.

In `@internal/api/send.go`:
- Around line 540-542: Update the approval-delivery error path in the send
handler around CreateAPIInvoiceApprovalRequest: when delivery fails, roll back
or inactivate the pending transaction and invoice lock, then return an
appropriate error response instead of continuing to the HTTP 202 accepted
response. Preserve the existing success path only when approval delivery
completes successfully.

---

Outside diff comments:
In `@internal/telegram/api_approval.go`:
- Around line 220-235: Update approveAPITransactionHandler to acquire the shared
sender-payment lock used by Service.sendToInvoice before
GetUserAvailableBalance, and retain it until Wallet.Pay completes. Ensure all
error and success paths release the lock, while preserving the existing balance
validation and payment behavior.
🪄 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 Plus

Run ID: 029f4233-196a-4bac-a79c-4969d9438a29

📥 Commits

Reviewing files that changed from the base of the PR and between 9cdc43c and 59d15d3.

📒 Files selected for processing (3)
  • internal/api/pending_transaction.go
  • internal/api/send.go
  • internal/telegram/api_approval.go

Comment on lines +123 to +125
if err != nil {
// No record for this hash: nothing outstanding.
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail closed when durable approval deduplication is unavailable.

A Bunt read error is treated as “no approval exists,” while a write error is merely logged. Once the temporary request lock ends, the same invoice can receive another approval request.

  • internal/api/pending_transaction.go#L123-L125: distinguish an absent key from storage failures and propagate unexpected errors.
  • internal/api/send.go#L537-L539: abort approval creation and clean up the pending state when recording the invoice lock fails.
📍 Affects 2 files
  • internal/api/pending_transaction.go#L123-L125 (this comment)
  • internal/api/send.go#L537-L539
🤖 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/pending_transaction.go` around lines 123 - 125, In
internal/api/pending_transaction.go:123-125, distinguish Bunt’s missing-key
result from other read errors: treat only an absent record as no outstanding
approval and propagate unexpected storage failures. In
internal/api/send.go:537-539, make approval creation fail when recording the
invoice lock returns an error, and clean up the pending state before returning.

Comment on lines +148 to +153
lock := &InvoiceApprovalLock{
Base: storage.New(storage.ID(invoiceApprovalLockKey(paymentHash))),
PaymentHash: paymentHash,
PendingTxID: pendingTxID,
}
return lock.Set(lock, bot.Bunt)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

The claimed 24-hour approval window uses five-minute cache records.

Both the invoice lock and the callback data are written through storage.New(...)/bot.Bunt. After the cache TTL, the approval either cannot be actioned or no longer blocks a duplicate invoice submission; PendingTransaction.ExpiryTime does not extend that storage lifetime.

  • internal/api/pending_transaction.go#L148-L153: store the invoice approval lock with a lifetime matching PendingTransactionExpiry.
  • internal/telegram/api_approval.go#L292-L297: persist callback/approval state for the same approval lifetime.

Based on learnings, the Bunt-backed transaction/confirmation cache enforces a 5-minute TTL.

📍 Affects 2 files
  • internal/api/pending_transaction.go#L148-L153 (this comment)
  • internal/telegram/api_approval.go#L292-L297
🤖 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/pending_transaction.go` around lines 148 - 153, The invoice
approval lock created in internal/api/pending_transaction.go at lines 148-153
must use storage with a lifetime matching PendingTransactionExpiry instead of
the default five-minute Bunt TTL; update the lock persistence around
InvoiceApprovalLock and Set. Apply the same approval-lifetime persistence change
to callback/approval state in internal/telegram/api_approval.go at lines
292-297, ensuring both records remain available for the full approval window.

Source: Learnings

Comment thread internal/api/send.go
Comment on lines +540 to +542
if err := telegram.CreateAPIInvoiceApprovalRequest(s.Bot, fromUser, paymentRequest, amount, memo, pendingTx.ID, clientIP); err != nil {
log.Warnf("[api/send] Failed to send invoice approval request: %v", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not return accepted when approval delivery fails.

This logs the error but still returns HTTP 202 claiming an approval was sent. The pending transaction and invoice lock remain, so the user cannot approve or resubmit the payment. Roll back/inactivate the pending approval state and return an error.

🤖 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 540 - 542, Update the approval-delivery
error path in the send handler around CreateAPIInvoiceApprovalRequest: when
delivery fails, roll back or inactivate the pending transaction and invoice
lock, then return an appropriate error response instead of continuing to the
HTTP 202 accepted response. Preserve the existing success path only when
approval delivery completes successfully.

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.

3 participants