openreceive 0.4.4__py3-none-any.whl

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.
Files changed (93) hide show
  1. openreceive/__init__.py +27 -0
  2. openreceive/_generated/fulfillment_note.py +59 -0
  3. openreceive/_generated/tables.py +426 -0
  4. openreceive/_version.py +4 -0
  5. openreceive/cli.py +598 -0
  6. openreceive/django/__init__.py +13 -0
  7. openreceive/django/admin.py +59 -0
  8. openreceive/django/apps.py +21 -0
  9. openreceive/django/checks.py +75 -0
  10. openreceive/django/conf.py +203 -0
  11. openreceive/django/management/__init__.py +0 -0
  12. openreceive/django/management/commands/__init__.py +0 -0
  13. openreceive/django/management/commands/openreceive_doctor.py +70 -0
  14. openreceive/django/management/commands/openreceive_install.py +211 -0
  15. openreceive/django/management/commands/openreceive_notifications.py +62 -0
  16. openreceive/django/management/commands/openreceive_reconcile.py +20 -0
  17. openreceive/django/migrations/0001_initial.py +91 -0
  18. openreceive/django/migrations/__init__.py +0 -0
  19. openreceive/django/models.py +111 -0
  20. openreceive/django/repository.py +404 -0
  21. openreceive/django/static/openreceive/MANIFEST.json +20 -0
  22. openreceive/django/static/openreceive/openreceive-checkout.css +4 -0
  23. openreceive/django/static/openreceive/openreceive-checkout.js +628 -0
  24. openreceive/django/static/openreceive/openreceive-checkout.js.map +7 -0
  25. openreceive/django/urls.py +23 -0
  26. openreceive/django/views.py +129 -0
  27. openreceive/fastapi/__init__.py +10 -0
  28. openreceive/fastapi/binding.py +216 -0
  29. openreceive/fastapi/lifespan.py +69 -0
  30. openreceive/fastapi/requests.py +71 -0
  31. openreceive/fastapi/router.py +127 -0
  32. openreceive/money.py +76 -0
  33. openreceive/nwc/__init__.py +39 -0
  34. openreceive/nwc/client.py +39 -0
  35. openreceive/nwc/errors.py +196 -0
  36. openreceive/nwc/info.py +77 -0
  37. openreceive/nwc/receive_client.py +103 -0
  38. openreceive/nwc/requests.py +169 -0
  39. openreceive/nwc/transport/__init__.py +6 -0
  40. openreceive/nwc/transport/errors.py +25 -0
  41. openreceive/nwc/transport/nip01.py +86 -0
  42. openreceive/nwc/transport/nip04.py +34 -0
  43. openreceive/nwc/transport/nip44.py +94 -0
  44. openreceive/nwc/transport/receive_client.py +271 -0
  45. openreceive/nwc/transport/relay.py +90 -0
  46. openreceive/nwc/uri.py +105 -0
  47. openreceive/payments/__init__.py +10 -0
  48. openreceive/payments/reconciliation.py +49 -0
  49. openreceive/payments/scan.py +105 -0
  50. openreceive/rates/__init__.py +194 -0
  51. openreceive/rates/cached_feed.py +204 -0
  52. openreceive/rates/simple_price.py +69 -0
  53. openreceive/rates/static.py +26 -0
  54. openreceive/server/__init__.py +31 -0
  55. openreceive/server/app.py +208 -0
  56. openreceive/server/client_ip.py +67 -0
  57. openreceive/server/doctor.py +150 -0
  58. openreceive/server/errors.py +184 -0
  59. openreceive/server/handler.py +827 -0
  60. openreceive/server/hosts.py +84 -0
  61. openreceive/server/notifications.py +100 -0
  62. openreceive/server/rate_limit.py +76 -0
  63. openreceive/server/reconcile.py +351 -0
  64. openreceive/server/service.py +795 -0
  65. openreceive/settlement.py +48 -0
  66. openreceive/storage/__init__.py +49 -0
  67. openreceive/storage/repository.py +310 -0
  68. openreceive/storage/sql/__init__.py +16 -0
  69. openreceive/storage/sql/ddl.py +68 -0
  70. openreceive/storage/sql/repository.py +471 -0
  71. openreceive/storage/sql/tables.py +134 -0
  72. openreceive/swap/__init__.py +55 -0
  73. openreceive/swap/address.py +112 -0
  74. openreceive/swap/assets.py +46 -0
  75. openreceive/swap/base58.py +23 -0
  76. openreceive/swap/budget.py +94 -0
  77. openreceive/swap/cache.py +148 -0
  78. openreceive/swap/fixedfloat.py +873 -0
  79. openreceive/swap/http.py +53 -0
  80. openreceive/swap/keccak.py +81 -0
  81. openreceive/swap/lsc_uri.py +105 -0
  82. openreceive/swap/rates_feed.py +271 -0
  83. openreceive/swap/state.py +79 -0
  84. openreceive/testing/__init__.py +9 -0
  85. openreceive/testing/fake_swap_provider.py +251 -0
  86. openreceive/testing/fake_wallet.py +221 -0
  87. openreceive/testing/fixtures.py +42 -0
  88. openreceive/values.py +65 -0
  89. openreceive-0.4.4.dist-info/METADATA +77 -0
  90. openreceive-0.4.4.dist-info/RECORD +93 -0
  91. openreceive-0.4.4.dist-info/WHEEL +4 -0
  92. openreceive-0.4.4.dist-info/entry_points.txt +2 -0
  93. openreceive-0.4.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,27 @@
1
+ """OpenReceive for Python: receive-only Lightning checkout over NWC.
2
+
3
+ The kernel (money, settlement, NIP-47 normalization, the wallet walk, the
4
+ closure decision, swaps) is pure functions over dicts; the server
5
+ (`openreceive.server`) binds it to a wallet client, a price feed and a payment
6
+ repository, and `openreceive.django` / `openreceive.fastapi` mount the
7
+ framework-free handler. This module re-exports only the kernel entry points
8
+ hosts reach for directly.
9
+ """
10
+
11
+ from openreceive._version import __version__
12
+ from openreceive.money import quote_fiat_to_msats
13
+ from openreceive.nwc.uri import NWC_CODE_HELP_URL, NwcUriParseError, parse_uri, redact_uri
14
+ from openreceive.settlement import is_settled
15
+
16
+ parse_nwc_uri = parse_uri
17
+ redact_nwc_uri = redact_uri
18
+
19
+ __all__ = [
20
+ "NWC_CODE_HELP_URL",
21
+ "NwcUriParseError",
22
+ "__version__",
23
+ "is_settled",
24
+ "parse_nwc_uri",
25
+ "quote_fiat_to_msats",
26
+ "redact_nwc_uri",
27
+ ]
@@ -0,0 +1,59 @@
1
+ """GENERATED FILE — DO NOT EDIT.
2
+
3
+ Source: spec/data/fulfillment-note.txt (npm run generate:models).
4
+ Twins: packages/js/core/src/generated/fulfillment-note-text.ts, packages/ruby/openreceive-rails/lib/openreceive/generated/fulfillment_note.rb,
5
+ packages/php/openreceive/src/Generated/FulfillmentNote.php; all render the same text, so no host scaffold can
6
+ give different advice.
7
+ """
8
+
9
+ from typing import Final
10
+
11
+ # The note's lines, with "{{table}}" awaiting the caller's table name.
12
+ FULFILLMENT_NOTE_TEMPLATE: Final = (
13
+ "Fulfilling exactly once",
14
+ "",
15
+ "WHAT OPENRECEIVE GUARANTEES",
16
+ "",
17
+ "Across every settlement path OpenReceive itself owns (wallet notifications,",
18
+ "the opportunistic reconcile pass, an explicit reconcile job), the settlement",
19
+ "hook runs AT MOST ONCE per reference. The library serializes on its own",
20
+ "`{{table}}` rows, decides the winner there, and runs your hook",
21
+ "inside that same transaction. A second payment to a second invoice for the",
22
+ "same order is still recorded - with `status_reason = 'duplicate_settlement'`",
23
+ "- but never fulfills a second time. You do not need to add a lock for this.",
24
+ "",
25
+ "That makes the reference the unit of fulfillment: give every payable order",
26
+ "its own reference, created before checkout, kept across retries, and never",
27
+ "reused. A new checkout under a reference that has already settled is refused",
28
+ "with a 409 rather than fulfilled again; a fresh reference per page load",
29
+ "leaves one order payable twice.",
30
+ "",
31
+ "WHAT YOU MUST GUARANTEE",
32
+ "",
33
+ "OpenReceive cannot see fulfillment that happens outside it. If ANY other",
34
+ "path can also mark this order fulfilled - an admin action, a second payment",
35
+ "processor, a support tool, a replayed webhook, a retried background job -",
36
+ "then those paths race each other, not OpenReceive, and you must make",
37
+ "fulfillment idempotent yourself.",
38
+ "",
39
+ "The usual way is to make the transition itself the lock: guard it with a",
40
+ "conditional write that only one transaction can win.",
41
+ "",
42
+ " -- Idempotent by construction: the WHERE clause is the guard. Whoever",
43
+ " -- flips 'awaiting_payment' -> 'paid' first is the only one who fulfills;",
44
+ " -- every later attempt updates 0 rows and must do nothing.",
45
+ " UPDATE orders",
46
+ " SET state = 'paid', paid_at = :paid_at",
47
+ " WHERE id = :reference",
48
+ " AND state = 'awaiting_payment';",
49
+ " -- then: if 0 rows were affected, return without shipping anything.",
50
+ "",
51
+ "If your fulfillment is a read-modify-write that cannot be expressed as one",
52
+ "conditional UPDATE, take a row lock for the duration instead:",
53
+ "",
54
+ " SELECT * FROM orders WHERE id = :reference FOR UPDATE; -- postgres/mysql",
55
+ " -- ...check state, ship, write the new state, all before COMMIT.",
56
+ "",
57
+ "Run either one inside the transaction OpenReceive hands your settlement",
58
+ "hook, so the order transition and the payment record commit together.",
59
+ )
@@ -0,0 +1,426 @@
1
+ """GENERATED FILE — DO NOT EDIT.
2
+
3
+ Source: spec/data/kernel-tables.json, spec/data/swap-state-table.json,
4
+ spec/schemas/error.schema.json, the OpenAPI and AsyncAPI documents
5
+ (npm run generate:models). Twins: packages/ruby/openreceive/lib/openreceive/generated/tables.rb,
6
+ packages/dotnet/BTCPayServer.Plugins.OpenReceive/Generated/OpenReceiveTables.cs,
7
+ packages/php/openreceive/src/Generated/Tables.php.
8
+ Every engine reads the same vocabularies from its rendering, so none can drift.
9
+
10
+ The closed vocabularies and fixed numbers every OpenReceive engine shares, plus
11
+ the FixedFloat status decision table. Read these; never restate them.
12
+ """
13
+
14
+ from typing import Final
15
+
16
+ # The OpenAPI info.version this engine was built from.
17
+ HTTP_CONTRACT_VERSION: Final = "0.4.1"
18
+
19
+ # The AsyncAPI info.version this engine was built from.
20
+ EVENT_CONTRACT_VERSION: Final = "0.2.0"
21
+
22
+ ERROR_CODES: Final = (
23
+ "NOT_IMPLEMENTED",
24
+ "RESTRICTED",
25
+ "UNAUTHORIZED",
26
+ "FORBIDDEN",
27
+ "RATE_LIMITED",
28
+ "QUOTA_EXCEEDED",
29
+ "INTERNAL",
30
+ "UNSUPPORTED_ENCRYPTION",
31
+ "OTHER",
32
+ "NOT_FOUND",
33
+ "TIMEOUT",
34
+ "INVALID_REQUEST",
35
+ "WALLET_UNAVAILABLE",
36
+ "INVOICE_EXPIRED",
37
+ "UNSUPPORTED_METHOD",
38
+ "CONFLICT",
39
+ )
40
+
41
+ RETRYABLE_ERROR_CODES: Final = (
42
+ "RATE_LIMITED",
43
+ "QUOTA_EXCEEDED",
44
+ "TIMEOUT",
45
+ "WALLET_UNAVAILABLE",
46
+ "INTERNAL",
47
+ )
48
+
49
+ PAYMENT_STATUSES: Final = (
50
+ "pending",
51
+ "settled",
52
+ "expired",
53
+ "failed",
54
+ "not_found",
55
+ )
56
+
57
+ PAYMENT_HASH_PATTERN: Final = "^[0-9a-f]{64}$"
58
+
59
+ MIN_AMOUNT_MSATS: Final = 1000
60
+
61
+ MAX_AMOUNT_MSATS: Final = 9007199254740991
62
+
63
+ NWC_REQUIRED_RECEIVE_METHODS: Final = (
64
+ "make_invoice",
65
+ "list_transactions",
66
+ )
67
+
68
+ NWC_SPEND_METHODS: Final = (
69
+ "pay_invoice",
70
+ "multi_pay_invoice",
71
+ "pay_keysend",
72
+ "multi_pay_keysend",
73
+ )
74
+
75
+ # Preference order: the first mode the wallet advertises wins.
76
+ NWC_ENCRYPTION_MODES: Final = (
77
+ "nip44_v2",
78
+ "nip04",
79
+ )
80
+
81
+ NWC_NOTIFICATION_TYPES: Final = (
82
+ "payment_received",
83
+ )
84
+
85
+ NWC_METADATA_MAX_BYTES: Final = 3900
86
+
87
+ # The page size every wallet-history walk requests.
88
+ TRANSACTION_PAGE_LIMIT: Final = 20
89
+
90
+ # Seconds past an attempt's expiry during which reconciliation still scans
91
+ # for a settlement before closing the attempt.
92
+ ATTEMPT_EXPIRY_GRACE_SECONDS: Final = 900
93
+
94
+ SWAP_PAY_IN_ASSETS: Final = (
95
+ "SOL_SOL",
96
+ "USDT_TRON",
97
+ "USDT_SOL",
98
+ "USDC_SOL",
99
+ "ETH_ETH",
100
+ "USDT_ETH",
101
+ "USDC_ETH",
102
+ )
103
+
104
+ SWAP_ASSET_INFO: Final = {
105
+ "SOL_SOL": {
106
+ "pay_in_asset": "SOL_SOL",
107
+ "label": "SOL",
108
+ "network_label": "Solana",
109
+ "coin": "SOL",
110
+ "network": "SOL",
111
+ },
112
+ "USDT_TRON": {
113
+ "pay_in_asset": "USDT_TRON",
114
+ "label": "USDT",
115
+ "network_label": "Tron",
116
+ "coin": "USDT",
117
+ "network": "TRX",
118
+ },
119
+ "USDT_SOL": {
120
+ "pay_in_asset": "USDT_SOL",
121
+ "label": "USDT",
122
+ "network_label": "Solana",
123
+ "coin": "USDT",
124
+ "network": "SOL",
125
+ },
126
+ "USDC_SOL": {
127
+ "pay_in_asset": "USDC_SOL",
128
+ "label": "USDC",
129
+ "network_label": "Solana",
130
+ "coin": "USDC",
131
+ "network": "SOL",
132
+ },
133
+ "ETH_ETH": {
134
+ "pay_in_asset": "ETH_ETH",
135
+ "label": "ETH",
136
+ "network_label": "Ethereum",
137
+ "coin": "ETH",
138
+ "network": "ETH",
139
+ },
140
+ "USDT_ETH": {
141
+ "pay_in_asset": "USDT_ETH",
142
+ "label": "USDT",
143
+ "network_label": "Ethereum",
144
+ "coin": "USDT",
145
+ "network": "ETH",
146
+ },
147
+ "USDC_ETH": {
148
+ "pay_in_asset": "USDC_ETH",
149
+ "label": "USDC",
150
+ "network_label": "Ethereum",
151
+ "coin": "USDC",
152
+ "network": "ETH",
153
+ },
154
+ }
155
+
156
+ # phase: coarse UI bucket; terminal: the attempt will not change again.
157
+ # "completed" is deliberately NOT terminal: provider completion is not wallet
158
+ # settlement.
159
+ SWAP_STATES: Final = {
160
+ "creating_provider_order": {
161
+ "phase": "preparing",
162
+ "terminal": False,
163
+ },
164
+ "awaiting_deposit": {
165
+ "phase": "awaiting_deposit",
166
+ "terminal": False,
167
+ },
168
+ "confirming": {
169
+ "phase": "processing",
170
+ "terminal": False,
171
+ },
172
+ "exchanging": {
173
+ "phase": "processing",
174
+ "terminal": False,
175
+ },
176
+ "paying_invoice": {
177
+ "phase": "processing",
178
+ "terminal": False,
179
+ },
180
+ "completed": {
181
+ "phase": "settling",
182
+ "terminal": False,
183
+ },
184
+ "expired": {
185
+ "phase": "terminal",
186
+ "terminal": True,
187
+ },
188
+ "refund_required": {
189
+ "phase": "refund",
190
+ "terminal": False,
191
+ },
192
+ "refund_pending": {
193
+ "phase": "refund",
194
+ "terminal": False,
195
+ },
196
+ "refunded": {
197
+ "phase": "terminal",
198
+ "terminal": True,
199
+ },
200
+ "attention": {
201
+ "phase": "attention",
202
+ "terminal": True,
203
+ },
204
+ "failed": {
205
+ "phase": "terminal",
206
+ "terminal": True,
207
+ },
208
+ }
209
+
210
+ SWAP_PROVIDER_STATES: Final = (
211
+ "creating_provider_order",
212
+ "awaiting_deposit",
213
+ "confirming",
214
+ "exchanging",
215
+ "paying_invoice",
216
+ "completed",
217
+ "expired",
218
+ "refund_required",
219
+ "refund_pending",
220
+ "refunded",
221
+ "attention",
222
+ "failed",
223
+ )
224
+
225
+ SWAP_ATTENTION_REASONS: Final = (
226
+ "provider_reported_emergency",
227
+ "provider_status_unrecognized",
228
+ "provider_completed_without_wallet_settlement",
229
+ )
230
+
231
+ SWAP_REFUND_REASONS: Final = (
232
+ "underpaid",
233
+ "overpaid",
234
+ "late_deposit",
235
+ "underpaid_and_late",
236
+ "overpaid_and_late",
237
+ )
238
+
239
+ SWAP_AVAILABILITY_REASONS: Final = (
240
+ "provider_unconfigured",
241
+ "amount_too_small",
242
+ "amount_too_large",
243
+ "pair_temporarily_unavailable",
244
+ "region_unsupported",
245
+ "provider_rate_limited",
246
+ "provider_unreachable",
247
+ )
248
+
249
+ # spec/data/swap-state-table.json: ordered, first-match-wins; the last row is a
250
+ # catch-all. "status" is the upper-cased provider status or "*" (narrowed by
251
+ # "status_contains"); "refund_tx_present" is True, False or "any"; "choice" is
252
+ # the upper-cased emergency choice, "absent" or "any". A non-None
253
+ # "attention_reason" means the result also carries attention: True. Pinned by
254
+ # spec/test-vectors/swap-state.json; how to read it lives once, in the JSON's
255
+ # how_to_read.
256
+ SWAP_STATUS_ROWS: Final = (
257
+ {
258
+ "status": "DONE",
259
+ "status_contains": None,
260
+ "refund_tx_present": True,
261
+ "choice": "any",
262
+ "state": "refunded",
263
+ "attention_reason": None,
264
+ "refund_reason_from_emergency": False,
265
+ },
266
+ {
267
+ "status": "FINISHED",
268
+ "status_contains": None,
269
+ "refund_tx_present": True,
270
+ "choice": "any",
271
+ "state": "refunded",
272
+ "attention_reason": None,
273
+ "refund_reason_from_emergency": False,
274
+ },
275
+ {
276
+ "status": "NEW",
277
+ "status_contains": None,
278
+ "refund_tx_present": "any",
279
+ "choice": "any",
280
+ "state": "awaiting_deposit",
281
+ "attention_reason": None,
282
+ "refund_reason_from_emergency": False,
283
+ },
284
+ {
285
+ "status": "PENDING",
286
+ "status_contains": None,
287
+ "refund_tx_present": "any",
288
+ "choice": "any",
289
+ "state": "confirming",
290
+ "attention_reason": None,
291
+ "refund_reason_from_emergency": False,
292
+ },
293
+ {
294
+ "status": "EXCHANGE",
295
+ "status_contains": None,
296
+ "refund_tx_present": "any",
297
+ "choice": "any",
298
+ "state": "exchanging",
299
+ "attention_reason": None,
300
+ "refund_reason_from_emergency": False,
301
+ },
302
+ {
303
+ "status": "WITHDRAW",
304
+ "status_contains": None,
305
+ "refund_tx_present": "any",
306
+ "choice": "any",
307
+ "state": "paying_invoice",
308
+ "attention_reason": None,
309
+ "refund_reason_from_emergency": False,
310
+ },
311
+ {
312
+ "status": "DONE",
313
+ "status_contains": None,
314
+ "refund_tx_present": "any",
315
+ "choice": "any",
316
+ "state": "completed",
317
+ "attention_reason": None,
318
+ "refund_reason_from_emergency": False,
319
+ },
320
+ {
321
+ "status": "EXPIRED",
322
+ "status_contains": None,
323
+ "refund_tx_present": "any",
324
+ "choice": "any",
325
+ "state": "expired",
326
+ "attention_reason": None,
327
+ "refund_reason_from_emergency": False,
328
+ },
329
+ {
330
+ "status": "EMERGENCY",
331
+ "status_contains": None,
332
+ "refund_tx_present": True,
333
+ "choice": "REFUND",
334
+ "state": "refunded",
335
+ "attention_reason": None,
336
+ "refund_reason_from_emergency": True,
337
+ },
338
+ {
339
+ "status": "EMERGENCY",
340
+ "status_contains": None,
341
+ "refund_tx_present": "any",
342
+ "choice": "REFUND",
343
+ "state": "refund_pending",
344
+ "attention_reason": None,
345
+ "refund_reason_from_emergency": True,
346
+ },
347
+ {
348
+ "status": "EMERGENCY",
349
+ "status_contains": None,
350
+ "refund_tx_present": "any",
351
+ "choice": "EXCHANGE",
352
+ "state": "attention",
353
+ "attention_reason": "provider_reported_emergency",
354
+ "refund_reason_from_emergency": False,
355
+ },
356
+ {
357
+ "status": "EMERGENCY",
358
+ "status_contains": None,
359
+ "refund_tx_present": "any",
360
+ "choice": "any",
361
+ "state": "refund_required",
362
+ "attention_reason": None,
363
+ "refund_reason_from_emergency": True,
364
+ },
365
+ {
366
+ "status": "*",
367
+ "status_contains": "FAIL",
368
+ "refund_tx_present": "any",
369
+ "choice": "any",
370
+ "state": "failed",
371
+ "attention_reason": None,
372
+ "refund_reason_from_emergency": False,
373
+ },
374
+ {
375
+ "status": "*",
376
+ "status_contains": None,
377
+ "refund_tx_present": "any",
378
+ "choice": "any",
379
+ "state": "attention",
380
+ "attention_reason": "provider_status_unrecognized",
381
+ "refund_reason_from_emergency": False,
382
+ },
383
+ )
384
+
385
+ # Emergency status spellings folded onto their canonical name before matching.
386
+ SWAP_EMERGENCY_STATUS_ALIASES: Final = {
387
+ "OVER": "MORE",
388
+ "OVERPAID": "MORE",
389
+ }
390
+
391
+ # Ordered; a row matches when every "all_of" status is present. No match, no
392
+ # refund_reason.
393
+ SWAP_REFUND_REASON_ROWS: Final = (
394
+ {
395
+ "all_of": (
396
+ "LESS",
397
+ "EXPIRED",
398
+ ),
399
+ "refund_reason": "underpaid_and_late",
400
+ },
401
+ {
402
+ "all_of": (
403
+ "MORE",
404
+ "EXPIRED",
405
+ ),
406
+ "refund_reason": "overpaid_and_late",
407
+ },
408
+ {
409
+ "all_of": (
410
+ "LESS",
411
+ ),
412
+ "refund_reason": "underpaid",
413
+ },
414
+ {
415
+ "all_of": (
416
+ "MORE",
417
+ ),
418
+ "refund_reason": "overpaid",
419
+ },
420
+ {
421
+ "all_of": (
422
+ "EXPIRED",
423
+ ),
424
+ "refund_reason": "late_deposit",
425
+ },
426
+ )
@@ -0,0 +1,4 @@
1
+ """The distribution version, written by `release:prepare` from the root
2
+ package.json (PEP 440 at the boundary: `0.5.0-alpha.1` becomes `0.5.0a1`)."""
3
+
4
+ __version__ = "0.4.4"