openreceive-rails 0.4.11 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b819ffb0e4f2872cd4de6da728c6404804496da560443a928d54d0aa25a5ac6
4
- data.tar.gz: 674d40eb671af219dbaafd038eb4e8bee4849649e8bf32d82bf3fd1f45405414
3
+ metadata.gz: e427ecbaa9d1cbb7007ea7799ff14eeef1a88b5e9b354d79dc9ad1c2d1fc4c53
4
+ data.tar.gz: e5a92ea94f6edb67a171c03d1bd1367bd66973d672bfd9cdf4f6eb16a52c9307
5
5
  SHA512:
6
- metadata.gz: a2ad30551213e01ce2fce1ccd0f083d0a26aacf8782a0e38d78c8e628d5a8bf68ebb765ba122fc8c9f8375c375bf7582e0fce8f92a5c8902d6e5a0f7effece7e
7
- data.tar.gz: 16b67e1d5797202128acb827f388f371c6d391158c5227f6d8f5acf5fd530b2f3b32604ebbe81609b51cf03b4049f6fcf41a756cf5e81c8119c986e4eec466e9
6
+ metadata.gz: 2c2a061b4ce15de084a423ee8f440e4bcaf6ac2ce0b156855daae25492ac1df0a51d788739e3008c81c08faeb924cdb8faa83c1ca7f6e1b29e3919b0a139b93f
7
+ data.tar.gz: 933f003c11889f222970ca79353b04a468b9076f82466a6d3f9f76790b0b9d267c483b135f81852ddeb03f0c0e79e2dcf60291e09cab6c699dc2b8cef087c737
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.14 - 2026-10-04
4
+
5
+ Fixes two 0.4.13 reconciliation regressions. A host-clock attempt could be
6
+ skipped indefinitely: with one scan-window slot taken, selection moved its
7
+ cursor past the whole batch and dropped the batch's host-clock attempts. The
8
+ cursor now moves only past attempts it has queued. And a wallet that answers
9
+ only one history page per slice re-read the same overlap page forever; an
10
+ overlap answered alone is now spent, and the next slice reads the unseen page
11
+ first. Both are pinned by new `reconcile-progress.json` vectors. The bundled
12
+ integration skill also documents the plain-HTML checkout's way back to a swap
13
+ refund (`resume-payment-hash`, `resumable`, the `openreceive-state` event).
14
+
15
+ ## 0.4.13 - 2026-10-01
16
+
17
+ Reconciliation no longer livelocks on a slow wallet. A wallet-history page
18
+ still in flight at the scan deadline (`Timeout` in the nwc-ruby adapter) failed
19
+ the whole pass and dropped its resume offset, so a long walk restarted at
20
+ offset 0 on every pass and logged `reconciliation failed (will retry):
21
+ OpenReceive::Server::WalletFailureError: execution expired` forever. A cut page
22
+ now ends the slice and keeps the completed pages' progress. A first page cut
23
+ before the wallet answers still fails the pass. Host-clock attempts (legacy
24
+ rows without `created_at_source`) get their own full-history cohort instead of
25
+ widening the window of every wallet-timed attempt pending with them. The gate
26
+ floor rises from 2 s to 3 s (`OpenReceive::MIN_RECONCILE_INTERVAL_SECONDS`).
27
+
28
+ ## 0.4.12 - 2026-10-01
29
+
30
+ Release in lockstep with the 0.4.12 checkout fixes. The Rails Buy a Button
31
+ demo's checkout now returns to the method grid on every "Switch payment
32
+ method" click. No Rails engine API change.
33
+
3
34
  ## 0.4.11 - 2026-09-21
4
35
 
5
36
  Reconciliation progress is durable and bounded. A capped wallet walk now
@@ -90,7 +90,7 @@ OpenReceive.configure do |config|
90
90
 
91
91
  # Settlement discovery is opportunistic by default: every engine request
92
92
  # first runs one reconcile pass through the durable openreceive_meta gate
93
- # (shared by all Puma workers; min 2s between real wallet scans), so pending
93
+ # (shared by all Puma workers; min 3s between real wallet scans), so pending
94
94
  # attempts settle or close on any later OpenReceive call — no scheduled job
95
95
  # required. Set false only if a dedicated worker owns scanning.
96
96
  # config.opportunistic_reconcile = false
@@ -81,7 +81,7 @@ module OpenReceive
81
81
  # durably gated reconcile pass when attempts are pending, so abandoned
82
82
  # checkouts settle on any later OpenReceive call with no scheduled job.
83
83
  # The openreceive_meta gate row is shared by every Puma worker/process on
84
- # the host database (min 2s between real wallet scans, stretched by
84
+ # the host database (min 3s between real wallet scans, stretched by
85
85
  # invoice age). Set false to disable (e.g. when the optional
86
86
  # `bin/rails openreceive:notifications` worker owns scanning), or a Hash
87
87
  # with min_interval_seconds to tune.
@@ -5,6 +5,6 @@ module OpenReceive
5
5
  # top-level `::Rails` framework constant — engine code always references the framework as
6
6
  # `::Rails` to avoid shadowing.
7
7
  module Rails
8
- VERSION = "0.4.11"
8
+ VERSION = "0.4.14"
9
9
  end
10
10
  end
@@ -6,9 +6,9 @@ require "openreceive/reconcile_scan"
6
6
 
7
7
  module OpenReceive
8
8
  # Floor for the durable reconcile-gate interval (seconds); stretched by
9
- # invoice age (2s while any pending invoice is under 2 minutes old, 6s under
9
+ # invoice age (3s while any pending invoice is under 2 minutes old, 6s under
10
10
  # 5 minutes, else 12s). Mirrors the JS OPENRECEIVE_MIN_RECONCILE_INTERVAL_SECONDS.
11
- MIN_RECONCILE_INTERVAL_SECONDS = 2
11
+ MIN_RECONCILE_INTERVAL_SECONDS = 3
12
12
  # The deadline reaches the wallet adapter, which bounds only network I/O.
13
13
  # Never interrupt the whole pass: it also runs host/database transactions.
14
14
  RECONCILE_SCAN_TIMEOUT_SECONDS = 9
@@ -82,11 +82,30 @@ module OpenReceive
82
82
  candidates = OpenReceivePayment.reconcilable_attempts
83
83
  end
84
84
  unless candidates.empty?
85
- last = candidates.last
86
- scheduler["cursor"] = candidates.length < Server::RECONCILE_BATCH_SIZE ? nil : last.slice("created_at", "payment_hash")
87
85
  queued = windows.flat_map { |w| w.fetch("attempts").map { |a| a.fetch("payment_hash") } }
88
- cohort = candidates.reject { |a| queued.include?(a.fetch("payment_hash")) }
89
- windows << ReconcileScan.new_window(cohort, observed_at, overlap_seconds) unless cohort.empty?
86
+ # A host-clock attempt's window spans the whole wallet history. It
87
+ # gets its own window, so it never drags wallet-timed attempts into
88
+ # that walk. Candidates are taken in keyset order, and the cursor
89
+ # moves only past those already queued or admitted: one whose clock
90
+ # source has no free slot stops the selection and is read again next
91
+ # time, never skipped.
92
+ cohorts = {}
93
+ taken = 0
94
+ candidates.each do |attempt|
95
+ unless queued.include?(attempt.fetch("payment_hash"))
96
+ wallet = attempt["created_at_source"] == "wallet"
97
+ break if !cohorts.key?(wallet) && windows.length + cohorts.length >= 2
98
+
99
+ (cohorts[wallet] ||= []) << attempt
100
+ end
101
+ taken += 1
102
+ end
103
+ # A short batch taken whole already reached the ledger's tail: wrap now.
104
+ tail = taken == candidates.length && candidates.length < Server::RECONCILE_BATCH_SIZE
105
+ scheduler["cursor"] = tail ? nil : candidates[taken - 1].slice("created_at", "payment_hash")
106
+ [true, false].each do |wallet|
107
+ windows << ReconcileScan.new_window(cohorts[wallet], observed_at, overlap_seconds) if cohorts.key?(wallet)
108
+ end
90
109
  end
91
110
  end
92
111
  window = windows.shift
@@ -266,7 +285,7 @@ module OpenReceive
266
285
  )
267
286
  end
268
287
 
269
- # Info, not debug: passes are durably gated (min 2s apart, and only while
288
+ # Info, not debug: passes are durably gated (min 3s apart, and only while
270
289
  # attempts are pending), so operators can watch settlement discovery and
271
290
  # the batched list_transactions window without raising the log level. All
272
291
  # pending attempts share one creation-time window walked at most twice —
@@ -296,7 +315,7 @@ module OpenReceive
296
315
 
297
316
  # The gate interval for the current pending set: the configured floor
298
317
  # (config.opportunistic_reconcile min_interval_seconds), stretched by
299
- # invoice age — 2s while any pending invoice is under 2 minutes old, 6s
318
+ # invoice age — 3s while any pending invoice is under 2 minutes old, 6s
300
319
  # under 5 minutes, else 12s. Mirrors the JS reconcile gate.
301
320
  def reconcile_gate_interval_seconds(attempts, now, setting)
302
321
  floor = MIN_RECONCILE_INTERVAL_SECONDS
@@ -307,7 +326,7 @@ module OpenReceive
307
326
  age_stretch = attempts.map do |attempt|
308
327
  elapsed = [now - Integer(attempt.fetch("created_at")), 0].max
309
328
  if elapsed < 120
310
- 2
329
+ 3
311
330
  elsif elapsed < 300
312
331
  6
313
332
  else
@@ -27,7 +27,16 @@ module OpenReceive
27
27
  anchor = resumed ? window["anchor_offset"] : nil
28
28
  replaying = !anchor.nil?
29
29
  previous = window["fingerprint"]
30
- max_pages.times do
30
+ # The last answered page was the overlap re-read. A slice that ends here
31
+ # spends the overlap, so the next slice reads the unseen page first: a
32
+ # wallet answering one page per slice still advances. The fingerprint
33
+ # stays, to catch a wallet that ignores offset.
34
+ overlap_only = false
35
+ continued = lambda do
36
+ window["anchor_offset"] = nil if overlap_only
37
+ [results.values, false, false]
38
+ end
39
+ max_pages.times do |page_number|
31
40
  break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
32
41
 
33
42
  offset = replaying ? anchor : window.fetch("offset")
@@ -35,8 +44,23 @@ module OpenReceive
35
44
  request["until"] = window["until"] unless window["until"].nil?
36
45
  request["unpaid"] = true if window.fetch("view") == "inclusive"
37
46
  request["_deadline"] = deadline
38
- page = OpenReceive.normalize_list_transactions_response(service.send(:call_nwc, :list_transactions, request))
39
- return [results.values, false, false] if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
47
+ page = begin
48
+ OpenReceive.normalize_list_transactions_response(service.send(:call_nwc, :list_transactions, request))
49
+ rescue Server::WalletFailureError
50
+ # The client cuts the in-flight page at this deadline. After at least
51
+ # one answered page that ends the slice like a page answered late:
52
+ # the completed pages keep their progress. Failing the pass would drop
53
+ # the resume offset, and a wallet too slow to finish the walk in one
54
+ # slice would then re-walk the same first pages on every pass. A cut
55
+ # first page made no progress and still fails the pass, so a wallet
56
+ # that never answers stays visible.
57
+ raise if page_number.zero? || Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
58
+
59
+ return continued.call
60
+ end
61
+ return continued.call if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
62
+
63
+ overlap_only = false
40
64
 
41
65
  rows = page.fetch("transactions")
42
66
  physical = rows.length + page.fetch("skipped_rows", 0)
@@ -60,6 +84,7 @@ module OpenReceive
60
84
  unless physical.zero?
61
85
  window["anchor_offset"] = offset
62
86
  window["fingerprint"] = fingerprint
87
+ overlap_only = true
63
88
  next
64
89
  end
65
90
  end
@@ -87,7 +112,7 @@ module OpenReceive
87
112
  previous = fingerprint
88
113
  return [results.values, true, false] if expected.all? { |hash| %w[settled expired failed].include?(window.fetch("observations").dig(hash, "status")) }
89
114
  end
90
- [results.values, false, false]
115
+ continued.call
91
116
  end
92
117
  end
93
118
  end
@@ -49,7 +49,7 @@ same diagnostics redacted, always exit 0 — safe to share.
49
49
  ## 4. Paid but never settles
50
50
 
51
51
  - Settlement is opportunistic: any OpenReceive request runs one reconcile pass
52
- through a durable gate (min 2s between wallet scans, stretched by invoice
52
+ through a durable gate (min 3s between wallet scans, stretched by invoice
53
53
  age). A quiet server settles on the next request — or run the optional
54
54
  notification worker. No timer is missing; that is the design.
55
55
  - An unpaid attempt closes only after a successful wallet scan at/after expiry
@@ -34,9 +34,10 @@ code** (`NWC_URI`).
34
34
  - BTCPay Server: [references/btcpay.md](references/btcpay.md) — a plugin,
35
35
  configured in BTCPay's store UI or Greenfield API; no application code,
36
36
  no npm packages, no gem. The rest of this file is about the library.
37
- 3. Follow its **Step 0** first: confirm `NWC_URI` is set in the server
38
- environment before writing code. Never print the value; never invent a
39
- placeholder.
37
+ 3. Follow its **Step 0** first: before writing code or searching the machine,
38
+ ask the user for the receive-only NWC code (then the swap URI), one question
39
+ per message, and store each pasted code in the project's env file yourself.
40
+ Never print the value; never invent a placeholder.
40
41
 
41
42
  Install, per adapter — Express: `npm install @openreceive/express @openreceive/react`;
42
43
  Fastify: `npm install @openreceive/fastify @openreceive/react`; Next.js:
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (BTCPay Server)
2
2
 
3
- These directions describe OpenReceive 0.4.11.
3
+ These directions describe OpenReceive 0.4.14.
4
4
 
5
5
  Connect a BTCPay Server store to a receive-only NWC wallet with the OpenReceive
6
6
  plugin, and optionally let payers pay BTCPay invoices with USDT, USDC, ETH or
@@ -35,10 +35,12 @@ refund path on the same checkout screen.
35
35
  1. Confirm the BTCPay Server version is 2.4.4 or later (Server Settings →
36
36
  About, or `GET /api/v1/server/info`). The plugin declares that minimum and
37
37
  BTCPay refuses to load it below.
38
- 2. Check whether the plugin is installed (Server Settings → Plugins, or the
39
- store navigation shows an "OpenReceive" entry). If not, install it from the
40
- BTCPay plugin directory (Server Settings → Plugins, search "OpenReceive"),
41
- as the quickstart says; do not invent an installer command.
38
+ 2. Check whether the plugin is installed (the Plugins menu — the plug icon in
39
+ the top-right corner — under Installed Plugins, or the store navigation
40
+ shows an "OpenReceive" entry). If not, install it from the BTCPay plugin
41
+ directory (the same Plugins menu → Plugin Directory, search "openreceive",
42
+ then Install and Restart now), as the quickstart says; do not invent an
43
+ installer command.
42
44
  3. Check whether the store already has an OpenReceive connection:
43
45
  `GET /api/v1/stores/{storeId}/openreceive/settings` returns
44
46
  `lightningNodeIsOpenReceive`. If true, the wallet step is done — go to
@@ -133,15 +135,15 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-btcp
133
135
  Requires BTCPay Server ≥ 2.4.4.
134
136
 
135
137
  The OpenReceive plugin makes a receive-only NWC wallet the Lightning node of a
136
- BTCPay store. BTCPay mints every Lightning invoice in that wallet and records
137
- payments through its own settlement machinery. Optionally, payers can pay a
138
- BTCPay invoice with USDT, USDC, ETH or SOL through a Lightning Swap Connect
139
- provider; the swap settles into the same wallet. The store's internal node is
138
+ BTCPay store. BTCPay creates every Lightning invoice in that wallet. It records
139
+ payments the same way it records any other payment. You can also let payers pay
140
+ a BTCPay invoice with USDT, USDC, ETH or SOL through a Lightning Swap Connect
141
+ provider. The swap pays into the same wallet. The store's internal node is
140
142
  never used.
141
143
 
142
144
  This is not the Node or Rails library. There are no hooks, no
143
- `openreceive_payments` table and no OpenReceive HTTP routes: BTCPay's
144
- invoices, checkout, webhooks and Greenfield API are the host.
145
+ `openreceive_payments` table and no OpenReceive HTTP routes. BTCPay's own
146
+ invoices, checkout, webhooks and Greenfield API do that work.
145
147
 
146
148
  ### 1. Prerequisites
147
149
 
@@ -157,39 +159,66 @@ invoices, checkout, webhooks and Greenfield API are the host.
157
159
 
158
160
  ### 2. Install the plugin
159
161
 
160
- In BTCPay, open **Server Settings → Plugins**, search the plugin directory
161
- for **OpenReceive**, click **Install**, and restart BTCPay when prompted.
162
- BTCPay creates the plugin's two tables (`openreceive_invoices` and
163
- `openreceive_swaps`, schema `BTCPayServer.Plugins.OpenReceive`) in its own
164
- Postgres at startup; nothing else is created.
162
+ Sign in as a **server administrator**. If someone else hosts your server, ask
163
+ them to install the plugin for you.
164
+
165
+ **1. Open the Plugins menu.** It is the plug icon in the top-right corner.
166
+
167
+ **2. Click Plugin Directory.**
168
+
169
+ **3. Search for `openreceive`** and click the **OpenReceive** result.
170
+
171
+ **4. Click Install in BTCPay Server.** Confirm when prompted, then click
172
+ **Restart now** and wait for BTCPay to come back.
173
+
174
+ At startup, BTCPay creates the plugin's two tables in its own Postgres
175
+ database: `openreceive_invoices` and `openreceive_swaps`, in the schema
176
+ `BTCPayServer.Plugins.OpenReceive`. Nothing else is created.
165
177
 
166
178
  To build the plugin from source instead, follow
167
179
  [the .NET workspace README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/README.md).
168
180
 
169
181
  ### 3. Connect the wallet
170
182
 
171
- Follow the plugin README's illustrated walkthrough:
172
- [OpenReceive for BTCPay Server](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/BTCPayServer.Plugins.OpenReceive/README.md).
173
- It opens the **OpenReceive** page in the store's sidebar, saves the
174
- receive-only NWC code, optionally saves the LSC code to turn swaps on, and
175
- creates a first test invoice. There is nothing else to configure: you never
176
- open BTCPay's Lightning node screen, and the plugin never reads the internal
177
- node.
183
+ 1. Select your store and open **OpenReceive** in its sidebar, under Wallets.
184
+ 2. Paste your receive-only NWC code. To see what the wallet supports first,
185
+ click **Test connection**.
186
+ 3. Click **Save NWC Code**.
187
+ 4. To turn swaps on, paste a Lightning Swap Connect code and click **Save swap
188
+ settings**.
178
189
 
179
- Saving fails closed if the wallet advertises a spend method such as
180
- `pay_invoice`. Mint a receive-only code instead; the override for a wallet
181
- that cannot is a deliberate, logged choice. Swaps raise the store's invoice
182
- expiration to 60 minutes when it is shorter, because a swap needs at least 45
183
- minutes of invoice life.
190
+ The page then shows **Wallet connected**. If you set up a provider, it also
191
+ shows **Swaps on**. There is nothing else to configure. You never open BTCPay's
192
+ Lightning node screen, and the plugin never reads the internal node.
193
+
194
+ Screenshots for each of those steps, and for creating a first test invoice,
195
+ are in the plugin's
196
+ [README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/BTCPayServer.Plugins.OpenReceive/README.md).
197
+
198
+ The plugin refuses to save a code whose wallet advertises a spend method such
199
+ as `pay_invoice`. Create a receive-only code instead. If your wallet cannot
200
+ make one, there is an override, but using it is a deliberate choice and the
201
+ plugin logs it.
202
+
203
+ Turning swaps on raises the store's invoice expiration to 60 minutes if it is
204
+ shorter. A swap needs the invoice to stay open for at least 45 minutes.
184
205
 
185
206
  ### 4. Check it
186
207
 
187
- **Run a health check** on the OpenReceive page runs every probe in place:
188
- the connection, the wallet preflight, payment notifications, the last wallet
189
- scan, the swap provider and its assets, the invoice expiration, and swaps
190
- that need a human. Each failing probe carries a fix link.
208
+ Click **Run a health check** on the OpenReceive page. It runs every check
209
+ right there:
210
+
211
+ - the connection
212
+ - the wallet preflight
213
+ - payment notifications
214
+ - the last wallet scan
215
+ - the swap provider and its assets
216
+ - the invoice expiration
217
+ - swaps that need a human
218
+
219
+ Each failing check comes with a link to the fix.
191
220
 
192
- Every setting, Greenfield route, swap state, log event and probe is in the
193
- [BTCPay plugin reference](https://openreceive.org/guides/btcpay-reference.md), including what is
194
- unsupported by design: every send-side feature, top-up invoices, and a bare
195
- `nostr+walletconnect://` string in BTCPay's Lightning node screen.
221
+ The [BTCPay plugin reference](https://openreceive.org/guides/btcpay-reference.md) lists every setting,
222
+ Greenfield route, swap state, log event and check. It also lists what the
223
+ plugin does not support by design: every send-side feature, top-up invoices,
224
+ and a bare `nostr+walletconnect://` string in BTCPay's Lightning node screen.