openreceive-rails 0.4.11 → 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 +4 -4
- data/CHANGELOG.md +19 -0
- data/lib/generators/openreceive/install/templates/initializer.rb +1 -1
- data/lib/openreceive/configuration.rb +1 -1
- data/lib/openreceive/rails/version.rb +1 -1
- data/lib/openreceive/reconcile.rb +11 -6
- data/lib/openreceive/reconcile_scan.rb +15 -2
- data/skills/debug-openreceive-payment/SKILL.md +1 -1
- data/skills/integrate-openreceive/SKILL.md +4 -3
- data/skills/integrate-openreceive/references/btcpay.md +65 -36
- data/skills/integrate-openreceive/references/django.md +288 -243
- data/skills/integrate-openreceive/references/fastapi.md +203 -154
- data/skills/integrate-openreceive/references/fastify.md +197 -146
- data/skills/integrate-openreceive/references/laravel.md +291 -233
- data/skills/integrate-openreceive/references/next.md +207 -161
- data/skills/integrate-openreceive/references/node.md +185 -137
- data/skills/integrate-openreceive/references/php.md +245 -192
- data/skills/integrate-openreceive/references/rails.md +259 -217
- data/skills/integrate-openreceive/references/woocommerce.md +66 -51
- metadata +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenReceive agent directions (Django)
|
|
2
2
|
|
|
3
|
-
These directions describe OpenReceive 0.4.
|
|
3
|
+
These directions describe OpenReceive 0.4.13.
|
|
4
4
|
|
|
5
5
|
Add OpenReceive to a Django project — the app you are already working in. You
|
|
6
6
|
do not need a copy of the OpenReceive source: the Python package is on PyPI
|
|
@@ -37,78 +37,101 @@ The one required credential is a receive-only NWC code (Nostr Wallet Connect):
|
|
|
37
37
|
a string from the merchant's wallet that can create invoices and read their
|
|
38
38
|
status, and cannot spend. A swap provider (an "LSC" code) optionally lets the
|
|
39
39
|
payer send USDT, USDC, ETH or SOL instead, converted into that same
|
|
40
|
-
Lightning payment.
|
|
40
|
+
Lightning payment. Step 0 collects those credentials from the user; you write three hooks — `authorize`,
|
|
41
41
|
`amount_for`, `on_paid` on a `Host` class named in `settings.OPENRECEIVE`;
|
|
42
42
|
OpenReceive supplies invoices, polling, settlement and the checkout UI. It never
|
|
43
43
|
owns orders, users, prices, or fulfillment.
|
|
44
44
|
|
|
45
|
-
## Step 0 —
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
>
|
|
74
|
-
>
|
|
75
|
-
>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
45
|
+
## Step 0 — ask for the two codes, one question at a time
|
|
46
|
+
|
|
47
|
+
Before anything else, look at one file: the project's `.env`, if it exists.
|
|
48
|
+
Read it only far enough to see whether `NWC_URI` and `LSC_URI_PRIMARY` are
|
|
49
|
+
there and non-empty, and never print the values. A code that is already
|
|
50
|
+
set is not asked for again; if both are set, skip to step 5
|
|
51
|
+
(making the server load the file), then the quickstart.
|
|
52
|
+
|
|
53
|
+
Otherwise your next action is a question to the user. Do not install the package,
|
|
54
|
+
edit the app, write `.env.example`, or search anywhere else before asking
|
|
55
|
+
it. Do not read deploy config (compose files, platform secrets), `settings.py` secrets, Docker containers, or process environments, and never run
|
|
56
|
+
`printenv`. A new shop has neither code yet: hunting for one burns the turn and
|
|
57
|
+
can leak other secrets into the transcript.
|
|
58
|
+
|
|
59
|
+
Two server-only URIs are needed before the quickstart:
|
|
60
|
+
|
|
61
|
+
- `NWC_URI` — a receive-only Nostr Wallet Connect code,
|
|
62
|
+
`nostr+walletconnect://…`. Required for Bitcoin.
|
|
63
|
+
- `LSC_URI_PRIMARY` — a Lightning Swap Connect URI,
|
|
64
|
+
`lightning+swapconnect://…`. Required for USDT, USDC, ETH and SOL. Skip it
|
|
65
|
+
only when the user says they want Bitcoin alone.
|
|
66
|
+
|
|
67
|
+
The user never edits an environment file. They paste each code into the chat;
|
|
68
|
+
you store it. Ask one question per message.
|
|
69
|
+
|
|
70
|
+
1. **First message — the NWC code, and nothing else.** Ask for it and walk them
|
|
71
|
+
through getting it:
|
|
72
|
+
|
|
73
|
+
> To receive payments I need a receive-only wallet code. In Rizful: open
|
|
74
|
+
> the menu, tap NWC, choose Receive-only NWC code, and tap Copy
|
|
75
|
+
> (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would
|
|
76
|
+
> rather run your own wallet, Alby Hub works too: Connections → Add
|
|
77
|
+
> Connection → Read Only. Paste the code here and I will store it.
|
|
78
|
+
|
|
79
|
+
Do not mention `.env`, exports, or "tell me when it's set".
|
|
80
|
+
2. **When they paste it.** If it does not start with `nostr+walletconnect://`,
|
|
81
|
+
ask them to copy the receive-only code again. Otherwise write
|
|
82
|
+
`NWC_URI=<paste>` into the project's `.env`, creating the file if needed.
|
|
83
|
+
Make sure `.gitignore` covers `.env` (and `.dockerignore`, if the app has
|
|
84
|
+
one). Never echo the value, commit it, or put it in client code. Reply only
|
|
85
|
+
that it is saved, then ask the next question.
|
|
86
|
+
3. **Second message — swaps.** If the user already asked for stablecoins or
|
|
87
|
+
altcoins, skip the yes/no and go straight to the walkthrough. Otherwise ask
|
|
88
|
+
whether payers should also be able to pay with USDT, USDC, ETH or SOL. The
|
|
89
|
+
walkthrough:
|
|
90
|
+
|
|
91
|
+
> Go to https://lightning-swap.com, sign in for API keys, create a key, and
|
|
92
|
+
> copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste
|
|
93
|
+
> it here and I will store it — or say "Bitcoin only" and I will continue
|
|
94
|
+
> without it.
|
|
95
|
+
|
|
96
|
+
Mention FixedFloat only if they already use it.
|
|
97
|
+
4. **When they paste it.** If it does not start with
|
|
98
|
+
`lightning+swapconnect://`, ask them to copy it again. Otherwise add
|
|
99
|
+
`LSC_URI_PRIMARY=<paste>` to the same `.env`, without echoing it. Swaps
|
|
100
|
+
are now on, so build the refund route back (the swap non-negotiable below) as
|
|
101
|
+
part of this integration. If they chose Bitcoin only, leave
|
|
102
|
+
`LSC_URI_PRIMARY` unset and skip that route.
|
|
103
|
+
5. **Make the server load the file — yourself.** Django does not
|
|
104
|
+
read `.env` on its own. If the app already uses `django-environ`, make sure
|
|
105
|
+
it reads this file; otherwise add it and call `environ.Env.read_env()` in
|
|
106
|
+
`settings.py` before anything reads the environment. When the app is started
|
|
107
|
+
with Docker Compose, give the service `env_file: .env`. Restart the server
|
|
108
|
+
after writing the file.
|
|
109
|
+
|
|
110
|
+
Do not invent placeholder URIs. Start the quickstart only once `NWC_URI` is
|
|
111
|
+
saved and `LSC_URI_PRIMARY` is saved or explicitly declined. The first boot
|
|
112
|
+
runs the receive-only preflight: if it reports spend methods such as
|
|
113
|
+
`pay_invoice`, remove `NWC_URI` from `.env` and ask for a receive-only code
|
|
114
|
+
again. Never set the spend-capable override to get past it.
|
|
115
|
+
|
|
116
|
+
Before the deploy that turns this on, run
|
|
117
|
+
`OPENRECEIVE_PREFLIGHT=1 manage.py check --deploy`: it runs the receive-only
|
|
118
|
+
wallet preflight as a system check (`openreceive.E002` on a missing, dead or
|
|
119
|
+
spend-capable code), because the wallet client is built lazily on the first
|
|
120
|
+
request rather than in `AppConfig.ready()`.
|
|
121
|
+
|
|
122
|
+
### Upgrading an existing install
|
|
123
|
+
|
|
124
|
+
This is not the opening move of a new integration. When OpenReceive is ALREADY
|
|
125
|
+
installed here, `manage.py openreceive_doctor` reports every credential as
|
|
126
|
+
present or missing (never the value), the wallet preflight, the two tables, the
|
|
127
|
+
mount, and any hook still on a placeholder; `--offline` skips the relay probe.
|
|
128
|
+
Check the installed `openreceive` (`pip show openreceive`) and
|
|
129
|
+
`@openreceive/browser` (or the `MANIFEST.json` beside the packaged static
|
|
130
|
+
checkout) against the release named at the top of this file: the headless
|
|
131
|
+
display models below do not exist in older versions, and the first tile click
|
|
132
|
+
throws with nothing saying why. Upgrade first — and if this app runs in
|
|
133
|
+
containers, rebuild the images: the package is baked into the image, so an
|
|
134
|
+
in-place `pip install -U` is undone by the next `compose up`.
|
|
112
135
|
|
|
113
136
|
Only then start the quickstart.
|
|
114
137
|
|
|
@@ -326,11 +349,11 @@ Install the Python package with the Django extra:
|
|
|
326
349
|
pip install "openreceive[django]"
|
|
327
350
|
```
|
|
328
351
|
|
|
329
|
-
That is the whole install
|
|
330
|
-
client (websockets, coincurve, cryptography), the
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
352
|
+
That is the whole install. `openreceive[django]` brings Django. The base
|
|
353
|
+
package brings the wallet client (websockets, coincurve, cryptography), the
|
|
354
|
+
HTTP engine and the `openreceive` CLI. Use `uv` or a virtualenv on Python 3.10
|
|
355
|
+
or newer. A system Python 3.9 cannot install it. If your app brings its own NWC
|
|
356
|
+
client, set `OPENRECEIVE["SERVICE"]` instead (see below).
|
|
334
357
|
|
|
335
358
|
Then add the app, point it at a host class, and mount the routes:
|
|
336
359
|
|
|
@@ -353,41 +376,43 @@ from django.urls import include, path
|
|
|
353
376
|
urlpatterns += [path("openreceive/", include("openreceive.django.urls"))]
|
|
354
377
|
```
|
|
355
378
|
|
|
356
|
-
`HOST` is a dotted path, not a callable
|
|
357
|
-
|
|
379
|
+
`HOST` is a dotted path, not a callable. That keeps it cache-safe and friendly
|
|
380
|
+
to `manage.py check`, like the `AUTH_USER_MODEL` setting. Then run:
|
|
358
381
|
|
|
359
382
|
```sh
|
|
360
383
|
manage.py openreceive_install shop # writes shop/openreceive_host.py, prints the lines above
|
|
361
384
|
manage.py migrate # creates openreceive_payments and openreceive_meta
|
|
362
385
|
```
|
|
363
386
|
|
|
364
|
-
`openreceive_install` writes one file
|
|
365
|
-
three hooks with the generated placeholders wired and the exactly-once
|
|
366
|
-
fulfillment note as comments
|
|
367
|
-
It never edits `settings.py` or `urls.py`. The migration ships
|
|
368
|
-
`openreceive.django` app, so `manage.py migrate` applies it
|
|
369
|
-
own
|
|
370
|
-
|
|
371
|
-
The `OpenReceivePayment` model
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
`OPTIONS = {"transaction_mode": "IMMEDIATE"}`
|
|
378
|
-
concurrent commits for one order
|
|
379
|
-
with "database is locked"
|
|
387
|
+
`openreceive_install` writes one file, `<app>/openreceive_host.py`. It holds
|
|
388
|
+
the three hooks with the generated placeholders wired in, and the exactly-once
|
|
389
|
+
fulfillment note as comments. The command then prints the settings and urls
|
|
390
|
+
lines to add. It never edits `settings.py` or `urls.py`. The migration ships
|
|
391
|
+
inside the `openreceive.django` app, so `manage.py migrate` applies it
|
|
392
|
+
alongside your own. It adapts to the configured database backend.
|
|
393
|
+
|
|
394
|
+
The engine owns the `OpenReceivePayment` model, so no model file is generated.
|
|
395
|
+
The migration adds only the engine's two tables to your database. The engine
|
|
396
|
+
owns the table's commit locking, write-once settlement, and reconciliation
|
|
397
|
+
state machine. `reference` is indexed but not unique, because one reference may
|
|
398
|
+
have many historical attempts. `payment_hash` is globally unique.
|
|
399
|
+
|
|
400
|
+
On SQLite, give the database `OPTIONS = {"transaction_mode": "IMMEDIATE"}`
|
|
401
|
+
(Django ≥ 5.1). Then two concurrent commits for one order wait on the busy
|
|
402
|
+
timeout instead of failing with "database is locked"
|
|
403
|
+
([Payment storage](https://openreceive.org/guides/storage.md)).
|
|
380
404
|
|
|
381
405
|
#### Fulfill exactly once
|
|
382
406
|
|
|
383
407
|
Within OpenReceive's own settlement paths, `on_paid` runs at most once per
|
|
384
|
-
reference
|
|
385
|
-
`status_reason = "duplicate_settlement"` and
|
|
408
|
+
reference. If a second invoice for the same reference is paid, OpenReceive
|
|
409
|
+
records that payment with `status_reason = "duplicate_settlement"` and does
|
|
410
|
+
not fulfill again.
|
|
386
411
|
|
|
387
|
-
|
|
388
|
-
an order
|
|
389
|
-
those paths race each other
|
|
390
|
-
host module
|
|
412
|
+
One case is yours to handle. **If anything other than OpenReceive can also
|
|
413
|
+
fulfill an order**, such as an admin action, a second payment processor, or a
|
|
414
|
+
replayed job, those paths race each other. Then `on_paid` must be idempotent.
|
|
415
|
+
The generated host module explains this and shows the guarded transition:
|
|
391
416
|
|
|
392
417
|
```python
|
|
393
418
|
def on_paid(self, settlement: PaymentSettlement) -> None:
|
|
@@ -402,19 +427,20 @@ def on_paid(self, settlement: PaymentSettlement) -> None:
|
|
|
402
427
|
FulfillOrder.run(settlement.reference, payment_hash=settlement.payment_hash)
|
|
403
428
|
```
|
|
404
429
|
|
|
405
|
-
Delivery is at-least-once
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
or webhook sent from here would survive the rollback and go
|
|
409
|
-
`state="paid"` transition above is the flag
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
430
|
+
Delivery is at-least-once. `on_paid` runs inside the settlement transaction,
|
|
431
|
+
which the engine wraps in `transaction.atomic()`. If it raises, the transaction
|
|
432
|
+
rolls back and the next pass retries. So keep `on_paid` to database writes on
|
|
433
|
+
the order. An email or webhook sent from here would survive the rollback and go
|
|
434
|
+
out again. The `state="paid"` transition above is the flag. Drain it after
|
|
435
|
+
commit, either from your own job or from `after_paid`. `after_paid` is the
|
|
436
|
+
optional fourth method. It runs once, after the settlement transaction
|
|
437
|
+
commits.
|
|
438
|
+
|
|
439
|
+
**`QuerySet.update()` fires no signals and calls no `save()`.** That is
|
|
440
|
+
intended. It runs one conditional `UPDATE`, so the claim is atomic and no model
|
|
441
|
+
code runs between the check and the write. It also means no `post_save`
|
|
442
|
+
handler runs. That is fine for a job that drains the flag. It does not work for
|
|
443
|
+
a model whose transition lives in `save()`. If your model owns the transition
|
|
418
444
|
through signals or an overridden `save()`, take a row lock for the duration
|
|
419
445
|
instead:
|
|
420
446
|
|
|
@@ -428,27 +454,30 @@ def on_paid(self, settlement: PaymentSettlement) -> None:
|
|
|
428
454
|
order.save() # signals fire
|
|
429
455
|
```
|
|
430
456
|
|
|
431
|
-
**Unlocking a download works the same way.** If
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
`get_object_or_404(Order, pk=reference, user=request.user, state="paid")
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
Both shapes are idempotent
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
457
|
+
**Unlocking a download works the same way.** If the payer bought a file, do not
|
|
458
|
+
unlock it in the browser. Gate the download view on the paid order row, and
|
|
459
|
+
serve the file only if that row exists:
|
|
460
|
+
`get_object_or_404(Order, pk=reference, user=request.user, state="paid")`. The
|
|
461
|
+
`state="paid"` written above is the unlock. The client never decides that an
|
|
462
|
+
order was fulfilled. It re-reads the row. Buy a Button's `download` view does
|
|
463
|
+
this in twenty lines.
|
|
464
|
+
|
|
465
|
+
Both shapes are idempotent and correct. They differ only in whether your model
|
|
466
|
+
layer runs:
|
|
467
|
+
|
|
468
|
+
- `update()` skips the model layer. It is the right default.
|
|
469
|
+
- The row lock holds the row for the duration of the method. Use it when the
|
|
470
|
+
transition has to go through your model. On SQLite the lock does nothing,
|
|
471
|
+
because the transaction itself already lets only one writer run at a time.
|
|
472
|
+
|
|
473
|
+
The generated fulfillment note says the same thing. If your fulfillment is a
|
|
474
|
+
read-modify-write that one conditional `UPDATE` cannot express, take the lock.
|
|
475
|
+
|
|
476
|
+
Buy a Button
|
|
477
|
+
(`examples/buttons/server/django`)
|
|
478
|
+
is a runnable illustration of this boundary. It is not a template to copy
|
|
479
|
+
models from. It has products, visitors, and orders, and the three hooks are the
|
|
480
|
+
only bridge. Map that shape onto the models in THIS app.
|
|
452
481
|
|
|
453
482
|
### Add wallet credentials
|
|
454
483
|
|
|
@@ -461,30 +490,36 @@ LSC_URI_BACKUP=
|
|
|
461
490
|
```
|
|
462
491
|
|
|
463
492
|
1. Get a receive-only NWC code from a compatible wallet
|
|
464
|
-
([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments))
|
|
465
|
-
|
|
466
|
-
2.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
`
|
|
475
|
-
|
|
476
|
-
|
|
493
|
+
([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
|
|
494
|
+
Put it in `NWC_URI`.
|
|
495
|
+
2. Optional: set up a [swap provider](https://openreceive.org/set_up_swap_provider).
|
|
496
|
+
Put its connection string in `LSC_URI_PRIMARY`, and a second one in
|
|
497
|
+
`LSC_URI_BACKUP` if you have one.
|
|
498
|
+
|
|
499
|
+
Never put these values in browser code. Your app refuses to start if the NWC
|
|
500
|
+
code also advertises spend methods such as `pay_invoice`. Create a
|
|
501
|
+
receive-only code instead ([Security](https://openreceive.org/guides/security.md)).
|
|
502
|
+
|
|
503
|
+
OpenReceive reads `os.environ`. Django does not load a `.env` file on its own,
|
|
504
|
+
so something has to put the values there first: `django-environ`, an exported
|
|
505
|
+
shell environment, or your production secret manager. To allow a
|
|
506
|
+
spend-capable code explicitly, set `OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC=true`.
|
|
477
507
|
→ [Environment variables](https://openreceive.org/guides/environment-variables.md).
|
|
478
508
|
|
|
479
509
|
### Configure the host hooks
|
|
480
510
|
|
|
481
511
|
The host class needs three things: authorization, the trusted price, and
|
|
482
|
-
fulfillment. All three receive the `reference
|
|
483
|
-
fulfillment identity
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
512
|
+
fulfillment. All three receive the `reference`. This is a string you choose,
|
|
513
|
+
and it is the fulfillment identity. Use your order id:
|
|
514
|
+
|
|
515
|
+
- one per thing you fulfill,
|
|
516
|
+
- created before checkout,
|
|
517
|
+
- kept across retries,
|
|
518
|
+
- never reused.
|
|
519
|
+
|
|
520
|
+
OpenReceive never looks inside it. But `on_paid` commits fulfillment once per
|
|
521
|
+
reference, and a new checkout under a reference that already settled is
|
|
522
|
+
refused with 409. A fresh id per page load would let one order be paid twice.
|
|
488
523
|
|
|
489
524
|
```python
|
|
490
525
|
# shop/openreceive_host.py — generated by `manage.py openreceive_install shop`, then filled in.
|
|
@@ -497,7 +532,6 @@ from shop.models import Order # YOUR model — it could be named anything. Open
|
|
|
497
532
|
# never sees it or touches its table; these hooks are the
|
|
498
533
|
# only bridge between the engine and your data.
|
|
499
534
|
|
|
500
|
-
|
|
501
535
|
class Host:
|
|
502
536
|
# Your policy, called before every checkout/payment/swap request. `context`
|
|
503
537
|
# has three attributes:
|
|
@@ -541,52 +575,59 @@ class Host:
|
|
|
541
575
|
)
|
|
542
576
|
```
|
|
543
577
|
|
|
544
|
-
`authorize` receives the Django request that carried the payer's call
|
|
545
|
-
`request.user`, `request.session` and cookies are all
|
|
546
|
-
project's authentication and its `User
|
|
547
|
-
own. The engine reads nothing else from the request
|
|
548
|
-
stack put on it
|
|
549
|
-
|
|
550
|
-
CSRF stays on. `CsrfViewMiddleware` is
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
578
|
+
`authorize` receives the Django request that carried the payer's call. So
|
|
579
|
+
`request.user`, `request.session` and cookies are all available. Keep your
|
|
580
|
+
project's authentication and its `User`. OpenReceive mints no tokens of its
|
|
581
|
+
own. The engine reads nothing else from the request. Your policy sees whatever
|
|
582
|
+
your middleware stack put on it.
|
|
583
|
+
|
|
584
|
+
CSRF stays on. `CsrfViewMiddleware` is yours, and the mounted routes run its
|
|
585
|
+
check like any other view. A failed check returns the shared `403 FORBIDDEN`
|
|
586
|
+
JSON error instead of the HTML failure page. To make the check pass:
|
|
587
|
+
|
|
588
|
+
- Render `<meta name="csrf-token" content="{{ csrf_token }}">` in the template
|
|
589
|
+
that shows the checkout.
|
|
590
|
+
- Give the element `csrf-header="X-CSRFToken"`. That is the header Django
|
|
591
|
+
reads. The default is Rails' `X-CSRF-Token`.
|
|
592
|
+
|
|
593
|
+
The checkout client then sends the token from that tag on every request. A view
|
|
594
|
+
that renders a page with no form normally needs `{% csrf_token %}` or
|
|
595
|
+
`ensure_csrf_cookie` for the cookie to exist. `{{ csrf_token }}` in the meta
|
|
596
|
+
tag does that on its own.
|
|
597
|
+
|
|
598
|
+
The generated host module ships two placeholders. Replace both, not just
|
|
599
|
+
`on_paid`:
|
|
600
|
+
|
|
601
|
+
- `on_paid = staticmethod(LOGGING_ON_PAID)` only logs the settlement and
|
|
602
|
+
fulfills nothing. Replace it with your real fulfillment (as above). Until you
|
|
603
|
+
do, orders would be recorded as settled without ever being fulfilled, so
|
|
604
|
+
`manage.py check` warns (`openreceive.W001`) at every boot.
|
|
605
|
+
- `authorize = staticmethod(ALLOW_ALL_AUTHORIZE)` allows everything. It treats
|
|
606
|
+
possession of the reference as authorization, which is safe only while
|
|
607
|
+
references are unguessable. The check warns (`openreceive.W002`) until you
|
|
608
|
+
replace it with your own ownership check (as above).
|
|
609
|
+
|
|
610
|
+
A `HOST` that is missing or does not import is `openreceive.E001`.
|
|
611
|
+
|
|
612
|
+
The amount always comes from your own order record. Payer-supplied amounts are
|
|
613
|
+
rejected. If your app brings its own NWC client, price feed or swap providers,
|
|
574
614
|
set `OPENRECEIVE["SERVICE"]` to the dotted path of a callable
|
|
575
|
-
`(env) -> openreceive.server.Service
|
|
576
|
-
|
|
577
|
-
For public web shops,
|
|
578
|
-
`"RATE_LIMITING": True` (or `{"limit_per_hour": …, "limit_per_day": …}`)
|
|
579
|
-
|
|
580
|
-
`REMOTE_ADDR` after your own proxy handling
|
|
581
|
-
trusted-proxy middleware first or the cap counts the proxy
|
|
615
|
+
`(env) -> openreceive.server.Service`. Everyone else leaves it out.
|
|
616
|
+
|
|
617
|
+
For public web shops, turn on the per-IP invoice cap with
|
|
618
|
+
`"RATE_LIMITING": True` (or `{"limit_per_hour": …, "limit_per_day": …}`).
|
|
619
|
+
Leave it off (the default) when many payers share one IP. The cap counts
|
|
620
|
+
`REMOTE_ADDR` after your own proxy handling. Behind a reverse proxy, put a
|
|
621
|
+
trusted-proxy middleware first, or the cap counts the proxy instead of the
|
|
622
|
+
payer.
|
|
582
623
|
→ [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
|
|
583
624
|
|
|
584
|
-
The wallet client
|
|
585
|
-
|
|
586
|
-
`collectstatic` and shells
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
deploy pipeline:
|
|
625
|
+
The wallet client, and its receive-only preflight, is built lazily on the first
|
|
626
|
+
request. It is never built in `AppConfig.ready()`. That method also runs for
|
|
627
|
+
`migrate`, `collectstatic` and shells, and a relay probe there would break them
|
|
628
|
+
on a machine with no relay access. To make a bad `NWC_URI` fail the deploy
|
|
629
|
+
instead of causing 500 errors for customers, run the preflight as a system check
|
|
630
|
+
in your deploy pipeline:
|
|
590
631
|
|
|
591
632
|
```sh
|
|
592
633
|
OPENRECEIVE_PREFLIGHT=1 manage.py check --deploy # openreceive.E002 on a missing, dead or spend-capable code
|
|
@@ -595,16 +636,16 @@ manage.py openreceive_doctor # the same, for humans; neve
|
|
|
595
636
|
|
|
596
637
|
### Render the checkout
|
|
597
638
|
|
|
598
|
-
Serve the compiled `styles.css` without Tailwind processing
|
|
599
|
-
JavaScript (with a CSS-capable bundler) or use a plain
|
|
600
|
-
Do not `@import` it into
|
|
601
|
-
|
|
639
|
+
Serve the compiled `styles.css` without Tailwind processing. Either import it
|
|
640
|
+
from JavaScript (with a CSS-capable bundler) or use a plain
|
|
641
|
+
`<link rel="stylesheet">`. Do not `@import` it into your Tailwind entry. Its
|
|
642
|
+
rules have zero specificity, so your own styles can override checkout styles.
|
|
643
|
+
Scoping does not prevent that.
|
|
602
644
|
|
|
603
|
-
The app serves JSON checkout routes only
|
|
604
|
-
OpenReceive frontend package works against the `/openreceive` mount
|
|
605
|
-
smallest is the custom element
|
|
606
|
-
|
|
607
|
-
bundler at all:
|
|
645
|
+
The app serves JSON checkout routes only. Your template does the rendering. Any
|
|
646
|
+
OpenReceive frontend package works against the `/openreceive` mount. The
|
|
647
|
+
smallest is the custom element. The Python package carries its standalone
|
|
648
|
+
build as static files, so a Django template needs no JavaScript bundler at all:
|
|
608
649
|
|
|
609
650
|
```django
|
|
610
651
|
{# templates/orders/pay.html #}
|
|
@@ -619,34 +660,35 @@ bundler at all:
|
|
|
619
660
|
```
|
|
620
661
|
|
|
621
662
|
`openreceive-checkout.js` registers the `<openreceive-checkout>` tag when it
|
|
622
|
-
loads
|
|
623
|
-
stylesheet is scoped to what OpenReceive renders. `collectstatic` ships both
|
|
624
|
-
static files
|
|
625
|
-
element creates the checkout for `reference`, then
|
|
626
|
-
|
|
663
|
+
loads. It is one self-contained ES module with un-minified identifiers. The
|
|
664
|
+
stylesheet is scoped to what OpenReceive renders. `collectstatic` ships both
|
|
665
|
+
with the rest of your static files. The package's `MANIFEST.json` names every
|
|
666
|
+
file and its hash. The element creates the checkout for `reference`, then
|
|
667
|
+
renders and polls itself. Its default `prefix` is already `/openreceive`.
|
|
627
668
|
|
|
628
669
|
Everything the checkout draws ships inside the JavaScript: the payment-method
|
|
629
670
|
icons, the wallet logos and the pay tutorials. There is no image file to copy
|
|
630
671
|
or serve and no asset option to set. Deploy your normal JavaScript and CSS
|
|
631
672
|
build output, including any generated JavaScript chunks. Bundlers with code
|
|
632
|
-
splitting can
|
|
633
|
-
|
|
634
|
-
Content-Security-Policy has a strict `img-src`, allow `data:`
|
|
673
|
+
splitting can load tutorial screenshots only when a tutorial is first opened.
|
|
674
|
+
Single-file builds, including the standalone checkout, include them upfront. If
|
|
675
|
+
your Content-Security-Policy has a strict `img-src`, allow `data:`
|
|
635
676
|
([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
|
|
636
677
|
|
|
637
|
-
Then open the checkout in a browser
|
|
678
|
+
Then open the checkout in a browser. Confirm the payment-method icons and
|
|
638
679
|
wallet logos render, and open a wallet's pay tutorial to check its screenshots.
|
|
639
|
-
If an image is missing,
|
|
680
|
+
If an image is missing, check the console for CSP violations and the Network
|
|
640
681
|
panel for failed JavaScript chunks. Allow `data:` in `img-src` and deploy the
|
|
641
682
|
complete build output. Do not add image routes, copy package source images, or
|
|
642
683
|
use registry `icon_path` / tutorial `path` keys as browser URLs.
|
|
643
684
|
|
|
644
|
-
|
|
645
|
-
element from `@openreceive/elements` (`defineElements()` once per
|
|
646
|
-
the tag above), or the matching React
|
|
647
|
-
`csrfHeader="X-CSRFToken"` prop
|
|
648
|
-
checkout](https://openreceive.org/guides/frontend-checkout.md)). Build a custom checkout
|
|
649
|
-
cannot use a drop-in
|
|
685
|
+
If your app has a JavaScript bundler, use the npm packages instead. Either use
|
|
686
|
+
the same element from `@openreceive/elements` (call `defineElements()` once per
|
|
687
|
+
page, then use the tag above), or use the matching React, Vue, Svelte, or
|
|
688
|
+
Angular wrapper with the `csrfHeader="X-CSRFToken"` prop. Both have the same
|
|
689
|
+
defaults ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)). Build a custom checkout
|
|
690
|
+
only if this app cannot use a drop-in. In that case
|
|
691
|
+
`@openreceive/browser/headless` is the API
|
|
650
692
|
([Headless checkout](https://openreceive.org/guides/headless-checkout.md)).
|
|
651
693
|
|
|
652
694
|
### Reconciliation
|
|
@@ -662,35 +704,38 @@ load:
|
|
|
662
704
|
manage.py openreceive_notifications
|
|
663
705
|
```
|
|
664
706
|
|
|
665
|
-
|
|
666
|
-
reconcile pass
|
|
667
|
-
15)
|
|
668
|
-
|
|
707
|
+
The worker listens for NWC-02 `payment_received` notifications. The same
|
|
708
|
+
process also runs a periodic reconcile pass, every
|
|
709
|
+
`OPENRECEIVE_NOTIFICATIONS_RECONCILE_INTERVAL_SECONDS` (default 15). That pass
|
|
710
|
+
catches notifications missed while the worker was down. Run one process in
|
|
711
|
+
total, not one per web instance.
|
|
669
712
|
|
|
670
|
-
|
|
671
|
-
drive a pass yourself.
|
|
713
|
+
To run a pass yourself, use the one-shot `manage.py openreceive_reconcile`.
|
|
672
714
|
|
|
673
715
|
### Swap secrets
|
|
674
716
|
|
|
675
|
-
The Python engine recognizes `LSC_URI_PRIMARY` and `LSC_URI_BACKUP
|
|
676
|
-
shared [Lightning Swap Connect](https://openreceive.org/guides/lightning-swap-connect.md) vectors
|
|
677
|
-
either one auto-builds the matching provider
|
|
717
|
+
The Python engine recognizes `LSC_URI_PRIMARY` and `LSC_URI_BACKUP`, using the
|
|
718
|
+
shared [Lightning Swap Connect](https://openreceive.org/guides/lightning-swap-connect.md) vectors. Setting
|
|
719
|
+
either one auto-builds the matching provider. So an app that wants swaps only
|
|
678
720
|
supplies the connection strings ([Environment
|
|
679
|
-
variables](https://openreceive.org/guides/environment-variables.md)).
|
|
680
|
-
|
|
681
|
-
to disable swaps.
|
|
721
|
+
variables](https://openreceive.org/guides/environment-variables.md)). To override this, use
|
|
722
|
+
`OPENRECEIVE["SERVICE"]`. Build the `Service` with your own providers, or with
|
|
723
|
+
an empty list to disable swaps.
|
|
682
724
|
|
|
683
|
-
One `openreceive_payments` row holds at most one provider order in its
|
|
684
|
-
server-only `swap_data` column. The engine
|
|
685
|
-
model's `repr`,
|
|
686
|
-
dict. Do not serialize it, log it, or return it from your own API
|
|
687
|
-
contain a provider credential.
|
|
725
|
+
One `openreceive_payments` row holds at most one provider order, in its
|
|
726
|
+
server-only `swap_data` column. The engine leaves `swap_data` out of the
|
|
727
|
+
model's `repr`, out of the read-only admin it registers, and out of every
|
|
728
|
+
public dict. Do not serialize it, log it, or return it from your own API. It
|
|
729
|
+
may contain a provider credential.
|
|
688
730
|
|
|
689
731
|
**Setting either connection string commits you to refunds.** A swap deposit can
|
|
690
|
-
arrive short or late
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
732
|
+
arrive short or late. The provider then marks it `refund_required`, and only
|
|
733
|
+
your UI can claim it. The payer claims it on a second visit, after leaving your
|
|
734
|
+
page to get an address in another wallet. That needs three things:
|
|
735
|
+
|
|
736
|
+
- a per-order URL your app serves,
|
|
737
|
+
- a route that restores the order behind it,
|
|
738
|
+
- something that restores the ATTEMPT, since `/checkouts/prepare` returns none.
|
|
739
|
+
|
|
740
|
+
[Swap refunds](https://openreceive.org/guides/swap-refunds.md) covers all of it. Read it before you set
|
|
696
741
|
`LSC_URI_PRIMARY`.
|