openreceive 0.4.10 → 0.4.13

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: e612d1c8beae58671f0e863dbf26622b07a689010b81c3f05abd1463b7f1a4ff
4
- data.tar.gz: 68c216cd101ad1d50dd6175b4e565793f7b147a26e1d07e1cda18ffe42659ed6
3
+ metadata.gz: 01e1bb860a7a293671cf05f57aab92369123dcade28a58bdb21c7c0917d3e53c
4
+ data.tar.gz: ccdce9021e041361aa8340972fea01cdf4e762bcf4798903900185ec9535f63c
5
5
  SHA512:
6
- metadata.gz: 24f0505bb26aa2b2b06f439854cf8839825c24d4b1634ecc184c86a6914269f25c621664efc7ba792e04fc644ab2107784b7b78737564669bf57e77e5a8e5452
7
- data.tar.gz: a3edc8c5fdda1f703da32f6abe8e87c2ee65fe0d8957c6da7d539b64a8f5762b2a9b0ec952c4f6ef7ee9d4ee5671d8d3675f67693eeff89c5f227b9d0026e089
6
+ metadata.gz: 72862dc8a7059c13ff37db4261c4c6f37daaf2eba0e7bceebaf6cc2eeae8754938bb2aa853790856a0429922d6eff465d65f8e293759d9757c998b7b7060b107
7
+ data.tar.gz: c3f5e0fc7c645e3a298579f29f926e1c33ee7bfa9fc5f764ef5e826d0905f34705439adde32156eb3758944dd14346ef18a50a4b315901683a3a512a056509e0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.13 - 2026-10-01
4
+
5
+ Release in lockstep with the 0.4.13 reconciliation fixes (in
6
+ `openreceive-rails`: a slow wallet no longer livelocks the reconcile pass,
7
+ host-clock attempts get their own scan window, the gate floor is 3 s). No
8
+ change to this gem's API.
9
+
10
+ ## 0.4.12 - 2026-10-01
11
+
12
+ Release in lockstep with the 0.4.12 checkout fixes ("Switch payment method"
13
+ always returns to the method grid; the swap deposit warning drops its "pay
14
+ with one method only" sentence). No Ruby core changes.
15
+
16
+ ## 0.4.11 - 2026-09-21
17
+
18
+ Wallet history scans stop trusting a page's usable length. A page the wallet
19
+ sent full but that lost a malformed row to normalization no longer reads as
20
+ the end of the history, the skipped-row count survives a second normalization
21
+ (the walk normalizes a page the client already normalized), and the offset
22
+ advances by the rows the wallet actually sent. A non-object transaction row is
23
+ now a skipped row rather than a normalization crash. Two new cases in
24
+ `spec/test-vectors/wallet-scan-truncation.json` cover both shapes.
25
+
26
+ Normalized wallet errors are redacted: `nostr+walletconnect:` and
27
+ `lightning+swapconnect:` URIs and `key=`/`token=`/`preimage=`-style parameters
28
+ never survive into a message, and `redact_secrets` is available for diagnostic
29
+ payloads. The nwc-ruby adapter enforces a scan deadline inside the wallet
30
+ request itself (`_deadline`), bounding only wallet I/O — no database
31
+ transaction or fulfillment callback is interrupted.
32
+
3
33
  ## 0.4.10 - 2026-09-16
4
34
 
5
35
  Release in lockstep with the 0.4.10 stablecoin checkout fix. The shared
@@ -185,9 +185,13 @@ module OpenReceive
185
185
  # settle nor close pending attempts (a permanent livelock while the bad
186
186
  # row stays inside the scan window). Bad rows are skipped and counted.
187
187
  # Mirrors the JS normalizeListTransactionsResult policy.
188
+ # A page a client already normalized arrives with its count: the wallet
189
+ # walk normalizes again, and must still see how many rows the wallet sent.
188
190
  transactions = []
189
- skipped_rows = 0
191
+ skipped_rows = data["skipped_rows"].is_a?(Integer) ? data["skipped_rows"] : 0
190
192
  rows.each do |row|
193
+ raise ArgumentError, "non-object transaction row" unless row.respond_to?(:each_pair)
194
+
191
195
  transactions << normalize_transaction(row)
192
196
  rescue StandardError
193
197
  skipped_rows += 1
@@ -357,6 +361,23 @@ module OpenReceive
357
361
  # Normalize any wallet/library failure into the canonical error body shape
358
362
  # shared with JS (spec/test-vectors/error-normalization.json):
359
363
  # { "code", "message", "retryable", "request_id"?, "details"? }.
364
+ def redact_error_text(value)
365
+ value.to_s.gsub(/nostr\+walletconnect:[^\s"'`<>]+/i, "[REDACTED_NWC]")
366
+ .gsub(/lightning\+swapconnect:[^\s"'`<>]+/i, "[REDACTED_LSC]")
367
+ .gsub(/((?:key|secret|client_secret|provider_token|api_key|apikey|token|preimage)=)[^&\s"'<>]+/i, '\\1[REDACTED]')
368
+ end
369
+
370
+ def redact_secrets(value)
371
+ case value
372
+ when String then redact_error_text(value)
373
+ when Array then value.map { |item| redact_secrets(item) }
374
+ when Hash
375
+ sensitive = %w[secret clientsecret providertoken apikey key token preimage invoice bolt11 swapdata authorization password nwc nwcuri lscuri]
376
+ value.to_h { |key, item| [key, sensitive.include?(key.to_s.downcase.gsub(/[^a-z0-9]/, "")) ? "[REDACTED]" : redact_secrets(item)] }
377
+ else value
378
+ end
379
+ end
380
+
360
381
  def normalize_wallet_error(raw)
361
382
  records = collect_error_records(raw)
362
383
  code = error_code_from_records(records) ||
@@ -364,7 +385,7 @@ module OpenReceive
364
385
  "OTHER"
365
386
  {
366
387
  "code" => code,
367
- "message" => error_message_from(records, raw, code),
388
+ "message" => redact_error_text(error_message_from(records, raw, code)),
368
389
  "retryable" => first_boolean(records, "retryable") { RETRYABLE_ERROR_CODES.include?(code) },
369
390
  "request_id" => first_string(records, %w[request_id requestId]),
370
391
  "details" => records.filter_map { |record| record["details"] if record["details"].is_a?(Hash) }.first
@@ -496,7 +517,8 @@ module OpenReceive
496
517
  request["unpaid"] = true if include_unpaid
497
518
  request["from"] = scan_from unless scan_from.nil?
498
519
  request["until"] = scan_until unless scan_until.nil?
499
- page = Nwc.normalize_list_transactions_response(client.list_transactions(request)).fetch("transactions")
520
+ response = Nwc.normalize_list_transactions_response(client.list_transactions(request))
521
+ page = response.fetch("transactions")
500
522
  page.each do |row|
501
523
  next unless row["type"].nil? || row["type"] == "incoming"
502
524
  payment_hash = row_payment_hash(row)
@@ -504,7 +526,10 @@ module OpenReceive
504
526
  rows[payment_hash] = row
505
527
  outstanding.delete(payment_hash)
506
528
  end
507
- if outstanding.empty? || page.length < TRANSACTION_PAGE_LIMIT
529
+ # The wallet ran out of rows only when the page IT sent was short: a
530
+ # row the normalizer dropped was still a row, and a full page with one
531
+ # of them dropped must not read as the end of the history.
532
+ if outstanding.empty? || page.length + response.fetch("skipped_rows", 0) == 0
508
533
  truncated = false
509
534
  break
510
535
  end
@@ -513,7 +538,7 @@ module OpenReceive
513
538
  page_key = page.map { |row| row["payment_hash"].to_s }.join(",")
514
539
  break if page_key == previous_page
515
540
  previous_page = page_key
516
- offset += TRANSACTION_PAGE_LIMIT
541
+ offset += page.length + response.fetch("skipped_rows", 0)
517
542
  end
518
543
  { rows: rows, truncated: truncated }
519
544
  end
@@ -6,6 +6,7 @@
6
6
  # `openreceive` umbrella, which loads this adapter — carries everything the
7
7
  # adapter calls, so requiring this file directly keeps working.
8
8
  require_relative "core"
9
+ require "timeout"
9
10
 
10
11
  module OpenReceive
11
12
  # Thin adapter binding the engine to the nwc-ruby gem (NwcRuby::Client).
@@ -29,7 +30,18 @@ module OpenReceive
29
30
  def list_transactions(request)
30
31
  params = symbolize_keys(OpenReceive.list_transactions_nip47_request(request))
31
32
  params[:until_ts] = params.delete(:until) if params.key?(:until)
32
- OpenReceive.normalize_list_transactions_response(@client.list_transactions(**params))
33
+ deadline = request["_deadline"]
34
+ response = if deadline.nil?
35
+ @client.list_transactions(**params)
36
+ else
37
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
38
+ raise Timeout::Error, "Wallet history scan deadline exceeded." unless remaining.positive?
39
+
40
+ # Bound only wallet I/O. The gem closes its per-call socket in
41
+ # ensure; no database transaction or fulfillment is interrupted.
42
+ Timeout.timeout(remaining) { @client.list_transactions(**params) }
43
+ end
44
+ OpenReceive.normalize_list_transactions_response(response)
33
45
  end
34
46
 
35
47
  def preflight
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenReceive
4
- VERSION = "0.4.10"
4
+ VERSION = "0.4.13"
5
5
  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.10.
3
+ These directions describe OpenReceive 0.4.13.
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
@@ -119,6 +121,8 @@ enough; drop the `.md` for the same page a person would read.
119
121
  Questions, or a problem with the plugin itself:
120
122
  https://openreceive.org/contact
121
123
 
124
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
125
+
122
126
  ---
123
127
 
124
128
  ## The quickstart, in full
@@ -131,15 +135,15 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-btcp
131
135
  Requires BTCPay Server ≥ 2.4.4.
132
136
 
133
137
  The OpenReceive plugin makes a receive-only NWC wallet the Lightning node of a
134
- BTCPay store. BTCPay mints every Lightning invoice in that wallet and records
135
- payments through its own settlement machinery. Optionally, payers can pay a
136
- BTCPay invoice with USDT, USDC, ETH or SOL through a Lightning Swap Connect
137
- 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
138
142
  never used.
139
143
 
140
144
  This is not the Node or Rails library. There are no hooks, no
141
- `openreceive_payments` table and no OpenReceive HTTP routes: BTCPay's
142
- 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.
143
147
 
144
148
  ### 1. Prerequisites
145
149
 
@@ -155,39 +159,66 @@ invoices, checkout, webhooks and Greenfield API are the host.
155
159
 
156
160
  ### 2. Install the plugin
157
161
 
158
- In BTCPay, open **Server Settings → Plugins**, search the plugin directory
159
- for **OpenReceive**, click **Install**, and restart BTCPay when prompted.
160
- BTCPay creates the plugin's one table (`openreceive_swaps`, schema
161
- `BTCPayServer.Plugins.OpenReceive`) in its own Postgres at startup; nothing
162
- 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.
163
177
 
164
178
  To build the plugin from source instead, follow
165
179
  [the .NET workspace README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/README.md).
166
180
 
167
181
  ### 3. Connect the wallet
168
182
 
169
- Follow the plugin README's illustrated walkthrough:
170
- [OpenReceive for BTCPay Server](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/BTCPayServer.Plugins.OpenReceive/README.md).
171
- It opens the **OpenReceive** page in the store's sidebar, saves the
172
- receive-only NWC code, optionally saves the LSC code to turn swaps on, and
173
- creates a first test invoice. There is nothing else to configure: you never
174
- open BTCPay's Lightning node screen, and the plugin never reads the internal
175
- 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**.
176
189
 
177
- Saving fails closed if the wallet advertises a spend method such as
178
- `pay_invoice`. Mint a receive-only code instead; the override for a wallet
179
- that cannot is a deliberate, logged choice. Swaps raise the store's invoice
180
- expiration to 60 minutes when it is shorter, because a swap needs at least 45
181
- 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.
182
205
 
183
206
  ### 4. Check it
184
207
 
185
- **Run a health check** on the OpenReceive page runs every probe in place:
186
- the connection, the wallet preflight, payment notifications, the last wallet
187
- scan, the swap provider and its assets, the invoice expiration, and swaps
188
- 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.
189
220
 
190
- Every setting, Greenfield route, swap state, log event and probe is in the
191
- [BTCPay plugin reference](https://openreceive.org/guides/btcpay-reference.md), including what is
192
- unsupported by design: every send-side feature, top-up invoices, and a bare
193
- `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.