fleetmesh-node 0.2.0__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 (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,541 @@
1
+ """Live Payment Gateways: NIP-47 (Nostr Wallet Connect / NWC) & Cashu HTTP Mint Client.
2
+
3
+ Integrates real external settlement rails with MeshNode:
4
+ 1. NIP-47 Nostr Wallet Connect (NWC):
5
+ - URI parser: nostr+walletconnect://<wallet_pubkey>?relay=<url>&secret=<secret_hex>&lud16=<lud16>
6
+ - Dual-mode encryption: NIP-04 (AES-256-CBC) and NIP-44 v2 (ChaCha20-Poly1305).
7
+ - Methods: pay_invoice, make_invoice, lookup_invoice, get_balance.
8
+ - `NWCLightningPaymentRail`: Implements PaymentRail for automated compute settlement via Lightning.
9
+ 2. Cashu Mint HTTP Client (NUT-00, NUT-03, NUT-04, NUT-05):
10
+ - Standard library urllib client for v1 Cashu mint REST API.
11
+ - Quote generation, token minting, token melting (ecash-to-Lightning), and token swapping (NUT-03).
12
+ - `LiveCashuPaymentRail`: Extends CashuPaymentRail with live mint validation and atomic swaps.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from fleetmesh_core.wire import spelling # noqa: E402
17
+
18
+ import base64
19
+ import json
20
+ import os
21
+ import threading
22
+ import time
23
+ from typing import Any, Mapping, Optional, Sequence, TypedDict, cast
24
+ from urllib.parse import parse_qs, urlparse
25
+
26
+ from cryptography.hazmat.primitives import padding
27
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
28
+
29
+ from fleetmesh_core.accounting.cashu import (
30
+ BlindedMessage, CashuProof, CheckStateResult,
31
+ MeltQuoteResult, MintQuoteResult, SwapResult,
32
+ hash_to_curve, point_to_hex,
33
+ )
34
+ from fleetmesh_core.crypto import crypto, curve, nip44
35
+ from fleetmesh_core.storage import store as store_mod
36
+ from fleetmesh_core.wire.interfaces import NostrEvent
37
+ from . import payment
38
+ from node.transport import netfetch
39
+ from node.protocols.nostr import client as relay
40
+
41
+ DEFAULT_MINT_URL: str = 'https://mint.example.org'
42
+ DEFAULT_RELAY_URL: str = 'wss://relay.example.org'
43
+ # A Cashu mint reply is JSON of a few KiB; 4 MiB leaves room for large
44
+ # keyset listings while bounding what a broken mint can push at us.
45
+ MINT_MAX_RESPONSE_BYTES: int = 4 * 1024 * 1024
46
+
47
+ # NIP-47 Kind Numbers
48
+ NWC_REQUEST_KIND: int = 23194
49
+ NWC_RESPONSE_KIND: int = 23195
50
+
51
+
52
+ class NwcPayResult(TypedDict, total=False):
53
+ preimage: str
54
+ fees_paid: int
55
+
56
+
57
+ class NwcMakeInvoiceResult(TypedDict, total=False):
58
+ invoice: str
59
+ payment_hash: str
60
+ amount: int
61
+ expiry: int
62
+
63
+
64
+ class NwcLookupInvoiceResult(TypedDict, total=False):
65
+ type: str
66
+ invoice: str
67
+ description: str
68
+ description_hash: Optional[str]
69
+ preimage: Optional[str]
70
+ payment_hash: str
71
+ amount: int
72
+ fees_paid: int
73
+ created_at: int
74
+ expires_at: int
75
+ settled_at: Optional[int]
76
+
77
+
78
+ class NwcBalanceResult(TypedDict, total=False):
79
+ balance: int
80
+
81
+
82
+ class MintKeyset(TypedDict, total=False):
83
+ id: str
84
+ unit: str
85
+ active: bool
86
+ keys: dict[str, str]
87
+
88
+
89
+ class MintKeysResult(TypedDict, total=False):
90
+ keysets: list[MintKeyset]
91
+
92
+
93
+ class MintInfoResult(TypedDict, total=False):
94
+ name: str
95
+ pubkey: str
96
+ version: str
97
+ description: str
98
+ description_long: str
99
+ contact: list[list[str]]
100
+ nuts: Mapping[str, Any]
101
+
102
+
103
+ class NwcPayParams(TypedDict, total=False):
104
+ invoice: str
105
+ amount: int
106
+
107
+
108
+ class NwcLookupParams(TypedDict, total=False):
109
+ payment_hash: str
110
+ invoice: str
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # NIP-04 / NIP-44 Encryption Utilities
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def nip04_encrypt(plaintext: str | bytes, sec_hex: str, pub_hex: str) -> str:
118
+ """Encrypt payload using standard NIP-04 (AES-256-CBC with PKCS#7 padding)."""
119
+ shared_x = bytes.fromhex(curve.ecdh_x(sec_hex, pub_hex))
120
+ iv = os.urandom(16)
121
+ data = plaintext.encode('utf-8') if isinstance(plaintext, str) else plaintext
122
+
123
+ padder = padding.PKCS7(128).padder()
124
+ padded_data = padder.update(data) + padder.finalize()
125
+
126
+ cipher = Cipher(algorithms.AES(shared_x), modes.CBC(iv))
127
+ encryptor = cipher.encryptor()
128
+ ct = encryptor.update(padded_data) + encryptor.finalize()
129
+
130
+ ct_b64 = base64.b64encode(ct).decode('ascii')
131
+ iv_b64 = base64.b64encode(iv).decode('ascii')
132
+ return f"{ct_b64}?iv={iv_b64}"
133
+
134
+
135
+ def nip04_decrypt(payload: str, sec_hex: str, pub_hex: str) -> str:
136
+ """Decrypt payload using standard NIP-04 (AES-256-CBC with PKCS#7 unpadding)."""
137
+ parts = payload.split('?iv=')
138
+ if len(parts) != 2:
139
+ raise ValueError("invalid NIP-04 payload: missing ?iv= delimiter")
140
+
141
+ ct = base64.b64decode(parts[0])
142
+ iv = base64.b64decode(parts[1])
143
+
144
+ shared_x = bytes.fromhex(curve.ecdh_x(sec_hex, pub_hex))
145
+ cipher = Cipher(algorithms.AES(shared_x), modes.CBC(iv))
146
+ decryptor = cipher.decryptor()
147
+ padded_data = decryptor.update(ct) + decryptor.finalize()
148
+
149
+ unpadder = padding.PKCS7(128).unpadder()
150
+ data = unpadder.update(padded_data) + unpadder.finalize()
151
+ return data.decode('utf-8')
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # NIP-47 (NWC) Client
156
+ # ---------------------------------------------------------------------------
157
+
158
+ class NWCError(Exception):
159
+ pass
160
+
161
+
162
+ class NWCUri:
163
+ """NIP-47 nostr+walletconnect:// URI representation."""
164
+
165
+ def __init__(self, uri: str) -> None:
166
+ raw = uri.strip()
167
+ if raw.startswith('nostrwalletconnect://'):
168
+ raw = 'nostr+walletconnect://' + raw[len('nostrwalletconnect://'):]
169
+ if not raw.startswith('nostr+walletconnect://'):
170
+ raise ValueError("URI must begin with nostr+walletconnect://")
171
+
172
+ parsed = urlparse(raw)
173
+ self.wallet_pubkey: str = (parsed.netloc or parsed.path.lstrip('/')).lower()
174
+ if not spelling.is_hex(self.wallet_pubkey, 64):
175
+ raise ValueError(f"invalid 64-hex wallet pubkey: {self.wallet_pubkey}")
176
+
177
+ qs = parse_qs(parsed.query)
178
+ self.relay: str = qs.get('relay', [''])[0]
179
+ self.secret: str = qs.get('secret', [''])[0].lower()
180
+ self.lud16: str = qs.get('lud16', [''])[0]
181
+
182
+ if not spelling.is_hex(self.secret, 64):
183
+ raise ValueError("secret must be a 64-hex secp256k1 private key")
184
+
185
+ self.client_pubkey: str = crypto.pubkey(self.secret)
186
+
187
+
188
+ class NWCClient:
189
+ """NIP-47 Nostr Wallet Connect client."""
190
+
191
+ def __init__(
192
+ self,
193
+ uri: str,
194
+ timeout: float = 10.0,
195
+ encryption_mode: str = 'nip04', # 'nip04' or 'nip44'
196
+ ) -> None:
197
+ self.uri_info: NWCUri = NWCUri(uri)
198
+ self.timeout: float = timeout
199
+ self.encryption_mode: str = encryption_mode
200
+ self._lock: threading.RLock = threading.RLock()
201
+
202
+ @property
203
+ def wallet_pubkey(self) -> str:
204
+ return self.uri_info.wallet_pubkey
205
+
206
+ @property
207
+ def client_pubkey(self) -> str:
208
+ return self.uri_info.client_pubkey
209
+
210
+ @property
211
+ def secret(self) -> str:
212
+ return self.uri_info.secret
213
+
214
+ @property
215
+ def relay_url(self) -> str:
216
+ return self.uri_info.relay
217
+
218
+ def _encrypt(self, plaintext: str) -> str:
219
+ if self.encryption_mode == 'nip44':
220
+ conv = nip44.conversation_key(self.secret, self.wallet_pubkey)
221
+ return nip44.encrypt(plaintext, conv)
222
+ return nip04_encrypt(plaintext, self.secret, self.wallet_pubkey)
223
+
224
+ def _decrypt(self, ciphertext: str) -> str:
225
+ if self.encryption_mode == 'nip44':
226
+ conv = nip44.conversation_key(self.secret, self.wallet_pubkey)
227
+ return nip44.decrypt(ciphertext, conv)
228
+ return nip04_decrypt(ciphertext, self.secret, self.wallet_pubkey)
229
+
230
+ def call_method(self, method: str, params: Optional[Mapping[str, Any]] = None) -> Mapping[str, Any]:
231
+ """Send NIP-47 request (Kind 23194) and wait for response (Kind 23195)."""
232
+ req_payload = {
233
+ 'method': method,
234
+ 'params': params or {},
235
+ }
236
+ json_body = json.dumps(req_payload, separators=(',', ':'))
237
+ ciphertext = self._encrypt(json_body)
238
+
239
+ now = int(time.time())
240
+ tags = [['p', self.wallet_pubkey]]
241
+ req_event = store_mod.make_signed_event(
242
+ self.secret,
243
+ now,
244
+ NWC_REQUEST_KIND,
245
+ tags,
246
+ ciphertext,
247
+ )
248
+ req_id = req_event['id']
249
+
250
+ if not self.relay_url:
251
+ raise NWCError("no relay URL configured in NWC URI")
252
+
253
+ # Publish request to relay and listen for response
254
+ r = relay.Relay(self.relay_url, timeout=self.timeout)
255
+ try:
256
+ r.connect()
257
+
258
+ # Subscribe to response first so ephemeral response is never missed
259
+ f = {
260
+ 'kinds': [NWC_RESPONSE_KIND],
261
+ 'authors': [self.wallet_pubkey],
262
+ '#p': [self.client_pubkey],
263
+ '#e': [req_id],
264
+ }
265
+ sub_id = r.subscribe([f], ready=True)
266
+
267
+ # Publish request to relay
268
+ r.publish(req_event)
269
+
270
+ resp_event: Optional[NostrEvent] = None
271
+ deadline = time.time() + self.timeout
272
+
273
+ for ev in r.events(sub_id, until=deadline):
274
+ if ev.get('kind') == NWC_RESPONSE_KIND:
275
+ tags_map = {t[0]: t[1] for t in ev.get('tags', []) if len(t) > 1}
276
+ if tags_map.get('e') == req_id:
277
+ resp_event = ev
278
+ break
279
+
280
+ if resp_event is None:
281
+ raise NWCError(f"timeout waiting for NWC response (method {method!r})")
282
+
283
+ # Decrypt response content
284
+ raw_resp = self._decrypt(resp_event.get('content', ''))
285
+ resp_data = json.loads(raw_resp)
286
+
287
+ if resp_data.get('error'):
288
+ err = resp_data['error']
289
+ msg_err = err.get('message') or err.get('code') or str(err)
290
+ raise NWCError(f"NWC error: {msg_err}")
291
+
292
+ return resp_data.get('result', {})
293
+
294
+ finally:
295
+ try:
296
+ r.close()
297
+ except OSError:
298
+ pass # best-effort close of a relay socket that may already be gone
299
+
300
+ def pay_invoice(self, invoice: str, amount_msat: Optional[int] = None) -> NwcPayResult:
301
+ """Pay a Bolt11 Lightning invoice via NWC. Returns {'preimage': '...', 'fees_paid': ...}."""
302
+ params: NwcPayParams = {'invoice': invoice}
303
+ if amount_msat:
304
+ params['amount'] = amount_msat
305
+ return cast(NwcPayResult, self.call_method('pay_invoice', params))
306
+
307
+ def make_invoice(self, amount_msat: int, description: str = '', expiry: int = 3600) -> NwcMakeInvoiceResult:
308
+ """Generate a Bolt11 Lightning invoice via NWC. Returns {'invoice': '...', 'payment_hash': '...'}."""
309
+ params = {
310
+ 'amount': amount_msat,
311
+ 'description': description,
312
+ 'expiry': expiry,
313
+ }
314
+ return cast(NwcMakeInvoiceResult, self.call_method('make_invoice', params))
315
+
316
+ def lookup_invoice(self, payment_hash: Optional[str] = None, invoice: Optional[str] = None) -> NwcLookupInvoiceResult:
317
+ """Check status of invoice via payment_hash or bolt11 string."""
318
+ params: NwcLookupParams = {}
319
+ if payment_hash:
320
+ params['payment_hash'] = payment_hash
321
+ if invoice:
322
+ params['invoice'] = invoice
323
+ return cast(NwcLookupInvoiceResult, self.call_method('lookup_invoice', params))
324
+
325
+ def get_balance(self) -> NwcBalanceResult:
326
+ """Query wallet balance in msats. Returns {'balance': int}."""
327
+ return cast(NwcBalanceResult, self.call_method('get_balance'))
328
+
329
+
330
+ # ---------------------------------------------------------------------------
331
+ # NWC Payment Rail Adapter
332
+ # ---------------------------------------------------------------------------
333
+
334
+ class NWCLightningPaymentRail(payment.PaymentRail):
335
+ """PaymentRail adapter backed by a live Nostr Wallet Connect (NWC) gateway.
336
+ The escrow state machine is the base class's; this rail's own I/O is
337
+ asking the wallet for an invoice and paying out over NWC."""
338
+
339
+ rail_id: str = 'nwc'
340
+
341
+ def __init__(self, nwc_client: NWCClient) -> None:
342
+ super().__init__()
343
+ self.client: NWCClient = nwc_client
344
+
345
+ def create_hold_invoice(
346
+ self,
347
+ amount_msat: int,
348
+ payment_hash: str,
349
+ expiry: int = 3600,
350
+ description: str = 'fleetmesh-nwc-compute',
351
+ ) -> payment.HoldInvoice:
352
+ """Request a wallet invoice and register the hold escrow keyed to payment_hash.
353
+
354
+ A wallet failure raises NWCError and registers nothing: an escrow
355
+ nobody can pay must not sit in CREATED looking like one awaiting
356
+ payment (the old code swallowed the error and stored invoice=None).
357
+ """
358
+ inv = self._new_invoice(amount_msat, payment_hash, expiry, description)
359
+ try:
360
+ res = self.client.make_invoice(amount_msat=amount_msat, description=description, expiry=expiry)
361
+ except NWCError:
362
+ raise
363
+ except Exception as e:
364
+ raise NWCError(f'make_invoice failed: {e}') from e
365
+ if not res.get('invoice'):
366
+ raise NWCError('make_invoice returned no invoice')
367
+ inv.invoice = res.get('invoice')
368
+ if res.get('payment_hash'):
369
+ inv.wallet_payment_hash = res['payment_hash'].lower()
370
+ return self._register(inv)
371
+
372
+ def pay_and_settle(self, payment_hash: str, bolt11: str) -> NwcPayResult:
373
+ """Trigger outbound Lightning payment via NWC and unlock escrow with returned preimage."""
374
+ h = payment_hash.lower()
375
+ res = self.client.pay_invoice(bolt11)
376
+ preimage = res.get('preimage', '')
377
+ if preimage:
378
+ self.settle_payment(h, preimage)
379
+ return res
380
+
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # Cashu HTTP Mint REST Client (NUT-00, NUT-03, NUT-04, NUT-05)
384
+ # ---------------------------------------------------------------------------
385
+
386
+ class CashuMintError(Exception):
387
+ pass
388
+
389
+
390
+ class CashuHttpClient:
391
+ """REST Client for standard Cashu mints (v1 API)."""
392
+
393
+ def __init__(self, mint_url: str = DEFAULT_MINT_URL, timeout: float = 10.0) -> None:
394
+ self.mint_url: str = mint_url.rstrip('/')
395
+ self.timeout: float = timeout
396
+
397
+ def _request(self, method: str, endpoint: str, body: Optional[Mapping[str, Any]] = None) -> Mapping[str, Any]:
398
+ url = f"{self.mint_url}{endpoint}"
399
+ data = json.dumps(body).encode('utf-8') if body is not None else None
400
+ headers = {'Content-Type': 'application/json'} if body is not None else {}
401
+
402
+ # The mint URL is operator configuration, never taken from a peer or
403
+ # a job, so the address policy is off (allow_private=True): many
404
+ # operators run their own mint on loopback or a LAN box. The byte cap
405
+ # and scheme check still apply -- a mint answer is small JSON.
406
+ try:
407
+ resp_bytes, _ = netfetch.fetch(
408
+ url,
409
+ max_bytes=MINT_MAX_RESPONSE_BYTES,
410
+ timeout=self.timeout,
411
+ method=method,
412
+ data=data,
413
+ headers=headers,
414
+ allow_private=True,
415
+ )
416
+ except netfetch.FetchHTTPError as e:
417
+ err_text = e.body.decode('utf-8', errors='replace')
418
+ try:
419
+ err_json = json.loads(err_text)
420
+ detail = err_json.get('detail') or err_json.get('error') or err_text
421
+ except Exception:
422
+ detail = err_text
423
+ raise CashuMintError(f"mint HTTP {e.code}: {detail}")
424
+ except netfetch.FetchError as e:
425
+ raise CashuMintError(f"mint connection error: {e}")
426
+ if not resp_bytes:
427
+ return {}
428
+ try:
429
+ return json.loads(resp_bytes.decode('utf-8'))
430
+ except (ValueError, UnicodeDecodeError) as e:
431
+ raise CashuMintError(f"mint returned malformed JSON: {e}")
432
+
433
+ def get_info(self) -> MintInfoResult:
434
+ """GET /v1/info: Mint capabilities and supported NUTs."""
435
+ return cast(MintInfoResult, self._request('GET', '/v1/info'))
436
+
437
+ def get_keys(self) -> MintKeysResult:
438
+ """GET /v1/keys: Mint active keysets."""
439
+ return cast(MintKeysResult, self._request('GET', '/v1/keys'))
440
+
441
+ def request_mint_quote(self, amount_sat: int, unit: str = 'sat') -> MintQuoteResult:
442
+ """POST /v1/mint/quote/bolt11: Request quote to deposit satoshis."""
443
+ return cast(MintQuoteResult, self._request('POST', '/v1/mint/quote/bolt11', {'amount': amount_sat, 'unit': unit}))
444
+
445
+ def check_mint_quote(self, quote_id: str) -> MintQuoteResult:
446
+ """GET /v1/mint/quote/bolt11/{quote_id}: Check if quote was paid."""
447
+ return cast(MintQuoteResult, self._request('GET', f"/v1/mint/quote/bolt11/{quote_id}"))
448
+
449
+ def mint_tokens(self, quote_id: str, outputs: Sequence[BlindedMessage]) -> SwapResult:
450
+ """POST /v1/mint/bolt11: Issue blinded signatures after quote payment."""
451
+ return cast(SwapResult, self._request('POST', '/v1/mint/bolt11', {'quote': quote_id, 'outputs': list(outputs)}))
452
+
453
+ def request_melt_quote(self, invoice: str, unit: str = 'sat') -> MeltQuoteResult:
454
+ """POST /v1/melt/quote/bolt11: Quote to melt ecash into paying a Lightning invoice."""
455
+ return cast(MeltQuoteResult, self._request('POST', '/v1/melt/quote/bolt11', {'request': invoice, 'unit': unit}))
456
+
457
+ def melt_tokens(self, quote_id: str, inputs: Sequence[CashuProof]) -> MeltQuoteResult:
458
+ """POST /v1/melt/bolt11: Spend ecash proofs to pay a Lightning invoice."""
459
+ return cast(MeltQuoteResult, self._request('POST', '/v1/melt/bolt11', {'quote': quote_id, 'inputs': list(inputs)}))
460
+
461
+ def swap_tokens(self, inputs: Sequence[CashuProof], outputs: Sequence[BlindedMessage]) -> SwapResult:
462
+ """POST /v1/swap: Atomic swap of input proofs for fresh output blinded secrets."""
463
+ return cast(SwapResult, self._request('POST', '/v1/swap', {'inputs': list(inputs), 'outputs': list(outputs)}))
464
+
465
+ def check_state(self, ys: Sequence[str]) -> CheckStateResult:
466
+ """POST /v1/checkstate (NUT-07): the mint's own word on whether each
467
+ proof, named by Y = hash_to_curve(secret), is UNSPENT, PENDING or SPENT.
468
+ A proof the mint does not know is reported SPENT-equivalent by most
469
+ implementations; either way it is not UNSPENT and is not payment."""
470
+ return cast(CheckStateResult, self._request('POST', '/v1/checkstate', {'Ys': list(ys)}))
471
+
472
+ def proofs_unspent(self, proofs: Sequence[CashuProof]) -> bool:
473
+ """`ProofVerifier` for `verify_nutzap_event`: every proof is UNSPENT at the mint."""
474
+ ys = [point_to_hex(hash_to_curve(p['secret'])) for p in proofs]
475
+ res = self.check_state(ys)
476
+ states = {s.get('Y'): s.get('state') for s in res.get('states', []) if isinstance(s, dict)}
477
+ return all(states.get(y) == 'UNSPENT' for y in ys)
478
+
479
+
480
+ # ---------------------------------------------------------------------------
481
+ # Live Cashu Payment Rail Adapter
482
+ # ---------------------------------------------------------------------------
483
+
484
+ class LiveCashuPaymentRail(payment.CashuPaymentRail):
485
+ """CashuPaymentRail with live HTTP mint verification and token swaps."""
486
+
487
+ def __init__(
488
+ self,
489
+ mint_url: str = DEFAULT_MINT_URL,
490
+ mint_privkey_hex: Optional[str] = None,
491
+ http_client: Optional[CashuHttpClient] = None,
492
+ spent: Optional[payment.SpentProofs] = None,
493
+ ) -> None:
494
+ self.http: CashuHttpClient = http_client or CashuHttpClient(mint_url=mint_url)
495
+ # Not being the mint, this rail's proof policy is "ask the mint"
496
+ # (NUT-07 check-state) - never "take the payer's word".
497
+ super().__init__(mint_url=mint_url, mint_privkey_hex=mint_privkey_hex, spent=spent,
498
+ proof_verifier=self.http.proofs_unspent)
499
+
500
+ def swap_and_settle(self, payment_hash: str, new_outputs: Sequence[BlindedMessage]) -> SwapResult:
501
+ """Swap held proofs for newly blinded proofs at the live mint to prevent double-spending.
502
+ HELD -> SETTLED through the base state machine; raises IllegalTransition
503
+ (a ValueError) for any other state, and the escrow stays HELD if the
504
+ mint refuses the swap."""
505
+ with self._lock:
506
+ inv = self._escrow_for(payment_hash, 'SETTLED')
507
+ self._check_transition(inv, ('HELD',), 'SETTLED')
508
+ proofs = inv.proofs
509
+ if not proofs:
510
+ raise payment.PaymentError("no proofs held in escrow")
511
+
512
+ # Execute live swap
513
+ swap_res = self.http.swap_tokens(inputs=proofs, outputs=new_outputs)
514
+ self._settle_io(inv, '')
515
+ self._transition(inv, ('HELD',), 'SETTLED')
516
+ inv.swapped_at = inv.settled_at
517
+ inv.new_signatures = list(swap_res.get('signatures', []))
518
+ return swap_res
519
+
520
+ def melt_to_lightning(self, payment_hash: str, bolt11: str) -> MeltQuoteResult:
521
+ """Melt held ecash proofs to pay an external Lightning invoice.
522
+ HELD -> SETTLED only when the mint reports the invoice paid."""
523
+ with self._lock:
524
+ inv = self._escrow_for(payment_hash, 'SETTLED')
525
+ self._check_transition(inv, ('HELD',), 'SETTLED')
526
+
527
+ proofs = inv.proofs
528
+ quote_res = self.http.request_melt_quote(invoice=bolt11)
529
+ quote_id = quote_res.get('quote')
530
+ if not isinstance(quote_id, str) or not quote_id:
531
+ # A melt without a quote id would be a blind spend; refuse rather
532
+ # than send the mint an empty reference.
533
+ raise payment.PaymentError('mint melt quote response carried no quote id')
534
+
535
+ melt_res = self.http.melt_tokens(quote_id=quote_id, inputs=proofs)
536
+ if melt_res.get('paid'):
537
+ preimage = str(melt_res.get('payment_preimage', ''))
538
+ self._settle_io(inv, preimage)
539
+ self._transition(inv, ('HELD',), 'SETTLED')
540
+ inv.preimage = preimage
541
+ return melt_res