Skip to content

feat(order): persist buyer payout hash and preimage#875

Merged
grunch merged 1 commit into
mainfrom
feat/persist-payout-hash
Jul 10, 2026
Merged

feat(order): persist buyer payout hash and preimage#875
grunch merged 1 commit into
mainfrom
feat/persist-payout-hash

Conversation

@grunch

@grunch grunch commented Jul 10, 2026

Copy link
Copy Markdown
Member

Closes #869.

Summary

An order records the hold invoice hash/secret (order.hash, order.secret) but nothing about the outgoing payment to the buyer. The payout's payment hash (payment.id) and preimage (payment.secret) are available on every successful payment result, but they were discarded — only order.routing_fee was kept.

This persists them as two new optional fields on the order: payout_hash and payout_preimage.

Why

  • Proof of payment. The preimage is cryptographic proof the buyer was actually paid. Today that proof lives only on the node; if the node history is lost or migrated, the DB cannot prove the payout happened.
  • Robust reconciliation / auditing. Accounting over the bot's data (e.g. the external stats dashboard) currently has to re-derive the outbound hash by parsing order.buyer_invoice — which is also left stale after a retried /setinvoice (see buyer_invoice is left stale after /setinvoice when the payout is retried with a different invoice #864). Storing payout_hash makes that a direct field lookup and removes a class of edge cases. This matters in practice: it is exactly what makes it easy to tell a real payout anomaly apart from a reconciliation artifact when auditing against the node.
  • Traceability. An operator inspecting an order can see exactly which payment settled it.

Changes

  • models/order.ts — add payout_hash?: string | null and payout_preimage?: string | null (plain String, no index).
  • util/completeOrder.ts — set both in completeOrderAsSuccess (the single buyer-payout success choke point, covering the pending-payment retry and healConfirmedOrder paths), inside the atomic $set and the in-memory copy.
  • ln/pay_request.ts — set both in the first-attempt success branch of payToBuyer.
  • tests/bot/pending-payments.spec.ts — assert both are persisted on the success routine.

Scoped to buyer payouts (which have an order). Community earnings withdrawals have no order; their payout hash is already stored on the pending payment.

Note

This records the values going forward. Orders completed before this change will have empty payout_hash/payout_preimage, so consumers should keep deriving the hash from buyer_invoice for the historical backlog. The two are complementary.

Verification

  • tsc -p tsconfig.test.json — passes.
  • eslint + prettier --check on the changed files — clean.
  • Runtime check of the compiled completeOrderAsSuccess confirms payout_hash / payout_preimage / routing_fee are set on success. (The full mocha suite could not run in this environment because the native canvas dependency fails to build without cairo/pango — unrelated to this change.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E7M5Q5My6Sgvvo8qS6zZCE


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Lightning payout hash and preimage details are now saved when an order payment succeeds.
    • Order records retain payout information for completed payments.
  • Bug Fixes

    • Improved consistency between completed order records and their associated Lightning payments.
    • Confirmed payments now preserve payout details during payment recovery.

The order records the hold invoice hash/secret but nothing about the
outgoing payment to the buyer. The payout's payment hash (payment.id) and
preimage (payment.secret) are available on every successful payment result
but were discarded — only routing_fee was kept.

Persist them as order.payout_hash and order.payout_preimage so an order is a
self-contained, verifiable record of the trade:

- Proof of payment: the preimage is cryptographic proof the buyer was paid;
  today it lives only on the node.
- Robust reconciliation: consumers no longer need to re-derive the outbound
  hash by parsing buyer_invoice (which is also stale after a retried
  /setinvoice, see #864) — it becomes a direct field lookup.
- Traceability: operators can see exactly which payment settled an order.

Set at the single buyer-payout success choke point (completeOrderAsSuccess,
covering the retry and heal paths) and in the first-attempt path in
payToBuyer. Scoped to buyer payouts; community withdrawals already store
their hash on the pending payment.

Historical orders keep an empty payout_hash, so consumers should retain the
buyer_invoice-derived fallback for the backlog; the two are complementary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7M5Q5My6Sgvvo8qS6zZCE
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The order model now defines payout hash and preimage fields. Confirmed Lightning payment paths persist these values during direct payment handling and atomic order completion, with healing-flow tests covering the saved fields.

Payout persistence

Layer / File(s) Summary
Payout field contract
models/order.ts
The order interface and schema add optional string fields for payout_hash and payout_preimage.
Confirmed payment writes
ln/pay_request.ts, util/completeOrder.ts, tests/bot/pending-payments.spec.ts
Successful payment handling and order completion store payout identifiers, while pending-payment tests verify persistence during healing.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • lnp2pBot/bot#831: Changes the adjacent payToBuyer failure-handling and retry flow.
  • lnp2pBot/bot#844: Refactors overlapping Lightning payment reconciliation and order completion paths.

Suggested reviewers: Luquitasjeffrey, mostronatorcoder, ermeme

Poem

A bunny hops where invoices gleam,
Payout secrets join the stream.
Hashes settle, preimages flow,
Orders keep what payments show.
Wiggle ears—success is stored!

🚥 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 matches the main change: persisting buyer payout hash and preimage on orders.
Linked Issues check ✅ Passed The PR implements the linked issue by storing payout hash/preimage on buyer-payout success paths and updating the order model and tests.
Out of Scope Changes check ✅ Passed The changes stay within the buyer payout persistence scope and do not introduce unrelated code paths.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/persist-payout-hash

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.

@grunch grunch requested a review from Luquitasjeffrey July 10, 2026 15:28

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
models/order.ts (1)

75-76: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider indexing payout_hash for reconciliation queries.

The PR objectives mention reconciliation and auditing. If consumers will query orders by payout_hash (e.g., to cross-reference a Lightning payment), a non-unique index would avoid collection scans. The existing hash field uses a partial unique index for similar lookup purposes.

💡 Optional index suggestion
   payout_hash: {
     type: String,
+    index: true,
   }, // payment hash of the buyer payout (proof of payment / reconciliation)
🤖 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 `@models/order.ts` around lines 75 - 76, Add a non-unique index for the
payout_hash field in the order schema, following the existing hash field
indexing pattern, so reconciliation lookups avoid collection scans while
allowing multiple orders to share a payout hash.
🤖 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 `@util/completeOrder.ts`:
- Around line 33-47: Update the pending-payments success fallback to persist
payout details consistently with completeOrderAsSuccess: in the relevant branch
of pending_payments.ts, route the order through completeOrderAsSuccess or set
payout_hash to payment.id and payout_preimage to payment.secret alongside status
SUCCESS.

---

Nitpick comments:
In `@models/order.ts`:
- Around line 75-76: Add a non-unique index for the payout_hash field in the
order schema, following the existing hash field indexing pattern, so
reconciliation lookups avoid collection scans while allowing multiple orders to
share a payout hash.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6c8d4019-225b-4d3d-aff0-01efce11a13c

📥 Commits

Reviewing files that changed from the base of the PR and between 11e1dfe and 537f7fb.

📒 Files selected for processing (4)
  • ln/pay_request.ts
  • models/order.ts
  • tests/bot/pending-payments.spec.ts
  • util/completeOrder.ts

Comment thread util/completeOrder.ts
Comment on lines +33 to +47
{
$set: {
status: 'SUCCESS',
routing_fee: payment.fee,
payout_hash: payment.id,
payout_preimage: payment.secret,
},
},
);
if (won === null) return false;
// Keep the in-memory document consistent for any later save by the caller.
order.status = 'SUCCESS';
order.routing_fee = payment.fee;
order.payout_hash = payment.id;
order.payout_preimage = payment.secret;

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all code paths that set order status to SUCCESS
rg -n --type=ts "status.*=.*'SUCCESS'|status.*SUCCESS|'SUCCESS'" -g '!**/node_modules/**' -g '!**/tests/**' | grep -v "enum\|//\|status: {"

Repository: lnp2pBot/bot

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## util/completeOrder.ts"
if [ -f util/completeOrder.ts ]; then
  wc -l util/completeOrder.ts
  sed -n '1,220p' util/completeOrder.ts | cat -n
else
  echo "missing util/completeOrder.ts"
fi

echo
echo "## SUCCESS assignments and payout fields"
rg -n --type=ts --type=tsx --glob '!**/node_modules/**' --glob '!**/tests/**' \
  "status\s*=\s*'SUCCESS'|status:\s*'SUCCESS'|payout_hash|payout_preimage|routing_fee" .

Repository: lnp2pBot/bot

Length of output: 5502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## all SUCCESS assignments in TypeScript files"
rg -n --type ts --glob '!**/node_modules/**' --glob '!**/tests/**' \
  "order\.status\s*=\s*'SUCCESS'|status:\s*'SUCCESS'|status\s*:\s*'SUCCESS'|['\"]SUCCESS['\"]" .

echo
echo "## files mentioning completeOrderAsSuccess or healConfirmedOrder"
rg -n --type ts --glob '!**/node_modules/**' --glob '!**/tests/**' \
  "completeOrderAsSuccess|healConfirmedOrder" .

Repository: lnp2pBot/bot

Length of output: 2238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for file in jobs/pending_payments.ts util/index.ts bot/modules/community/actions.ts scripts/migrate_completed_by_admin_orders.ts ln/pay_request.ts; do
  if [ -f "$file" ]; then
    echo "## $file"
    wc -l "$file"
    sed -n '1,260p' "$file" | cat -n
    echo
  fi
done

Repository: lnp2pBot/bot

Length of output: 39363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## payout field usages"
rg -n --type ts --glob '!**/node_modules/**' --glob '!**/tests/**' \
  "payout_hash|payout_preimage" .

Repository: lnp2pBot/bot

Length of output: 920


Persist payout fields on the pending-payments success fallback
jobs/pending_payments.ts:113-114 still marks an order as SUCCESS without setting payout_hash or payout_preimage. That leaves some completed orders without payout records; route this branch through completeOrderAsSuccess or set the same fields here.

🤖 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 `@util/completeOrder.ts` around lines 33 - 47, Update the pending-payments
success fallback to persist payout details consistently with
completeOrderAsSuccess: in the relevant branch of pending_payments.ts, route the
order through completeOrderAsSuccess or set payout_hash to payment.id and
payout_preimage to payment.secret alongside status SUCCESS.

@grunch grunch merged commit b7b2af0 into main Jul 10, 2026
6 checks passed
@grunch grunch deleted the feat/persist-payout-hash branch July 10, 2026 16:32
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.

Persist the buyer payout's payment hash and preimage on the order

2 participants