openreceive-server 0.4.2 → 0.4.4

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: 8531811b5ffc69913bcf13970f0c18f6de494360f0ee746251fccd7c0d73ea92
4
- data.tar.gz: 38e4a6c85d95235b12a907bf6852b286f498c8aaceb6b91ca0ec8bc4aa7862c4
3
+ metadata.gz: '08bf7311603d77400f63f99b2ac15650105a25fdeb21ac3f38f8cf2df243f394'
4
+ data.tar.gz: 07ff30f8eab72b50544bc4022135dcf9a4d1e80e28e5062ccb22793bf01222fc
5
5
  SHA512:
6
- metadata.gz: 5b4484815ec4ff40aaf49e6a374a2d9a512843b29c15447bd7476a7828cee9cc8ce8d0a6b8c59823daae9e0f920b231c3f0cab1ee6a88aca7b3ac6c38608ba4e
7
- data.tar.gz: f7c1edd3b2ec73d908e3b3698fe0389f9afc1479e1010be41983c53b36b405360ae64ee247082b20a2e3538e6236e5def2ba708b7a93eb4838900557b8a5525b
6
+ metadata.gz: 1f3aa5d0622a47af6f2b0d1f40866f2763359564875cbfa7299781e840ee6a503e0e8bae78ce8d8825545f49d19c7c1a8a047b35780368410df65818ec8da3af
7
+ data.tar.gz: b7012062378dcbc8da84cd8ed9488c1e9efff9a0e54abd9580e5093cb38617b35760073d0748b1737807364299a3217ed86ad715f8cb8a70cc78ce678bddc501
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4 - 2026-09-07
4
+
5
+ Version lockstep with the 0.4.4 npm release (every checkout image now ships
6
+ inside the JavaScript; no asset option). Ruby changes in this gem:
7
+
8
+ - The FixedFloat status normalizer (`OpenReceive::Server::Swap::FixedFloat`)
9
+ is now a short interpreter of the generated swap-state table shared with
10
+ every engine, pinned by the `swap-state` test vector. Same states and
11
+ reasons as before for every input.
12
+ - The bundled agent skill gains references for Django, FastAPI, Fastify,
13
+ Laravel and Next.js beside Node, Rails and BTCPay Server.
14
+
15
+ ## 0.4.3 - 2026-09-03
16
+
17
+ Version lockstep with the 0.4.3 npm release (container-query checkout
18
+ layout, themed pre-create screens, padded root, money caption). No Ruby
19
+ changes in this gem.
20
+
3
21
  ## 0.4.2 - 2026-09-03
4
22
 
5
23
  Version lockstep with the 0.4.2 npm release (the scoped frontend stylesheet).
@@ -10,10 +10,10 @@ module OpenReceive
10
10
  # Seconds past an attempt's expiry during which reconciliation still scans
11
11
  # for a settlement before closing the attempt. Covers clock skew and
12
12
  # wallets that accept a payment moments after nominal invoice expiry.
13
- # The value 900 is pinned by spec/test-vectors/attempt-reconciliation.json
14
- # ("expiry_grace_seconds", asserted by tools/conformance/ruby-crosslang.rb)
15
- # and mirrored by JS OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS.
16
- EXPIRY_GRACE_SECONDS = 900
13
+ # The value comes from spec/data/kernel-tables.json (generated into
14
+ # OpenReceive::Generated), is pinned by spec/test-vectors/attempt-
15
+ # reconciliation.json, and is the JS OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS.
16
+ EXPIRY_GRACE_SECONDS = OpenReceive::Generated::ATTEMPT_EXPIRY_GRACE_SECONDS
17
17
 
18
18
  module_function
19
19
 
@@ -5,44 +5,15 @@ require "openreceive"
5
5
  module OpenReceive
6
6
  module Server
7
7
  module Swap
8
- # Ruby port of packages/js/node/src/swap/assets.ts: the OpenReceive
8
+ # Ruby twin of packages/js/node/src/swap/assets.ts: the OpenReceive
9
9
  # pay-in asset catalog (labels, network labels, provider matching) and
10
10
  # the network helpers the FixedFloat provider uses to map /ccies rows.
11
11
  module Assets
12
- PAY_IN_ASSETS = %w[
13
- SOL_SOL USDT_TRON USDT_SOL USDC_SOL ETH_ETH USDT_ETH USDC_ETH
14
- ].freeze
15
-
16
- ASSET_INFO = {
17
- "SOL_SOL" => {
18
- "pay_in_asset" => "SOL_SOL", "label" => "SOL", "network_label" => "Solana",
19
- "coin" => "SOL", "network" => "SOL"
20
- }.freeze,
21
- "USDT_TRON" => {
22
- "pay_in_asset" => "USDT_TRON", "label" => "USDT", "network_label" => "Tron",
23
- "coin" => "USDT", "network" => "TRX"
24
- }.freeze,
25
- "USDT_SOL" => {
26
- "pay_in_asset" => "USDT_SOL", "label" => "USDT", "network_label" => "Solana",
27
- "coin" => "USDT", "network" => "SOL"
28
- }.freeze,
29
- "USDC_SOL" => {
30
- "pay_in_asset" => "USDC_SOL", "label" => "USDC", "network_label" => "Solana",
31
- "coin" => "USDC", "network" => "SOL"
32
- }.freeze,
33
- "ETH_ETH" => {
34
- "pay_in_asset" => "ETH_ETH", "label" => "ETH", "network_label" => "Ethereum",
35
- "coin" => "ETH", "network" => "ETH"
36
- }.freeze,
37
- "USDT_ETH" => {
38
- "pay_in_asset" => "USDT_ETH", "label" => "USDT", "network_label" => "Ethereum",
39
- "coin" => "USDT", "network" => "ETH"
40
- }.freeze,
41
- "USDC_ETH" => {
42
- "pay_in_asset" => "USDC_ETH", "label" => "USDC", "network_label" => "Ethereum",
43
- "coin" => "USDC", "network" => "ETH"
44
- }.freeze
45
- }.freeze
12
+ # The asset table is kernel vocabulary (spec/data/kernel-tables.json),
13
+ # generated into every engine; this module adds the lookups and the
14
+ # provider network matching on top of it.
15
+ PAY_IN_ASSETS = OpenReceive::Generated::SWAP_PAY_IN_ASSETS
16
+ ASSET_INFO = OpenReceive::Generated::SWAP_ASSET_INFO
46
17
 
47
18
  module_function
48
19
 
@@ -609,72 +609,46 @@ module OpenReceive
609
609
  { "currency" => "USD", "pay_in_fiat" => pay_in_fiat, "payout_fiat" => payout_fiat }
610
610
  end
611
611
 
612
+ # FixedFloat status + emergency block + refund-tx presence → state and
613
+ # reasons: an interpreter of spec/data/swap-state-table.json (rendered
614
+ # into OpenReceive::Generated), first-match-wins. Pinned across engines
615
+ # by spec/test-vectors/swap-state.json; how to read the rows lives in
616
+ # the JSON. The validator asserts the table ends in a catch-all row.
612
617
  def normalize_status(status, emergency, refund_tx_id)
613
618
  normalized = status.to_s.upcase
614
- if !refund_tx_id.nil? && %w[DONE FINISHED].include?(normalized)
615
- return { "state" => "refunded" }
619
+ refund_tx_present = !refund_tx_id.nil?
620
+ choice = read_string(emergency["choice"])&.upcase
621
+ row = OpenReceive::Generated::SWAP_STATUS_ROWS.find do |candidate|
622
+ status_matches =
623
+ if candidate["status"] == "*"
624
+ candidate["status_contains"].nil? || normalized.include?(candidate["status_contains"])
625
+ else
626
+ candidate["status"] == normalized
627
+ end
628
+ status_matches &&
629
+ (candidate["refund_tx_present"] == "any" || candidate["refund_tx_present"] == refund_tx_present) &&
630
+ (candidate["choice"] == "any" ||
631
+ (choice.nil? ? candidate["choice"] == "absent" : candidate["choice"] == choice))
616
632
  end
617
- case normalized
618
- when "NEW" then return { "state" => "awaiting_deposit" }
619
- when "PENDING" then return { "state" => "confirming" }
620
- when "EXCHANGE" then return { "state" => "exchanging" }
621
- when "WITHDRAW" then return { "state" => "paying_invoice" }
622
- when "DONE" then return { "state" => "completed" }
623
- when "EXPIRED" then return { "state" => "expired" }
633
+ result = { "state" => row.fetch("state") }
634
+ if row["attention_reason"]
635
+ result["attention"] = true
636
+ result["attention_reason"] = row["attention_reason"]
624
637
  end
625
- if normalized == "EMERGENCY"
626
- choice = read_string(emergency["choice"])&.upcase
627
- statuses = read_string_array(emergency["status"]).map(&:upcase)
628
- refund_reason = refund_reason_from_emergency_statuses(statuses)
629
- if choice == "REFUND" && !refund_tx_id.nil?
630
- result = { "state" => "refunded" }
631
- result["refund_reason"] = refund_reason unless refund_reason.nil?
632
- return result
633
- end
634
- if choice == "REFUND"
635
- result = { "state" => "refund_pending" }
636
- result["refund_reason"] = refund_reason unless refund_reason.nil?
637
- return result
638
- end
639
- if choice == "EXCHANGE"
640
- return {
641
- "state" => "attention", "attention" => true,
642
- "attention_reason" => "provider_reported_emergency"
643
- }
644
- end
645
- # An overpay (MORE) takes the same self-serve full-refund path as
646
- # LESS and EXPIRED — it is what the provider itself does with it.
647
- # There is no partial refund of the surplus: the payout invoice is
648
- # a fixed amount, so an emergency deposit is returned whole or not
649
- # at all.
650
- result = { "state" => "refund_required" }
638
+ if row["refund_reason_from_emergency"]
639
+ refund_reason = refund_reason_from_emergency_statuses(read_string_array(emergency["status"]))
651
640
  result["refund_reason"] = refund_reason unless refund_reason.nil?
652
- return result
653
641
  end
654
- return { "state" => "failed" } if normalized.include?("FAIL")
655
-
656
- # An unrecognized status is NOT a provider-reported emergency:
657
- # label it as unknown so operators land on the right runbook section.
658
- {
659
- "state" => "attention", "attention" => true,
660
- "attention_reason" => "provider_status_unrecognized"
661
- }
642
+ result
662
643
  end
663
644
 
664
- # LIMIT rides along with LESS/MORE when the deposit fell outside the
665
- # pair's limits. It says nothing the payer can act on beyond the
666
- # amount itself, so it names no reason of its own.
667
645
  def refund_reason_from_emergency_statuses(statuses)
668
- less = statuses.include?("LESS")
669
- more = (statuses & %w[MORE OVER OVERPAID]).any?
670
- expired = statuses.include?("EXPIRED")
671
- return "underpaid_and_late" if less && expired
672
- return "overpaid_and_late" if more && expired
673
- return "underpaid" if less
674
- return "overpaid" if more
675
- return "late_deposit" if expired
676
-
677
- nil
646
+ aliases = OpenReceive::Generated::SWAP_EMERGENCY_STATUS_ALIASES
647
+ present = statuses.map { |item| aliases.fetch(item.upcase, item.upcase) }
648
+ row = OpenReceive::Generated::SWAP_REFUND_REASON_ROWS.find do |candidate|
649
+ (candidate["all_of"] - present).empty?
650
+ end
651
+ row && row["refund_reason"]
678
652
  end
679
653
 
680
654
  def refund_path_state?(state)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenReceive
4
4
  module Server
5
- VERSION = "0.4.2"
5
+ VERSION = "0.4.4"
6
6
  end
7
7
  end
@@ -10,8 +10,10 @@ module OpenReceive
10
10
  # encryption-mode choice, spend-capability detection, and receive
11
11
  # readiness must be identical in both engines.
12
12
  module WalletInfo
13
- REQUIRED_RECEIVE_METHODS = %w[make_invoice list_transactions].freeze
14
- SPEND_METHODS = %w[pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend].freeze
13
+ # Kernel vocabulary (spec/data/kernel-tables.json), shared with the JS
14
+ # engine and the BTCPay plugin.
15
+ REQUIRED_RECEIVE_METHODS = OpenReceive::Generated::NWC_REQUIRED_RECEIVE_METHODS
16
+ SPEND_METHODS = OpenReceive::Generated::NWC_SPEND_METHODS
15
17
 
16
18
  module_function
17
19
 
@@ -5,7 +5,8 @@ description: >
5
5
  Use when adding Bitcoin, Lightning, or crypto checkout to a Node.js, Express,
6
6
  Fastify, Next.js, Rails, React, Vue, Svelte, Angular, or plain-HTML
7
7
  application with OpenReceive (the @openreceive/* npm packages or the
8
- openreceive-rails gem).
8
+ openreceive-rails gem), or when connecting a BTCPay Server store to a
9
+ receive-only NWC wallet with the OpenReceive plugin.
9
10
  license: MIT
10
11
  ---
11
12
 
@@ -23,15 +24,24 @@ code** (`NWC_URI`).
23
24
  1. Identify the server stack of the application you are in.
24
25
  2. Open the matching reference — it is complete (quickstart inlined) and needs
25
26
  no network access:
26
- - Node (Express / Fastify / Next.js): [references/node.md](references/node.md)
27
+ - Node, Express: [references/node.md](references/node.md)
28
+ - Node, Fastify: [references/fastify.md](references/fastify.md)
29
+ - Node, Next.js App Router: [references/next.md](references/next.md)
27
30
  - Rails: [references/rails.md](references/rails.md)
31
+ - Django: [references/django.md](references/django.md)
32
+ - Laravel: [references/laravel.md](references/laravel.md)
33
+ - BTCPay Server: [references/btcpay.md](references/btcpay.md) — a plugin,
34
+ configured in BTCPay's store UI or Greenfield API; no application code,
35
+ no npm packages, no gem. The rest of this file is about the library.
28
36
  3. Follow its **Step 0** first: confirm `NWC_URI` is set in the server
29
37
  environment before writing code. Never print the value; never invent a
30
38
  placeholder.
31
39
 
32
- Install (Node): `npm install @openreceive/express @openreceive/react` — swap
33
- the adapter (`fastify`, `next`) and UI package (`vue`, `svelte`, `angular`,
34
- `elements`) for the stack. Install (Rails): `bundle add openreceive-rails`.
40
+ Install, per adapter — Express: `npm install @openreceive/express @openreceive/react`;
41
+ Fastify: `npm install @openreceive/fastify @openreceive/react`; Next.js:
42
+ `npm install @openreceive/next @openreceive/react`. Swap the UI package (`vue`,
43
+ `svelte`, `angular`, `elements`) for the frontend the app already has. Install
44
+ (Rails): `bundle add openreceive-rails`.
35
45
 
36
46
  ## The three server objects
37
47
 
@@ -0,0 +1,193 @@
1
+ # OpenReceive agent directions (BTCPay Server)
2
+
3
+ These directions describe OpenReceive 0.4.4.
4
+
5
+ Connect a BTCPay Server store to a receive-only NWC wallet with the OpenReceive
6
+ plugin, and optionally let payers pay BTCPay invoices with USDT, USDC, ETH or
7
+ SOL. You do not need a copy of the OpenReceive source, and there is no
8
+ application code to write: the plugin is configured through BTCPay's store UI
9
+ or its Greenfield API, and the quickstart is appended to this file in full.
10
+
11
+ This is the BTCPay plugin, not the Node or Rails library. Do not install
12
+ `@openreceive/*` packages or the `openreceive-rails` gem into a BTCPay
13
+ deployment, do not add `openreceive_payments` tables, and do not mount
14
+ OpenReceive HTTP routes. BTCPay's invoices, checkout, webhooks and Greenfield
15
+ API are the host; the plugin only supplies the Lightning backend and the swap
16
+ rail.
17
+
18
+ ## What the plugin is
19
+
20
+ A BTCPay Server plugin (`BTCPayServer.Plugins.OpenReceive`) that registers a
21
+ Lightning connection-string handler for `type=openreceive;nwc=<NWC URI>`.
22
+ Saving that string makes the NWC wallet the store's Lightning node: BTCPay
23
+ mints every Lightning invoice in that wallet and its own `LightningListener`
24
+ records the payments. The plugin never calls a NIP-47 `pay_*` method, so
25
+ every send-side BTCPay feature (Lightning payouts, pull-payment refunds over
26
+ Lightning, the send tab) is unavailable by design.
27
+
28
+ The one required credential is a receive-only NWC code. A Lightning Swap
29
+ Connect (LSC) code optionally adds server-side swaps: a provider order aimed at
30
+ the invoice's existing BOLT11, tracked in the plugin's own table, with the
31
+ refund path on the same checkout screen.
32
+
33
+ ## Step 0 — check the deployment before you change anything
34
+
35
+ 1. Confirm the BTCPay Server version is 2.4.2 or later (Server Settings →
36
+ About, or `GET /api/v1/server/info`). The plugin declares that minimum and
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.
42
+ 3. Check whether the store already has an OpenReceive connection:
43
+ `GET /api/v1/stores/{storeId}/openreceive/settings` returns
44
+ `lightningNodeIsOpenReceive`. If true, the wallet step is done — go to
45
+ swaps only if the user wants them.
46
+ 4. If no receive-only NWC code is available, stop and tell the user exactly
47
+ what to create:
48
+
49
+ > OpenReceive cannot mint an invoice without a receive-only NWC code. Get
50
+ > one at https://openreceive.org/get_a_nwc_code_to_receive_payments and
51
+ > paste it into Store → OpenReceive → Test connection, or hand it to me and
52
+ > I will set it through the Greenfield API.
53
+
54
+ Never print, log or echo the code; report only whether it is set. Never
55
+ paste a bare `nostr+walletconnect://` string into BTCPay's Lightning node
56
+ screen — that form is claimed by the Nostr plugin, without the receive-only
57
+ guard.
58
+ 5. If the user wants altcoin payments, ask for an LSC code from
59
+ https://openreceive.org/set_up_swap_provider. Do not wait for it: the
60
+ wallet works without it, and swaps switch on later with one settings change.
61
+
62
+ Only then start the quickstart.
63
+
64
+ ## Non-negotiables
65
+
66
+ - The connection string is `type=openreceive;nwc=<NWC URI>[;allow-spend=true]`
67
+ and nothing else. Set it through the setup page or
68
+ `PUT /api/v1/stores/{storeId}/openreceive/settings` with `nwcUri`, never by
69
+ editing BTCPay's Lightning node screen by hand.
70
+ - Receive-only is required. A code that advertises `pay_invoice` or another
71
+ spend method is refused on save. The override (`allowSpendCapableWallet`,
72
+ the checkbox on the setup page) is the user's explicit choice; never tick it
73
+ to make a save succeed.
74
+ - The wallet's network must match BTCPay's. A mismatch is a refusal, not a
75
+ warning.
76
+ - The wallet must grant `make_invoice` and `list_transactions`.
77
+ `lookup_invoice` is optional; do not ask the user for a code that grants it.
78
+ - Swaps require the store's Lightning node to be the OpenReceive connection.
79
+ Enabling swaps on a store using the internal node is refused
80
+ (`wallet_required`).
81
+ - Swaps set the store's invoice expiration to 60 minutes when it is shorter,
82
+ and the plugin refuses to create a swap on an invoice with less than the
83
+ provider's window left. Do not lower the expiration below 45 minutes on a
84
+ swap-enabled store.
85
+ - Top-up (amountless) invoices are unsupported on this backend. Do not
86
+ configure a point of sale or payment link that relies on them with this
87
+ wallet.
88
+ - Secrets stay server-side. The NWC code and LSC code live in BTCPay's
89
+ database like every other BTCPay credential; never copy them into
90
+ screenshots, tickets, browser code or logs. The provider's order token never
91
+ leaves the server.
92
+ - BTCPay's `LightningListener` is the settlement authority. Provider
93
+ `completed` is not payment; only the wallet reporting the Lightning invoice
94
+ settled is. Do not build anything that fulfils on a provider state.
95
+ - There is no merchant-initiated refund of a settled Lightning payment. A swap
96
+ refund is a payer reclaiming a deposit that never converted, and only from
97
+ the `refund_required` provider state.
98
+
99
+ ## Verifying
100
+
101
+ Store → OpenReceive → **Run a health check** (the doctor page) runs every probe now: connection, preflight,
102
+ notifications, last scan, provider reachability, invoice expiration, swaps
103
+ needing attention. On a regtest machine, `packages/dotnet/docker/up.sh` then
104
+ `e2e.sh` in the OpenReceive repository proves the whole path end to end, and
105
+ that is the only situation where cloning the repository is the right move.
106
+
107
+ ## More documentation
108
+
109
+ Fetch one when the moment comes. Each is raw markdown, so a plain GET is
110
+ enough; drop the `.md` for the same page a person would read.
111
+
112
+ - https://openreceive.org/guides/btcpay-reference.md — every setting, route, swap state, doctor probe and log event of the plugin
113
+ - https://openreceive.org/guides/security.md — why receive-only is the only wallet credential
114
+ - https://openreceive.org/guides/lightning-swap-connect.md — what an LSC code actually is
115
+ - https://openreceive.org/guides/automated-swaps.md — provider states, and what turning swaps on commits a merchant to
116
+ - https://openreceive.org/guides/swap-refunds.md — the refund states; the route back is BTCPay's own invoice checkout page here
117
+ - https://openreceive.org/guides.md — the index, if what you need is not above
118
+
119
+ Questions, or a problem with the plugin itself:
120
+ https://openreceive.org/contact
121
+
122
+ ---
123
+
124
+ ## The quickstart, in full
125
+
126
+ Inlined verbatim so this file needs no network access — follow it once Step 0
127
+ passes. The page it comes from is https://openreceive.org/guides/quickstart-btcpay.
128
+
129
+ ## BTCPay Server quickstart
130
+
131
+ Requires BTCPay Server ≥ 2.4.2.
132
+
133
+ 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
+ never used.
139
+
140
+ 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.
143
+
144
+ ### 1. Prerequisites
145
+
146
+ - A BTCPay Server, version 2.4.2 or later, on any network (mainnet, testnet,
147
+ signet, regtest). The wallet must be on the same network.
148
+ - A receive-only NWC code for the wallet you want to receive into
149
+ ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
150
+ The code must grant `make_invoice` and `list_transactions` and must not
151
+ advertise any spend method. `lookup_invoice` is optional.
152
+ - Optionally, a Lightning Swap Connect (LSC) code from a
153
+ [swap provider](https://openreceive.org/set_up_swap_provider), if payers
154
+ should be able to pay with USDT, USDC, ETH or SOL.
155
+
156
+ ### 2. Install the plugin
157
+
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.
163
+
164
+ To build the plugin from source instead, follow
165
+ [the .NET workspace README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/README.md).
166
+
167
+ ### 3. Connect the wallet
168
+
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.
176
+
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.
182
+
183
+ ### 4. Check it
184
+
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.
189
+
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.