openreceive 0.4.3 → 0.4.5

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: fcf74bd2d30e343699aec8c8cde07877a32fe35e6dd0b9bd13024c8001b3e76f
4
- data.tar.gz: ce4b49f8a5e973223bfbaebde4facb23b02f9f0cb57ee467fbd8a8a9dac2576c
3
+ metadata.gz: 34ae80bac72c06fbd721f83d77e972048e8fe5e279f837318b223fe089f63af1
4
+ data.tar.gz: d51e11f2c282f1816b5d86de0db98a76039f3c21cfb0190b2cf4286941d98724
5
5
  SHA512:
6
- metadata.gz: 983f8761fe5e9aa080ff7847e7987fe962d6edb07e31aae868b966c957b1d2180c9926ba70b554785c734608f4638e38d644d940ba225562d00d7e8144056484
7
- data.tar.gz: 6e2863ec1643a2013d30028c0485c4e69b79bc809968bb51b457942c648b89fc6913e47d20c6ea52c53799e12298fa0d3a38e1159d0ad8f23d786bc21e353728
6
+ metadata.gz: 241ceb79663eb4fb7ffdcc617342784521830a1e12208ffea8286d1b8b577102a3926328757647dcfccaef7955f4e5445ee16710ec8c57574db907d0ab62f7ae
7
+ data.tar.gz: 72b4e5ba4aadc646d46858808432558a8f7a4aec45ad7274c8ccad6603d485271a75a22d0923d29384eee88a825771592b4191e466bef7a1460f012ebd8bb87a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5 - 2026-09-07
4
+
5
+ Release in lockstep with the Python/PHP publishing workflow fixes and complete
6
+ WooCommerce integration documentation. Shared HTTP golden fixtures now use
7
+ the canonical placeholder definitions. No Ruby public API change.
8
+
9
+ ## 0.4.4 - 2026-09-07
10
+
11
+ Version lockstep with the 0.4.4 npm release (every checkout image now ships
12
+ inside the JavaScript; no asset option). Ruby changes in this gem:
13
+
14
+ - `OpenReceive::Generated` (`lib/openreceive/generated/tables.rb`) is rendered
15
+ from `spec/data/kernel-tables.json` and now also carries the FixedFloat
16
+ status decision table (`spec/data/swap-state-table.json`); engine code
17
+ reads the rendering instead of restating the vocabularies by hand.
18
+ - Four attention reasons no engine has emitted since 0.1.1
19
+ (`provider_order_creation_stale`, `provider_order_creation_failed`,
20
+ `provider_order_creation_needs_reconcile`,
21
+ `provider_order_expires_after_shadow_invoice`) leave the vocabulary;
22
+ `provider_completed_without_wallet_settlement` stays reserved. Wire output
23
+ is unchanged.
24
+ - The bundled agent skill gains references for Django, FastAPI, Fastify,
25
+ Laravel and Next.js beside Node, Rails and BTCPay Server.
26
+
3
27
  ## 0.4.3 - 2026-09-03
4
28
 
5
29
  Version lockstep with the 0.4.3 npm release (container-query checkout
@@ -5,15 +5,19 @@ require "json"
5
5
  require "uri"
6
6
 
7
7
  require_relative "version"
8
+ # Generated kernel vocabularies (spec/data/kernel-tables.json); required here as
9
+ # well as from openreceive.rb because tests and nwc_ruby.rb load core directly.
10
+ require_relative "generated/tables"
8
11
 
9
12
  module OpenReceive
10
13
  NWC_CODE_HELP_URL = "https://openreceive.org/get_a_nwc_code_to_receive_payments"
11
- NWC_METADATA_MAX_BYTES = 3900
12
- MIN_AMOUNT_MSATS = 1000
13
- MAX_AMOUNT_MSATS = 9_007_199_254_740_991
14
- # Mirrors JS OPENRECEIVE_TRANSACTION_PAGE_LIMIT: the page size every
15
- # wallet-history walk requests.
16
- TRANSACTION_PAGE_LIMIT = 20
14
+ # Kernel constants shared with the JS engine and the BTCPay plugin, generated
15
+ # from spec/data/kernel-tables.json into OpenReceive::Generated.
16
+ NWC_METADATA_MAX_BYTES = Generated::NWC_METADATA_MAX_BYTES
17
+ MIN_AMOUNT_MSATS = Generated::MIN_AMOUNT_MSATS
18
+ MAX_AMOUNT_MSATS = Generated::MAX_AMOUNT_MSATS
19
+ # The page size every wallet-history walk requests.
20
+ TRANSACTION_PAGE_LIMIT = Generated::TRANSACTION_PAGE_LIMIT
17
21
  HEX_64_PATTERN = /\A[0-9a-fA-F]{64}\z/
18
22
 
19
23
  class NwcUriParseError < StandardError
@@ -295,14 +299,10 @@ module OpenReceive
295
299
  "#{text[0..query_start]}#{redacted}#{text[query_end..]}"
296
300
  end
297
301
 
298
- # Canonical OpenReceive error codes (mirrors the JS generated contract).
299
- ERROR_CODES = %w[
300
- NOT_IMPLEMENTED RESTRICTED UNAUTHORIZED FORBIDDEN RATE_LIMITED QUOTA_EXCEEDED
301
- INTERNAL UNSUPPORTED_ENCRYPTION
302
- OTHER NOT_FOUND TIMEOUT INVALID_REQUEST WALLET_UNAVAILABLE
303
- INVOICE_EXPIRED UNSUPPORTED_METHOD CONFLICT
304
- ].freeze
305
- RETRYABLE_ERROR_CODES = %w[RATE_LIMITED QUOTA_EXCEEDED TIMEOUT WALLET_UNAVAILABLE INTERNAL].freeze
302
+ # Canonical OpenReceive error codes and the retryable subset, generated from
303
+ # spec/schemas/error.schema.json and spec/data/kernel-tables.json.
304
+ ERROR_CODES = Generated::ERROR_CODES
305
+ RETRYABLE_ERROR_CODES = Generated::RETRYABLE_ERROR_CODES
306
306
  # Wallet/library spellings that map onto canonical codes (mirrors JS
307
307
  # NWC_ERROR_CODE_ALIASES).
308
308
  ERROR_CODE_ALIASES = {
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ # GENERATED FILE — DO NOT EDIT.
4
+ # Source: spec/data/kernel-tables.json, spec/data/swap-state-table.json,
5
+ # spec/schemas/error.schema.json and the OpenAPI document (npm run generate:models).
6
+ # JS twins: packages/js/core/src/generated/contracts.ts,
7
+ # packages/js/node/src/generated/swap-tables.ts and
8
+ # packages/js/node/src/generated/swap-state-table.ts
9
+ # C# twin: packages/dotnet/BTCPayServer.Plugins.OpenReceive/Generated/OpenReceiveTables.cs
10
+ # Every engine reads the same vocabularies from its rendering, so none can drift.
11
+
12
+ module OpenReceive
13
+ module Generated
14
+ HTTP_CONTRACT_VERSION = "0.4.1"
15
+
16
+ ERROR_CODES = [
17
+ "NOT_IMPLEMENTED",
18
+ "RESTRICTED",
19
+ "UNAUTHORIZED",
20
+ "FORBIDDEN",
21
+ "RATE_LIMITED",
22
+ "QUOTA_EXCEEDED",
23
+ "INTERNAL",
24
+ "UNSUPPORTED_ENCRYPTION",
25
+ "OTHER",
26
+ "NOT_FOUND",
27
+ "TIMEOUT",
28
+ "INVALID_REQUEST",
29
+ "WALLET_UNAVAILABLE",
30
+ "INVOICE_EXPIRED",
31
+ "UNSUPPORTED_METHOD",
32
+ "CONFLICT",
33
+ ].freeze
34
+ RETRYABLE_ERROR_CODES = [
35
+ "RATE_LIMITED",
36
+ "QUOTA_EXCEEDED",
37
+ "TIMEOUT",
38
+ "WALLET_UNAVAILABLE",
39
+ "INTERNAL",
40
+ ].freeze
41
+ PAYMENT_STATUSES = [
42
+ "pending",
43
+ "settled",
44
+ "expired",
45
+ "failed",
46
+ "not_found",
47
+ ].freeze
48
+ PAYMENT_HASH_PATTERN = "^[0-9a-f]{64}$"
49
+ MIN_AMOUNT_MSATS = 1000
50
+ MAX_AMOUNT_MSATS = 9_007_199_254_740_991
51
+
52
+ NWC_REQUIRED_RECEIVE_METHODS = [
53
+ "make_invoice",
54
+ "list_transactions",
55
+ ].freeze
56
+ NWC_SPEND_METHODS = [
57
+ "pay_invoice",
58
+ "multi_pay_invoice",
59
+ "pay_keysend",
60
+ "multi_pay_keysend",
61
+ ].freeze
62
+ # Preference order: the first mode the wallet advertises wins.
63
+ NWC_ENCRYPTION_MODES = [
64
+ "nip44_v2",
65
+ "nip04",
66
+ ].freeze
67
+ NWC_NOTIFICATION_TYPES = [
68
+ "payment_received",
69
+ ].freeze
70
+ NWC_METADATA_MAX_BYTES = 3900
71
+ # The page size every wallet-history walk requests.
72
+ TRANSACTION_PAGE_LIMIT = 20
73
+
74
+ # Seconds past an attempt's expiry during which reconciliation still scans
75
+ # for a settlement before closing the attempt.
76
+ ATTEMPT_EXPIRY_GRACE_SECONDS = 900
77
+
78
+ SWAP_PAY_IN_ASSETS = [
79
+ "SOL_SOL",
80
+ "USDT_TRON",
81
+ "USDT_SOL",
82
+ "USDC_SOL",
83
+ "ETH_ETH",
84
+ "USDT_ETH",
85
+ "USDC_ETH",
86
+ ].freeze
87
+ SWAP_ASSET_INFO = {
88
+ "SOL_SOL" => {
89
+ "pay_in_asset" => "SOL_SOL", "label" => "SOL",
90
+ "network_label" => "Solana", "coin" => "SOL",
91
+ "network" => "SOL"
92
+ }.freeze,
93
+ "USDT_TRON" => {
94
+ "pay_in_asset" => "USDT_TRON", "label" => "USDT",
95
+ "network_label" => "Tron", "coin" => "USDT",
96
+ "network" => "TRX"
97
+ }.freeze,
98
+ "USDT_SOL" => {
99
+ "pay_in_asset" => "USDT_SOL", "label" => "USDT",
100
+ "network_label" => "Solana", "coin" => "USDT",
101
+ "network" => "SOL"
102
+ }.freeze,
103
+ "USDC_SOL" => {
104
+ "pay_in_asset" => "USDC_SOL", "label" => "USDC",
105
+ "network_label" => "Solana", "coin" => "USDC",
106
+ "network" => "SOL"
107
+ }.freeze,
108
+ "ETH_ETH" => {
109
+ "pay_in_asset" => "ETH_ETH", "label" => "ETH",
110
+ "network_label" => "Ethereum", "coin" => "ETH",
111
+ "network" => "ETH"
112
+ }.freeze,
113
+ "USDT_ETH" => {
114
+ "pay_in_asset" => "USDT_ETH", "label" => "USDT",
115
+ "network_label" => "Ethereum", "coin" => "USDT",
116
+ "network" => "ETH"
117
+ }.freeze,
118
+ "USDC_ETH" => {
119
+ "pay_in_asset" => "USDC_ETH", "label" => "USDC",
120
+ "network_label" => "Ethereum", "coin" => "USDC",
121
+ "network" => "ETH"
122
+ }.freeze,
123
+ }.freeze
124
+
125
+ # phase: coarse UI bucket; terminal: the attempt will not change again.
126
+ # "completed" is deliberately NOT terminal: provider completion is not
127
+ # wallet settlement.
128
+ SWAP_STATES = {
129
+ "creating_provider_order" => { "phase" => "preparing", "terminal" => false }.freeze,
130
+ "awaiting_deposit" => { "phase" => "awaiting_deposit", "terminal" => false }.freeze,
131
+ "confirming" => { "phase" => "processing", "terminal" => false }.freeze,
132
+ "exchanging" => { "phase" => "processing", "terminal" => false }.freeze,
133
+ "paying_invoice" => { "phase" => "processing", "terminal" => false }.freeze,
134
+ "completed" => { "phase" => "settling", "terminal" => false }.freeze,
135
+ "expired" => { "phase" => "terminal", "terminal" => true }.freeze,
136
+ "refund_required" => { "phase" => "refund", "terminal" => false }.freeze,
137
+ "refund_pending" => { "phase" => "refund", "terminal" => false }.freeze,
138
+ "refunded" => { "phase" => "terminal", "terminal" => true }.freeze,
139
+ "attention" => { "phase" => "attention", "terminal" => true }.freeze,
140
+ "failed" => { "phase" => "terminal", "terminal" => true }.freeze,
141
+ }.freeze
142
+ SWAP_PROVIDER_STATES = SWAP_STATES.keys.freeze
143
+
144
+ SWAP_ATTENTION_REASONS = [
145
+ "provider_reported_emergency",
146
+ "provider_status_unrecognized",
147
+ "provider_completed_without_wallet_settlement",
148
+ ].freeze
149
+ SWAP_REFUND_REASONS = [
150
+ "underpaid",
151
+ "overpaid",
152
+ "late_deposit",
153
+ "underpaid_and_late",
154
+ "overpaid_and_late",
155
+ ].freeze
156
+ SWAP_AVAILABILITY_REASONS = [
157
+ "provider_unconfigured",
158
+ "amount_too_small",
159
+ "amount_too_large",
160
+ "pair_temporarily_unavailable",
161
+ "region_unsupported",
162
+ "provider_rate_limited",
163
+ "provider_unreachable",
164
+ ].freeze
165
+
166
+ # spec/data/swap-state-table.json: the FixedFloat status → state/reason decision
167
+ # table. Ordered, first-match-wins; the last row is a catch-all. "status"
168
+ # is the upper-cased provider status or "*" (narrowed by "status_contains");
169
+ # "refund_tx_present" is true, false or "any"; "choice" is the upper-cased
170
+ # emergency choice, "absent" or "any". How to read it lives once, in the
171
+ # JSON's how_to_read. Pinned by spec/test-vectors/swap-state.json.
172
+ SWAP_STATUS_ROWS = [
173
+ {
174
+ "status" => "DONE",
175
+ "refund_tx_present" => true,
176
+ "choice" => "any",
177
+ "state" => "refunded",
178
+ "refund_reason_from_emergency" => false
179
+ }.freeze,
180
+ {
181
+ "status" => "FINISHED",
182
+ "refund_tx_present" => true,
183
+ "choice" => "any",
184
+ "state" => "refunded",
185
+ "refund_reason_from_emergency" => false
186
+ }.freeze,
187
+ {
188
+ "status" => "NEW",
189
+ "refund_tx_present" => "any",
190
+ "choice" => "any",
191
+ "state" => "awaiting_deposit",
192
+ "refund_reason_from_emergency" => false
193
+ }.freeze,
194
+ {
195
+ "status" => "PENDING",
196
+ "refund_tx_present" => "any",
197
+ "choice" => "any",
198
+ "state" => "confirming",
199
+ "refund_reason_from_emergency" => false
200
+ }.freeze,
201
+ {
202
+ "status" => "EXCHANGE",
203
+ "refund_tx_present" => "any",
204
+ "choice" => "any",
205
+ "state" => "exchanging",
206
+ "refund_reason_from_emergency" => false
207
+ }.freeze,
208
+ {
209
+ "status" => "WITHDRAW",
210
+ "refund_tx_present" => "any",
211
+ "choice" => "any",
212
+ "state" => "paying_invoice",
213
+ "refund_reason_from_emergency" => false
214
+ }.freeze,
215
+ {
216
+ "status" => "DONE",
217
+ "refund_tx_present" => "any",
218
+ "choice" => "any",
219
+ "state" => "completed",
220
+ "refund_reason_from_emergency" => false
221
+ }.freeze,
222
+ {
223
+ "status" => "EXPIRED",
224
+ "refund_tx_present" => "any",
225
+ "choice" => "any",
226
+ "state" => "expired",
227
+ "refund_reason_from_emergency" => false
228
+ }.freeze,
229
+ {
230
+ "status" => "EMERGENCY",
231
+ "refund_tx_present" => true,
232
+ "choice" => "REFUND",
233
+ "state" => "refunded",
234
+ "refund_reason_from_emergency" => true
235
+ }.freeze,
236
+ {
237
+ "status" => "EMERGENCY",
238
+ "refund_tx_present" => "any",
239
+ "choice" => "REFUND",
240
+ "state" => "refund_pending",
241
+ "refund_reason_from_emergency" => true
242
+ }.freeze,
243
+ {
244
+ "status" => "EMERGENCY",
245
+ "refund_tx_present" => "any",
246
+ "choice" => "EXCHANGE",
247
+ "state" => "attention",
248
+ "attention_reason" => "provider_reported_emergency",
249
+ "refund_reason_from_emergency" => false
250
+ }.freeze,
251
+ {
252
+ "status" => "EMERGENCY",
253
+ "refund_tx_present" => "any",
254
+ "choice" => "any",
255
+ "state" => "refund_required",
256
+ "refund_reason_from_emergency" => true
257
+ }.freeze,
258
+ {
259
+ "status" => "*",
260
+ "status_contains" => "FAIL",
261
+ "refund_tx_present" => "any",
262
+ "choice" => "any",
263
+ "state" => "failed",
264
+ "refund_reason_from_emergency" => false
265
+ }.freeze,
266
+ {
267
+ "status" => "*",
268
+ "refund_tx_present" => "any",
269
+ "choice" => "any",
270
+ "state" => "attention",
271
+ "attention_reason" => "provider_status_unrecognized",
272
+ "refund_reason_from_emergency" => false
273
+ }.freeze,
274
+ ].freeze
275
+ # Emergency status spellings folded onto their canonical name before matching.
276
+ SWAP_EMERGENCY_STATUS_ALIASES = {
277
+ "OVER" => "MORE",
278
+ "OVERPAID" => "MORE",
279
+ }.freeze
280
+ # Ordered; a row matches when every "all_of" status is present. No match,
281
+ # no refund_reason.
282
+ SWAP_REFUND_REASON_ROWS = [
283
+ { "all_of" => ["LESS", "EXPIRED"].freeze, "refund_reason" => "underpaid_and_late" }.freeze,
284
+ { "all_of" => ["MORE", "EXPIRED"].freeze, "refund_reason" => "overpaid_and_late" }.freeze,
285
+ { "all_of" => ["LESS"].freeze, "refund_reason" => "underpaid" }.freeze,
286
+ { "all_of" => ["MORE"].freeze, "refund_reason" => "overpaid" }.freeze,
287
+ { "all_of" => ["EXPIRED"].freeze, "refund_reason" => "late_deposit" }.freeze,
288
+ ].freeze
289
+ end
290
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenReceive
4
- VERSION = "0.4.3"
4
+ VERSION = "0.4.5"
5
5
  end
data/lib/openreceive.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "openreceive/version"
4
+ # The kernel vocabularies every engine shares, generated from spec/ (npm run
5
+ # generate:models). Loaded first: core and the server gem alias its constants.
6
+ require_relative "openreceive/generated/tables"
4
7
  require_relative "openreceive/core"
5
8
  require_relative "openreceive/nwc_ruby"
6
9
  require_relative "openreceive/rates"
@@ -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,25 @@ 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
+ - WordPress + WooCommerce: [references/woocommerce.md](references/woocommerce.md) — the packaged gateway and merchant settings.
34
+ - BTCPay Server: [references/btcpay.md](references/btcpay.md) — a plugin,
35
+ configured in BTCPay's store UI or Greenfield API; no application code,
36
+ no npm packages, no gem. The rest of this file is about the library.
28
37
  3. Follow its **Step 0** first: confirm `NWC_URI` is set in the server
29
38
  environment before writing code. Never print the value; never invent a
30
39
  placeholder.
31
40
 
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`.
41
+ Install, per adapter — Express: `npm install @openreceive/express @openreceive/react`;
42
+ Fastify: `npm install @openreceive/fastify @openreceive/react`; Next.js:
43
+ `npm install @openreceive/next @openreceive/react`. Swap the UI package (`vue`,
44
+ `svelte`, `angular`, `elements`) for the frontend the app already has. Install
45
+ (Rails): `bundle add openreceive-rails`.
35
46
 
36
47
  ## The three server objects
37
48
 
@@ -0,0 +1,193 @@
1
+ # OpenReceive agent directions (BTCPay Server)
2
+
3
+ These directions describe OpenReceive 0.4.5.
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.