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,512 @@
1
+ """Modular Settlement Rails: the adapters that move money, and nothing portable.
2
+
3
+ Spec reference: spec.html "Modular Settlement Rails & Zero-Custody Payments
4
+ (PaymentRail)" and "Bridge 4: NIP-60 / NIP-61 Cashu Nutzap Asymmetric
5
+ Settlement".
6
+
7
+ The maths and wire formats - BDHKE over secp256k1 (NUT-00..03), Cashu tokens,
8
+ NIP-61 Nutzap events and the proof-acceptance policy - live in
9
+ fleetmesh_core.accounting.cashu. What this module owns:
10
+
11
+ - `PaymentRail`, the adapter interface and the escrow state machine
12
+ - `CashuPaymentRail` (Chaumian e-cash blind token settlement)
13
+ - `LightningHoldPaymentRail` (HTLC hold-invoice escrow lifecycle)
14
+ - `MockPaymentRail` (in-memory testing rail)
15
+ - `SpentProofs`, the durable set of proofs this node already took
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import abc
20
+ import hashlib
21
+ import logging
22
+ import threading
23
+ import time
24
+ from collections import Counter
25
+ from dataclasses import asdict, dataclass, field
26
+ from typing import Any, Mapping, Optional, Sequence, cast
27
+
28
+ from fleetmesh_core.accounting.cashu import (
29
+ BlindSignature, CashuProof, NUTZAP_KIND, ProofVerifier,
30
+ accept_proofs, deserialize_cashu_token, point_to_hex,
31
+ proofs_of, verify_nutzap_event,
32
+ )
33
+ from fleetmesh_core.crypto import curve
34
+ from fleetmesh_core.wire.interfaces import NostrEvent
35
+
36
+ log = logging.getLogger(__name__)
37
+
38
+ __all__ = [
39
+ 'DEFAULT_MINT_URL',
40
+ 'PaymentError',
41
+ 'IllegalTransition',
42
+ 'TRANSITIONS',
43
+ 'TERMINAL_STATES',
44
+ 'HoldInvoice',
45
+ 'UNKNOWN_INVOICE',
46
+ 'preimage_matches',
47
+ 'SpentProofs',
48
+ 'PaymentRail',
49
+ 'LightningHoldPaymentRail',
50
+ 'CashuPaymentRail',
51
+ 'MockPaymentRail',
52
+ ]
53
+
54
+ DEFAULT_MINT_URL: str = 'https://mint.example.org'
55
+
56
+
57
+ class PaymentError(ValueError):
58
+ """A rail refused to move money. A ValueError so the callers that already
59
+ catch ValueError around rail actions (settlement subsystem) keep working."""
60
+
61
+
62
+ class IllegalTransition(PaymentError):
63
+ """The one error for an escrow asked to go where its state machine does
64
+ not: settle from CREATED, hold twice, cancel what is settled, hold what
65
+ has expired, or act on a hash no rail has seen (from_state UNKNOWN)."""
66
+
67
+ def __init__(self, payment_hash: str, from_state: str, to_state: str, why: str = '') -> None:
68
+ self.payment_hash = payment_hash
69
+ self.from_state = from_state
70
+ self.to_state = to_state
71
+ self.why = why
72
+ tail = f' ({why})' if why else ''
73
+ super().__init__(f'escrow {payment_hash[:16]}: {from_state} -> {to_state} is not a legal transition{tail}')
74
+
75
+
76
+ # The escrow state machine every rail shares: CREATED -> HELD -> SETTLED |
77
+ # CANCELLED | FAILED, CREATED -> CANCELLED. Terminal states have no exits.
78
+ TRANSITIONS: dict[str, frozenset[str]] = {
79
+ 'CREATED': frozenset({'HELD', 'CANCELLED'}),
80
+ 'HELD': frozenset({'SETTLED', 'CANCELLED', 'FAILED'}),
81
+ 'SETTLED': frozenset(),
82
+ 'CANCELLED': frozenset(),
83
+ 'FAILED': frozenset(),
84
+ }
85
+ TERMINAL_STATES: frozenset[str] = frozenset(s for s, outs in TRANSITIONS.items() if not outs)
86
+
87
+
88
+ from .types import HoldInvoiceDict, PaymentRailStats
89
+
90
+
91
+ @dataclass(slots=True)
92
+ class HoldInvoice:
93
+ """One rail's escrow against a payment hash, from CREATED through HELD to
94
+ SETTLED or CANCELLED. Every rail keeps the same record; the Lightning
95
+ one adds a bolt11, the Cashu one the proofs it is holding, the NWC one
96
+ the wallet's invoice. UNKNOWN is what get_status answers for a hash no
97
+ rail has seen."""
98
+ rail: str
99
+ payment_hash: str
100
+ amount_msat: int
101
+ status: str = 'CREATED'
102
+ created_at: int = 0
103
+ expiry: int = 3600
104
+ description: str = ''
105
+ preimage: Optional[str] = None
106
+ bolt11: Optional[str] = None
107
+ mint: Optional[str] = None
108
+ invoice: Optional[str] = None # the wallet's bolt11, on the NWC rail
109
+ wallet_payment_hash: Optional[str] = None
110
+ proofs: list[CashuProof] = field(default_factory=list)
111
+ held_at: Optional[int] = None
112
+ settled_at: Optional[int] = None
113
+ cancelled_at: Optional[int] = None
114
+ cancel_reason: Optional[str] = None
115
+ swapped_at: Optional[int] = None # live Cashu rail: held proofs swapped at the mint
116
+ new_signatures: list[BlindSignature] = field(default_factory=list)
117
+
118
+ def to_dict(self) -> HoldInvoiceDict:
119
+ return cast(HoldInvoiceDict, asdict(self))
120
+
121
+
122
+ UNKNOWN_INVOICE = HoldInvoice(rail='', payment_hash='', amount_msat=0, status='UNKNOWN')
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Payment Rail Adapter Interface & Drivers
127
+ # ---------------------------------------------------------------------------
128
+
129
+ def preimage_matches(preimage: Any, payment_hash: str) -> bool:
130
+ """SHA256(preimage) == payment_hash, and False - never an exception - for
131
+ a preimage that is not hex. Every rail took bytes.fromhex on the caller's
132
+ string, so a stranger settling with 'zz' raised ValueError out of the
133
+ handler rather than being refused like any other wrong preimage."""
134
+ if not isinstance(preimage, str):
135
+ return False
136
+ try:
137
+ p_bytes = bytes.fromhex(preimage)
138
+ except ValueError:
139
+ return False
140
+ return hashlib.sha256(p_bytes).hexdigest().lower() == payment_hash.lower()
141
+
142
+
143
+ class SpentProofs:
144
+ """The set of proof secrets this node has already accepted.
145
+
146
+ An ecash proof is bearer money: whoever shows it first is paid, and the
147
+ only thing stopping the same proof buying twice is this set. It was a
148
+ Python set on the rail object, so a restart emptied it, and the
149
+ `spent_proofs` SQLite table beside it was written to and never read -
150
+ every proof ever accepted became spendable again on every restart.
151
+
152
+ Subclasses back this with a table; the default is in-memory for rails
153
+ that are constructed without a store (tests, throwaway engines).
154
+ """
155
+
156
+ def __init__(self) -> None:
157
+ self._mem: set[str] = set()
158
+
159
+ def is_spent(self, secret: str) -> bool:
160
+ return secret in self._mem
161
+
162
+ def mark_spent(self, secret: str, keyset_id: Optional[str] = None,
163
+ amount: Optional[int] = None, now: Optional[int] = None) -> None:
164
+ self._mem.add(secret)
165
+
166
+ def __contains__(self, secret: object) -> bool:
167
+ return isinstance(secret, str) and self.is_spent(secret)
168
+
169
+
170
+ class PaymentRail(abc.ABC):
171
+ """The zero-custody escrow state machine every rail shares.
172
+
173
+ One escrow per payment hash, CREATED -> HELD -> SETTLED | CANCELLED |
174
+ FAILED (see TRANSITIONS). The base owns the escrow table, the lock, the
175
+ transitions, expiry and the bookkeeping (`stats`); a concrete rail only
176
+ implements its own I/O: `_hold_io` (is this token/proof/payment real
177
+ money for this escrow?) and `_settle_io` (spend/forward it). Rails that
178
+ need to talk to a wallet to create an invoice override
179
+ `create_hold_invoice` and call `_new_invoice` + `_register`.
180
+
181
+ The public verbs (`hold_payment`, `settle_payment`, `cancel_payment`)
182
+ answer False for every illegal move - unknown hash, wrong state, expired
183
+ invoice, bad preimage - and count the refusal; `create_hold_invoice`
184
+ raises IllegalTransition for a hash already in flight, and
185
+ PaymentError for a non-positive amount or expiry. Nothing silently
186
+ succeeds twice.
187
+ """
188
+
189
+ rail_id: str = ''
190
+
191
+ def __init__(self) -> None:
192
+ self.escrows: dict[str, HoldInvoice] = {}
193
+ self._lock: threading.RLock = threading.RLock()
194
+ self._refusals: Counter[str] = Counter()
195
+
196
+ # -- the table -----------------------------------------------------------
197
+
198
+ @property
199
+ def invoices(self) -> dict[str, HoldInvoice]:
200
+ """The escrow table under its Lightning-era name (`rail.invoices`)."""
201
+ return self.escrows
202
+
203
+ def _new_invoice(self, amount_msat: int, payment_hash: str, expiry: int, description: str) -> HoldInvoice:
204
+ """A CREATED escrow for this rail, not yet registered. Refuses a
205
+ non-positive amount or expiry and a hash whose escrow is in flight -
206
+ a second CREATED over a HELD one would be a way to lose the hold."""
207
+ h = payment_hash.lower()
208
+ if amount_msat <= 0:
209
+ raise PaymentError(f'escrow {h[:16]}: amount_msat must be positive, got {amount_msat}')
210
+ if expiry <= 0:
211
+ raise PaymentError(f'escrow {h[:16]}: expiry must be positive, got {expiry}')
212
+ with self._lock:
213
+ prior = self.escrows.get(h)
214
+ if prior is not None:
215
+ raise IllegalTransition(h, prior.status, 'CREATED', 'an escrow for this hash already exists')
216
+ return HoldInvoice(rail=self.rail_id, payment_hash=h, amount_msat=amount_msat, expiry=expiry,
217
+ created_at=int(time.time()), description=description)
218
+
219
+ def _register(self, inv: HoldInvoice) -> HoldInvoice:
220
+ with self._lock:
221
+ prior = self.escrows.get(inv.payment_hash)
222
+ if prior is not None:
223
+ raise IllegalTransition(inv.payment_hash, prior.status, 'CREATED', 'an escrow for this hash already exists')
224
+ self.escrows[inv.payment_hash] = inv
225
+ return inv
226
+
227
+ def _escrow_for(self, payment_hash: str, to_state: str) -> HoldInvoice:
228
+ """The escrow for a hash, or IllegalTransition(UNKNOWN -> to_state)."""
229
+ inv = self.escrows.get(payment_hash.lower())
230
+ if inv is None:
231
+ raise IllegalTransition(payment_hash.lower(), 'UNKNOWN', to_state, 'no escrow for this hash')
232
+ return inv
233
+
234
+ # -- the state machine ---------------------------------------------------
235
+
236
+ @staticmethod
237
+ def is_expired(inv: HoldInvoice, now: Optional[int] = None) -> bool:
238
+ """A CREATED escrow nobody paid within its expiry. HELD and the
239
+ terminal states do not expire: money already locked is settled or
240
+ cancelled explicitly."""
241
+ if inv.status != 'CREATED' or not inv.created_at:
242
+ return False
243
+ return (now if now is not None else int(time.time())) >= inv.created_at + inv.expiry
244
+
245
+ def _check_transition(self, inv: HoldInvoice, from_states: Sequence[str], to_state: str) -> None:
246
+ """Raise IllegalTransition unless inv may go to to_state from here."""
247
+ if inv.status not in from_states or to_state not in TRANSITIONS.get(inv.status, frozenset()):
248
+ raise IllegalTransition(inv.payment_hash, inv.status, to_state)
249
+ if to_state == 'HELD' and self.is_expired(inv):
250
+ raise IllegalTransition(inv.payment_hash, inv.status, to_state, 'invoice expired')
251
+
252
+ def _transition(self, inv: HoldInvoice, from_states: Sequence[str], to_state: str) -> HoldInvoice:
253
+ """Move inv to to_state, stamping the matching *_at field. Caller holds the lock."""
254
+ self._check_transition(inv, from_states, to_state)
255
+ now = int(time.time())
256
+ inv.status = to_state
257
+ if to_state == 'HELD':
258
+ inv.held_at = now
259
+ elif to_state == 'SETTLED':
260
+ inv.settled_at = now
261
+ elif to_state in ('CANCELLED', 'FAILED'):
262
+ inv.cancelled_at = now
263
+ return inv
264
+
265
+ def _refuse(self, action: str, err: PaymentError) -> bool:
266
+ self._refusals[action] += 1
267
+ log.info('%s rail refused %s: %s', self.rail_id, action, err)
268
+ return False
269
+
270
+ # -- rail-specific I/O hooks ---------------------------------------------
271
+
272
+ def _hold_io(self, inv: HoldInvoice, token_or_proof: Any) -> None:
273
+ """Verify that token_or_proof pays inv; raise PaymentError if not.
274
+ The default rail takes the hold on the operator's word (Lightning
275
+ hold invoice already accepted by the node, mock)."""
276
+
277
+ def _settle_io(self, inv: HoldInvoice, preimage: str) -> None:
278
+ """Spend/forward the held money; raise PaymentError to keep it HELD."""
279
+
280
+ # -- the public verbs ----------------------------------------------------
281
+
282
+ def create_hold_invoice(
283
+ self,
284
+ amount_msat: int,
285
+ payment_hash: str,
286
+ expiry: int = 3600,
287
+ description: str = '',
288
+ ) -> HoldInvoice:
289
+ """Create a conditional hold invoice keyed to payment_hash."""
290
+ return self._register(self._new_invoice(amount_msat, payment_hash, expiry, description))
291
+
292
+ def hold_payment(self, payment_hash: str, token_or_proof: Any = None) -> bool:
293
+ """Lock/hold funds in escrow against payment_hash: CREATED -> HELD."""
294
+ with self._lock:
295
+ try:
296
+ inv = self._escrow_for(payment_hash, 'HELD')
297
+ self._check_transition(inv, ('CREATED',), 'HELD')
298
+ self._hold_io(inv, token_or_proof)
299
+ self._transition(inv, ('CREATED',), 'HELD')
300
+ except PaymentError as e:
301
+ return self._refuse('hold', e)
302
+ return True
303
+
304
+ def settle_payment(self, payment_hash: str, preimage: str) -> bool:
305
+ """Release escrowed funds upon providing the preimage: HELD -> SETTLED."""
306
+ with self._lock:
307
+ try:
308
+ inv = self._escrow_for(payment_hash, 'SETTLED')
309
+ self._check_transition(inv, ('HELD',), 'SETTLED')
310
+ if not preimage_matches(preimage, inv.payment_hash):
311
+ raise PaymentError(f'escrow {inv.payment_hash[:16]}: preimage does not hash to payment_hash')
312
+ self._settle_io(inv, preimage)
313
+ self._transition(inv, ('HELD',), 'SETTLED')
314
+ inv.preimage = preimage
315
+ except PaymentError as e:
316
+ return self._refuse('settle', e)
317
+ return True
318
+
319
+ def cancel_payment(self, payment_hash: str, reason: str = '') -> bool:
320
+ """Cancel the escrow and release what is locked: CREATED | HELD -> CANCELLED."""
321
+ with self._lock:
322
+ try:
323
+ inv = self._escrow_for(payment_hash, 'CANCELLED')
324
+ self._transition(inv, ('CREATED', 'HELD'), 'CANCELLED')
325
+ inv.cancel_reason = reason
326
+ except PaymentError as e:
327
+ return self._refuse('cancel', e)
328
+ return True
329
+
330
+ def fail_payment(self, payment_hash: str, reason: str = '') -> bool:
331
+ """A HELD escrow whose settlement I/O is known lost: HELD -> FAILED."""
332
+ with self._lock:
333
+ try:
334
+ inv = self._escrow_for(payment_hash, 'FAILED')
335
+ self._transition(inv, ('HELD',), 'FAILED')
336
+ inv.cancel_reason = reason
337
+ except PaymentError as e:
338
+ return self._refuse('fail', e)
339
+ return True
340
+
341
+ def get_status(self, payment_hash: str) -> HoldInvoice:
342
+ """The escrow as it stands: CREATED, HELD, SETTLED, CANCELLED, FAILED; UNKNOWN_INVOICE for a hash never seen."""
343
+ return self.escrows.get(payment_hash.lower(), UNKNOWN_INVOICE)
344
+
345
+ def stats(self) -> PaymentRailStats:
346
+ """Escrow counts and msat by state, plus refusals by action."""
347
+ with self._lock:
348
+ by_state: Counter[str] = Counter()
349
+ msat: Counter[str] = Counter()
350
+ for inv in self.escrows.values():
351
+ by_state[inv.status] += 1
352
+ msat[inv.status] += inv.amount_msat
353
+ return {
354
+ 'rail': self.rail_id,
355
+ 'escrows': len(self.escrows),
356
+ 'by_state': dict(by_state),
357
+ 'msat_by_state': dict(msat),
358
+ 'refusals': dict(self._refusals),
359
+ }
360
+
361
+
362
+ class LightningHoldPaymentRail(PaymentRail):
363
+ """Native Lightning hold-invoice rail. Its only I/O is minting the bolt11
364
+ for the escrow; the node's own LN backend accepts the HTLC."""
365
+
366
+ rail_id: str = 'lightning'
367
+
368
+ def create_hold_invoice(
369
+ self,
370
+ amount_msat: int,
371
+ payment_hash: str,
372
+ expiry: int = 3600,
373
+ description: str = 'fleetmesh-compute',
374
+ ) -> HoldInvoice:
375
+ inv = self._new_invoice(amount_msat, payment_hash, expiry, description)
376
+ inv.bolt11 = f"lnbc{amount_msat}u1p{inv.payment_hash[:32]}"
377
+ return self._register(inv)
378
+
379
+
380
+ class CashuPaymentRail(PaymentRail):
381
+ """Chaumian ecash settlement rail implementing NIP-61 Nutzap tokens."""
382
+
383
+ rail_id: str = 'cashu'
384
+
385
+ def __init__(
386
+ self,
387
+ mint_url: str = DEFAULT_MINT_URL,
388
+ mint_privkey_hex: Optional[str] = None,
389
+ spent: Optional[SpentProofs] = None,
390
+ proof_verifier: Optional[ProofVerifier] = None,
391
+ allow_unverified_proofs: bool = False,
392
+ ) -> None:
393
+ super().__init__()
394
+ self.mint_url: str = mint_url
395
+ self.mint_privkey_hex: Optional[str] = mint_privkey_hex
396
+ self.mint_pubkey_hex: Optional[str] = None
397
+ if mint_privkey_hex:
398
+ mint_point = curve.mul_g(int(mint_privkey_hex, 16))
399
+ # mul_g returns None only for k == 0 mod N, which is not a usable
400
+ # signing key; refuse it loudly instead of running keyless.
401
+ if mint_point is None:
402
+ raise ValueError('mint private key is zero mod N')
403
+ self.mint_pubkey_hex = point_to_hex(mint_point)
404
+ # One ledger of spent secrets, shared by the escrow path and the
405
+ # grant-expansion path, persistent when the engine hands one in.
406
+ self.spent: SpentProofs = spent if spent is not None else SpentProofs()
407
+ # How proofs are vouched for when this node is not the mint. None with
408
+ # no mint key means every nutzap is refused, which is the safe default;
409
+ # allow_unverified_proofs is the explicit opt-in for a test rail.
410
+ self.proof_verifier: Optional[ProofVerifier] = proof_verifier
411
+ self.allow_unverified_proofs: bool = allow_unverified_proofs
412
+
413
+ def is_spent(self, secret: str) -> bool:
414
+ return self.spent.is_spent(secret)
415
+
416
+ def mark_spent(self, secret: str, keyset_id: Optional[str] = None, amount: Optional[int] = None,
417
+ now: Optional[int] = None) -> None:
418
+ self.spent.mark_spent(secret, keyset_id, amount, now)
419
+
420
+ def verify_nutzap(self, event: NostrEvent | Mapping[str, Any], expected_recipient: Optional[str] = None) -> tuple[bool, str, int]:
421
+ """`verify_nutzap_event` under this rail's own proof policy."""
422
+ return verify_nutzap_event(event, expected_recipient=expected_recipient,
423
+ mint_privkey_hex=self.mint_privkey_hex,
424
+ proof_verifier=self.proof_verifier,
425
+ allow_unverified_proofs=self.allow_unverified_proofs)
426
+
427
+ def _accept_proofs(self, proofs: Sequence[CashuProof]) -> tuple[bool, str]:
428
+ """`accept_proofs` under this rail's own proof policy: the same rule
429
+ verify_nutzap applies, for proofs that did not arrive in a nutzap."""
430
+ return accept_proofs(proofs, mint_privkey_hex=self.mint_privkey_hex,
431
+ proof_verifier=self.proof_verifier,
432
+ allow_unverified_proofs=self.allow_unverified_proofs)
433
+
434
+ def can_verify_proofs(self) -> bool:
435
+ """False when this rail would take any JSON with a `secret` as money."""
436
+ return bool(self.mint_privkey_hex) or self.proof_verifier is not None or self.allow_unverified_proofs
437
+
438
+ def create_hold_invoice(
439
+ self,
440
+ amount_msat: int,
441
+ payment_hash: str,
442
+ expiry: int = 3600,
443
+ description: str = 'fleetmesh-nutzap',
444
+ ) -> HoldInvoice:
445
+ inv = self._new_invoice(amount_msat, payment_hash, expiry, description)
446
+ inv.mint = self.mint_url
447
+ return self._register(inv)
448
+
449
+ def _hold_io(self, inv: HoldInvoice, token_or_proof: Any) -> None:
450
+ """Accept Cashu proofs or a Nutzap event as the money behind inv.
451
+
452
+ Whatever the shape - a cashuA token string, a NIP-61 event or a bare
453
+ proof list - the proofs go through the rail's one proof policy
454
+ (`_accept_proofs`). The string and list shapes used to skip it, so a
455
+ rail with no mint key held any JSON with a `secret` as payment.
456
+ """
457
+ h = inv.payment_hash
458
+ proofs: list[CashuProof] = []
459
+ policy_applied = False
460
+ if isinstance(token_or_proof, str) and token_or_proof.startswith('cashuA'):
461
+ try:
462
+ tok = deserialize_cashu_token(token_or_proof)
463
+ except (ValueError, TypeError) as e:
464
+ raise PaymentError(f'escrow {h[:16]}: malformed cashu token: {e}') from e
465
+ for t in tok.get('token', []):
466
+ proofs.extend(t.get('proofs', []))
467
+ elif isinstance(token_or_proof, dict) and token_or_proof.get('kind') == NUTZAP_KIND:
468
+ # verify_nutzap applies the proof policy to these itself.
469
+ ok_nut, why, _ = self.verify_nutzap(token_or_proof)
470
+ if not ok_nut:
471
+ raise PaymentError(f'escrow {h[:16]}: nutzap refused: {why}')
472
+ policy_applied = True
473
+ try:
474
+ proofs = proofs_of(token_or_proof)
475
+ except ValueError as e:
476
+ raise PaymentError(f'escrow {h[:16]}: malformed nutzap proofs: {e}') from e
477
+ elif isinstance(token_or_proof, list):
478
+ proofs = list(token_or_proof)
479
+
480
+ if not proofs:
481
+ raise PaymentError(f'escrow {h[:16]}: no proofs offered')
482
+
483
+ # Verify not double spent
484
+ for p in proofs:
485
+ if not isinstance(p, dict):
486
+ raise PaymentError(f'escrow {h[:16]}: proof is not an object')
487
+ sec = p.get('secret')
488
+ if not isinstance(sec, str) or not sec:
489
+ raise PaymentError(f'escrow {h[:16]}: proof has no secret')
490
+ if self.is_spent(sec):
491
+ raise PaymentError(f'escrow {h[:16]}: proof already spent')
492
+
493
+ if not policy_applied:
494
+ ok_proofs, why = self._accept_proofs(proofs)
495
+ if not ok_proofs:
496
+ raise PaymentError(f'escrow {h[:16]}: proofs refused: {why}')
497
+
498
+ inv.proofs = proofs
499
+
500
+ def _settle_io(self, inv: HoldInvoice, preimage: str) -> None:
501
+ """Mark the held proofs spent; the escrow then goes SETTLED."""
502
+ now = int(time.time())
503
+ for p in inv.proofs:
504
+ if 'secret' in p:
505
+ self.mark_spent(p['secret'], p.get('id'), p.get('amount'), now)
506
+
507
+
508
+ class MockPaymentRail(PaymentRail):
509
+ """Deterministic in-memory settlement rail for testing: the base state
510
+ machine with no I/O at all."""
511
+
512
+ rail_id: str = 'mock'
@@ -0,0 +1,74 @@
1
+ """SQLite persistence for settlement: the escrow ledger and the spent-proof
2
+ table, plus the `SpentProofs` that reads through to it so "has this secret
3
+ paid before" survives a restart."""
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Optional
7
+
8
+ from . import payment
9
+ from node.compartment.schema import Column, Index, Table
10
+ from node.storage import SqliteStore
11
+
12
+
13
+ class SettlementStore(SqliteStore):
14
+ """Thread-safe SQLite storage for settlement escrows and spent proofs."""
15
+
16
+ SCHEMA = (
17
+ Table('escrows', (
18
+ Column('ask_id', 'TEXT', primary_key=True),
19
+ Column('buyer', 'TEXT', not_null=True),
20
+ Column('seller', 'TEXT', not_null=True),
21
+ Column('amount_msat', 'INTEGER', not_null=True),
22
+ Column('payment_hash', 'TEXT', not_null=True),
23
+ Column('preimage', 'TEXT'),
24
+ Column('status', 'TEXT', not_null=True, default="'CREATED'"),
25
+ Column('rail', 'TEXT', not_null=True),
26
+ Column('created_at', 'INTEGER', not_null=True),
27
+ Column('settled_at', 'INTEGER'),
28
+ ), indices=(
29
+ Index('idx_escrows_hash', ('payment_hash',)),
30
+ )),
31
+ Table('spent_proofs', (
32
+ Column('secret', 'TEXT', primary_key=True),
33
+ Column('keyset_id', 'TEXT'),
34
+ Column('amount', 'INTEGER'),
35
+ Column('spent_at', 'INTEGER', not_null=True),
36
+ )),
37
+ )
38
+
39
+ class PersistentSpentProofs(payment.SpentProofs):
40
+ """`SpentProofs` read through to the settlement store's spent_proofs table.
41
+
42
+ Loaded whole on init and written through on every mark, so the answer to
43
+ "has this secret paid before" survives a restart - which is the only
44
+ question the table exists to answer, and the one it was never asked.
45
+ The in-memory set is a cache of the table, not a second source of truth:
46
+ a miss in the cache still asks the table, so a second engine opened on
47
+ the same file (or a row written by another process) is honoured.
48
+ """
49
+
50
+ def __init__(self, db: SettlementStore, clock: Any) -> None:
51
+ super().__init__()
52
+ self.db = db
53
+ self.clock = clock
54
+ for (secret,) in self.db._all('SELECT secret FROM spent_proofs'):
55
+ self._mem.add(secret)
56
+
57
+ def is_spent(self, secret: str) -> bool:
58
+ if secret in self._mem:
59
+ return True
60
+ row = self.db._one('SELECT 1 FROM spent_proofs WHERE secret = ?', (secret,))
61
+ if row:
62
+ self._mem.add(secret)
63
+ return True
64
+ return False
65
+
66
+ def mark_spent(self, secret: str, keyset_id: Optional[str] = None,
67
+ amount: Optional[int] = None, now: Optional[int] = None) -> None:
68
+ self.db.execute('''
69
+ INSERT OR IGNORE INTO spent_proofs (secret, keyset_id, amount, spent_at)
70
+ VALUES (?, ?, ?, ?)
71
+ ''', (secret, keyset_id, amount, now if now is not None else self.clock()))
72
+ self.db.commit()
73
+ self._mem.add(secret)
74
+
@@ -0,0 +1,58 @@
1
+ """Types and records for the Settlement compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, TypedDict
5
+
6
+ from fleetmesh_core.accounting.cashu import BlindSignature, CashuProof
7
+
8
+
9
+ class HoldInvoiceDict(TypedDict, total=False):
10
+ """Normalized dictionary representation of a HoldInvoice record."""
11
+ rail: str
12
+ payment_hash: str
13
+ amount_msat: int
14
+ status: str
15
+ created_at: int
16
+ expiry: int
17
+ description: str
18
+ preimage: Optional[str]
19
+ bolt11: Optional[str]
20
+ mint: Optional[str]
21
+ invoice: Optional[str]
22
+ wallet_payment_hash: Optional[str]
23
+ proofs: list[CashuProof]
24
+ held_at: Optional[int]
25
+ settled_at: Optional[int]
26
+ cancelled_at: Optional[int]
27
+ cancel_reason: Optional[str]
28
+ swapped_at: Optional[int]
29
+ new_signatures: list[BlindSignature]
30
+
31
+
32
+ class PaymentRailStats(TypedDict):
33
+ """Aggregate statistics for an escrow settlement rail."""
34
+ rail: str
35
+ escrows: int
36
+ by_state: dict[str, int]
37
+ msat_by_state: dict[str, int]
38
+ refusals: dict[str, int]
39
+
40
+
41
+ class EscrowRowDict(TypedDict, total=False):
42
+ """Row from the escrows database table."""
43
+ ask_id: str
44
+ buyer: str
45
+ seller: str
46
+ amount_msat: int
47
+ payment_hash: str
48
+ preimage: Optional[str]
49
+ status: str
50
+ rail: str
51
+ created_at: int
52
+ settled_at: Optional[int]
53
+
54
+
55
+ class SettleShowDict(TypedDict):
56
+ """Summary of active settlement rails and current held escrows."""
57
+ rails: list[str]
58
+ escrows: list[EscrowRowDict]
@@ -0,0 +1,8 @@
1
+ """Sidecars: fronting off-the-shelf projects the node does not reimplement.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import Sidecars
6
+ from . import manifest
7
+
8
+ __all__ = ['Sidecars', 'manifest']