enigma-memory 0.1.18 → 0.1.22

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 (276) hide show
  1. package/README.md +76 -24
  2. package/apps/cli/bin/enigma-desktop.mjs +140 -0
  3. package/apps/cli/bin/enigma-terminal.mjs +78 -0
  4. package/apps/cli/bin/enigma.mjs +1923 -285
  5. package/apps/desktop/electron-main.cjs +217 -0
  6. package/apps/desktop/package.json +12 -0
  7. package/apps/desktop/src/app.js +264 -7
  8. package/apps/desktop/src/index.html +3514 -1373
  9. package/apps/desktop/src/launch-electron.mjs +51 -0
  10. package/apps/desktop/src/server.mjs +2914 -0
  11. package/apps/desktop/src/styles.css +2972 -260
  12. package/apps/desktop/src/zk-browser-prove.mjs +53 -0
  13. package/apps/desktop/src/zk-state.mjs +1789 -0
  14. package/apps/gateway/bin/enigma-gateway.mjs +102 -5
  15. package/apps/gateway/src/server.mjs +271 -8
  16. package/apps/ios/EnigmaCore/Package.swift +12 -0
  17. package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
  18. package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
  19. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
  20. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
  21. package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
  22. package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
  23. package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
  24. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
  25. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
  26. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
  27. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
  28. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
  29. package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
  30. package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
  31. package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
  32. package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
  33. package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
  34. package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
  35. package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
  36. package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
  37. package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
  38. package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
  39. package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
  40. package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
  41. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
  42. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
  43. package/apps/native-host/README.md +19 -8
  44. package/apps/native-host/bin/enigma-native-host.mjs +229 -13
  45. package/apps/relay/bin/enigma-relay.mjs +103 -5
  46. package/apps/relay/src/federation-runtime.mjs +618 -0
  47. package/apps/relay/src/server.mjs +310 -9
  48. package/apps/verifier/bin/enigma-verify.mjs +327 -11
  49. package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
  50. package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
  51. package/cortex-v3/circuits/build/vk_bytes.json +32 -0
  52. package/cortex-v3/proving-assets.json +64 -0
  53. package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
  54. package/cortex-v3/zk/action-transition-vk.json +119 -0
  55. package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
  56. package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
  57. package/cortex-v3/zk/intent-witness.mjs +365 -0
  58. package/cortex-v3/zk/intent-witness.test.mjs +485 -0
  59. package/cortex-v3/zk/proving-assets.mjs +203 -0
  60. package/cortex-v3/zk/sale-witness.mjs +783 -0
  61. package/cortex-v3/zk/sale-witness.test.mjs +784 -0
  62. package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
  63. package/cortex-v3/zk/settlement-evidence.mjs +722 -0
  64. package/cortex-v3/zk/setup-intent.mjs +688 -0
  65. package/cortex-v3/zk/setup-sale.mjs +666 -0
  66. package/cortex-v3/zk/setup.mjs +594 -0
  67. package/cortex-v3/zk/witness.mjs +184 -0
  68. package/cortex-v3/zk/zk-codec.mjs +232 -0
  69. package/cortex-v3/zk/zk-codec.test.mjs +293 -0
  70. package/cortex-v3/zk/zk-settle.mjs +370 -0
  71. package/cortex-v3/zk/zk-tree.mjs +256 -0
  72. package/cortex-v3/zk/zk-tree.test.mjs +419 -0
  73. package/deploy/docker-compose.local-production-simulation.yml +36 -0
  74. package/docs/browser-extension-install.md +8 -6
  75. package/docs/client-connectors.md +15 -11
  76. package/docs/developer-ecosystem.md +15 -13
  77. package/docs/enigma-memory-ready-conformance.md +11 -9
  78. package/docs/install-anywhere.md +61 -28
  79. package/docs/installers-and-desktop.md +8 -7
  80. package/docs/novelty-invention-candidates.md +161 -161
  81. package/docs/proof-network-claim-boundaries.md +320 -318
  82. package/examples/01-quickstart-agent/index.mjs +49 -0
  83. package/examples/01_agent_memory_quickstart.mjs +57 -0
  84. package/examples/02-multi-agent-swarm/index.mjs +57 -0
  85. package/examples/02_cross_model_passport.mjs +64 -0
  86. package/examples/03-langchain-memory/index.mjs +41 -0
  87. package/examples/03_poseidon_commitment_verification.mjs +71 -0
  88. package/examples/04-python-trading-agent/trader.py +49 -0
  89. package/examples/README.md +27 -0
  90. package/examples/ci/github-actions.yml +7 -2
  91. package/package.json +142 -11
  92. package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
  93. package/packages/connectors/src/index.js +196 -4
  94. package/packages/connectors/swarm-router.mjs +168 -0
  95. package/packages/core/src/index.js +248 -1
  96. package/packages/core/src/version.mjs +7 -0
  97. package/packages/dev-tools/package.json +19 -0
  98. package/packages/dev-tools/src/index.js +4 -0
  99. package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
  100. package/packages/dev-tools/src/swarm-simulator.js +101 -0
  101. package/packages/dev-tools/src/vault-inspector.js +114 -0
  102. package/packages/dev-tools/src/vector-benchmark.js +100 -0
  103. package/packages/developer-platform/src/access-credentials.js +341 -0
  104. package/packages/developer-platform/src/http.js +132 -0
  105. package/packages/developer-platform/src/index.js +4 -0
  106. package/packages/developer-platform/src/usage-http.js +60 -0
  107. package/packages/developer-platform/src/usage.js +295 -0
  108. package/packages/enclave-runtime/attestation.mjs +159 -0
  109. package/packages/enclave-runtime/index.mjs +47 -0
  110. package/packages/enclave-runtime/session-manager.mjs +253 -0
  111. package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
  112. package/packages/enigma-reflex/package.json +14 -0
  113. package/packages/enigma-reflex/src/index.js +204 -0
  114. package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
  115. package/packages/enigma-reflex/training/requirements.txt +8 -0
  116. package/packages/enigma-reflex/training/train.py +314 -0
  117. package/packages/enigma-weave/LICENSE +22 -0
  118. package/packages/enigma-weave/UPSTREAM.json +21 -0
  119. package/packages/enigma-weave/package.json +14 -0
  120. package/packages/enigma-weave/src/index.js +286 -0
  121. package/packages/hosted-cloud/src/index.js +80 -5
  122. package/packages/importers/src/index.js +432 -0
  123. package/packages/inference-runtime/src/browser.js +401 -0
  124. package/packages/inference-runtime/src/chat.js +265 -0
  125. package/packages/inference-runtime/src/code.js +407 -0
  126. package/packages/inference-runtime/src/contracts.js +162 -0
  127. package/packages/inference-runtime/src/http.js +232 -0
  128. package/packages/inference-runtime/src/image.js +186 -0
  129. package/packages/inference-runtime/src/index.js +10 -0
  130. package/packages/inference-runtime/src/model-router.js +320 -0
  131. package/packages/inference-runtime/src/platform.js +125 -0
  132. package/packages/inference-runtime/src/privacy.js +400 -0
  133. package/packages/inference-runtime/src/video.js +253 -0
  134. package/packages/mcp-server/README.md +22 -6
  135. package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
  136. package/packages/mcp-server/src/index.js +1418 -105
  137. package/packages/mcp-server/src/oauth.js +561 -0
  138. package/packages/mcp-server/src/private-handoff.js +84 -0
  139. package/packages/mcp-server/src/remote-http.js +273 -0
  140. package/packages/mcp-server/src/remote-policy.js +72 -0
  141. package/packages/mcp-server/swarm-bridge.mjs +361 -0
  142. package/packages/mesh/index.d.ts +283 -0
  143. package/packages/mesh/package.json +23 -0
  144. package/packages/mesh/src/crypto.js +189 -0
  145. package/packages/mesh/src/federation-packets.js +353 -0
  146. package/packages/mesh/src/gossip.js +311 -0
  147. package/packages/mesh/src/index.js +6 -0
  148. package/packages/mesh/src/protocol.js +255 -0
  149. package/packages/mesh/src/router.js +279 -0
  150. package/packages/mesh/src/transport.js +306 -0
  151. package/packages/passport/src/index.js +426 -1
  152. package/packages/private-economy/src/credits-http.js +100 -0
  153. package/packages/private-economy/src/credits.js +447 -0
  154. package/packages/private-economy/src/index.js +5 -0
  155. package/packages/private-economy/src/payments-http.js +120 -0
  156. package/packages/private-economy/src/payments.js +509 -0
  157. package/packages/private-economy/src/x402.js +346 -0
  158. package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
  159. package/packages/rag/index.d.ts +182 -0
  160. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
  161. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
  162. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
  163. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
  164. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
  165. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
  166. package/packages/rag/package.json +27 -0
  167. package/packages/rag/src/blinded-search.js +109 -0
  168. package/packages/rag/src/bm25.js +169 -0
  169. package/packages/rag/src/embeddings.js +459 -0
  170. package/packages/rag/src/hybrid.js +76 -0
  171. package/packages/rag/src/index.js +38 -0
  172. package/packages/rag/src/reranker.js +61 -0
  173. package/packages/rag/src/research.js +107 -0
  174. package/packages/rag/src/vector-store.js +430 -0
  175. package/packages/rag/src/verify-model-artifacts.mjs +4 -0
  176. package/packages/sdk/index.d.ts +760 -0
  177. package/packages/sdk/package.json +33 -0
  178. package/packages/sdk/python/README.md +24 -0
  179. package/packages/sdk/python/enigma_sdk.py +250 -0
  180. package/packages/sdk/python/pyproject.toml +34 -0
  181. package/packages/sdk/python/requirements.txt +1 -0
  182. package/packages/sdk/python/setup.py +20 -0
  183. package/packages/sdk/src/federation/capability-grant.js +389 -0
  184. package/packages/sdk/src/federation/federation-router.js +360 -0
  185. package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
  186. package/packages/sdk/src/federation/index.js +3 -0
  187. package/packages/sdk/src/index.js +1796 -0
  188. package/packages/sdk/src/intelligence/contradiction.js +337 -0
  189. package/packages/sdk/src/intelligence/decision-engine.js +155 -0
  190. package/packages/sdk/src/intelligence/index.js +4 -0
  191. package/packages/sdk/src/intelligence/ontology.js +122 -0
  192. package/packages/sdk/src/intelligence/temporal.js +123 -0
  193. package/packages/sdk/src/market-client.js +142 -0
  194. package/packages/sdk/src/mesh-client.js +110 -0
  195. package/packages/sdk/src/middleware/index.js +3 -0
  196. package/packages/sdk/src/middleware/langchain.js +159 -0
  197. package/packages/sdk/src/middleware/llamaindex.js +101 -0
  198. package/packages/sdk/src/middleware/vercel-ai.js +112 -0
  199. package/packages/sdk/src/rag-client.js +85 -0
  200. package/packages/sdk/src/swarm-orchestrator.js +260 -0
  201. package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
  202. package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
  203. package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
  204. package/packages/snapcompact/index.d.ts +284 -0
  205. package/packages/snapcompact/package.json +25 -0
  206. package/packages/snapcompact/src/index.js +716 -0
  207. package/packages/storage/PACKAGE_CONTRACT.md +1 -1
  208. package/packages/terminal-console/animations.mjs +240 -0
  209. package/packages/terminal-console/auto-anchor.mjs +220 -0
  210. package/packages/terminal-console/banner.mjs +91 -0
  211. package/packages/terminal-console/commands.mjs +459 -0
  212. package/packages/terminal-console/delegation.mjs +152 -0
  213. package/packages/terminal-console/index.mjs +5 -0
  214. package/packages/terminal-console/outbox.mjs +143 -0
  215. package/packages/terminal-console/phantom-bridge.mjs +637 -0
  216. package/packages/terminal-console/repl.mjs +136 -0
  217. package/packages/terminal-console/signer-store.mjs +130 -0
  218. package/packages/terminal-console/solana-rpc.mjs +214 -0
  219. package/packages/terminal-console/solana-transport.mjs +189 -0
  220. package/packages/terminal-tui/dashboard.mjs +214 -0
  221. package/packages/terminal-tui/index.mjs +28 -0
  222. package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
  223. package/packages/terminal-tui/telemetry-hud.mjs +137 -0
  224. package/packages/vault/index.d.ts +449 -0
  225. package/packages/vault/package.json +27 -0
  226. package/packages/vault/src/e2ee.mjs +393 -0
  227. package/packages/vault/src/enclave.js +481 -0
  228. package/packages/vault/src/erasure.js +207 -0
  229. package/packages/vault/src/index.js +1018 -155
  230. package/packages/vault/src/persistence.js +307 -0
  231. package/packages/vault/src/poseidon.js +354 -0
  232. package/packages/vault/src/receipt.js +459 -0
  233. package/scripts/benchmark-optical-context.mjs +166 -0
  234. package/scripts/bootstrap-enigma.mjs +502 -0
  235. package/scripts/build-edge-backend-workers.mjs +20 -5
  236. package/scripts/build-goal-completion-audit.mjs +72 -25
  237. package/scripts/build-hosted-api-key-lifecycle.mjs +26 -8
  238. package/scripts/build-hosted-customer-lifecycle.mjs +20 -3
  239. package/scripts/build-hosted-probe-worker.mjs +19 -4
  240. package/scripts/build-installer-assets.mjs +41 -21
  241. package/scripts/build-operator-evidence-starter.mjs +59 -1
  242. package/scripts/build-production-backend-env-kit.mjs +2 -0
  243. package/scripts/build-production-unblocker.mjs +3 -0
  244. package/scripts/check.mjs +17 -3
  245. package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
  246. package/scripts/install-enigma-local.mjs +18 -5
  247. package/scripts/release-audit.mjs +65 -109
  248. package/scripts/release-provenance.mjs +6 -0
  249. package/scripts/run-backend-readiness-smoke.mjs +112 -10
  250. package/scripts/scan-secrets.mjs +1 -0
  251. package/scripts/simulate-production-env.mjs +7 -2
  252. package/scripts/validate-hosted-backend-live.mjs +112 -1
  253. package/specs/antibody-pack-v1.schema.json +95 -0
  254. package/specs/antigen-envelope-v1.schema.json +81 -0
  255. package/specs/boundary-manifest-v1.schema.json +35 -35
  256. package/specs/capsule-v1.schema.json +55 -55
  257. package/specs/claim-boundary-manifest-v1.schema.json +22 -22
  258. package/specs/claim-ledger-v1.schema.json +291 -0
  259. package/specs/context-passport-v1.schema.json +59 -0
  260. package/specs/deletion-tombstone-v1.schema.json +26 -26
  261. package/specs/evidence-packet-v1.schema.json +177 -0
  262. package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
  263. package/specs/immune-scan-report-v1.schema.json +112 -0
  264. package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
  265. package/specs/memory-atom-v1.schema.json +59 -0
  266. package/specs/memory-event-v1.schema.json +42 -42
  267. package/specs/passport-v1.schema.json +50 -50
  268. package/specs/proof-of-non-use-v1.schema.json +65 -0
  269. package/specs/quarantine-record-v1.schema.json +126 -0
  270. package/specs/receipt-v1.schema.json +61 -61
  271. package/specs/state-checkpoint-v1.schema.json +37 -37
  272. package/specs/trust-bundle-v1.schema.json +56 -56
  273. package/specs/trust-card-v1.schema.json +119 -0
  274. package/docs/proof-network-launch-plan.md +0 -421
  275. package/packages/metering/PACKAGE_CONTRACT.md +0 -20
  276. package/scripts/build-ai-orchestration-plan.mjs +0 -248
@@ -0,0 +1,783 @@
1
+ // sale-witness.mjs — client-side witness builder for
2
+ // circuits/sealed_sale_release.circom — the ZKCP "Sealed Sale" release proof.
3
+ //
4
+ // Binding spec (SaleCircuit-confirmed via hub, 2026-08-23): a Groth16 proof
5
+ // that a Poseidon-CTR ciphertext (committed in a note of the shared note tree)
6
+ // decrypts under a committed data-encryption key (DEK), AND that the same DEK
7
+ // is wrapped to the buyer's BabyJubJub public key in a recipient-bound
8
+ // envelope. Payment releases ONLY against this proof; no key material ever
9
+ // touches the chain.
10
+ //
11
+ // Circuit legs mirrored here EXACTLY:
12
+ // Leg A — note membership: note = Poseidon3(ownerKey, rho, payloadCommitPos)
13
+ // ∈ noteRoot (shared note tree, D=20, LSB-first path bits).
14
+ // Leg B — encryption consistency (Poseidon-CTR stream cipher, field add):
15
+ // ks_i = Poseidon4(dekLo, dekHi, nonce, i)
16
+ // c_i = p_i + ks_i (mod r; circuit enforces c_i - ks_i = p_i)
17
+ // payloadCommitPos = fold(acc=0; Poseidon2(acc, p_i)) (PUBLIC)
18
+ // cipherCommit = fold(acc=0; Poseidon2(acc, c_i)) (PUBLIC —
19
+ // fund-loss fix: the delivered ciphertext is
20
+ // bound on-chain, so junk delivery is provable
21
+ // before the buyer pays)
22
+ // Leg C — keyCommit = Poseidon2(dekLo, dekHi); dekLo/dekHi each < 2^128.
23
+ // Leg D — recipient-bound envelope (BabyJubJub on BN254, circomlib babyjub):
24
+ // buyer pubkey B on-curve (BabyCheck equation) AND not small-order
25
+ // (B8 = 8·B via three doublings, constrain B8.x ≠ 0 — the only
26
+ // on-curve points with x=0 are identity (0,1) and order-2 (0,-1),
27
+ // so x≠0 ⟺ 8·B ≠ identity ⟺ B not in the 8-torsion).
28
+ // epk = esk·Base8, shared = esk·B (esk < 2^253, Num2Bits(253))
29
+ // ss = Poseidon2(shared.x, shared.y)
30
+ // encDEK_lo = dekLo + Poseidon2(ss, 0) (mod r)
31
+ // encDEK_hi = dekHi + Poseidon2(ss, 1) (mod r)
32
+ // envelopeCommit = Poseidon4(epk.x, epk.y, encDEK_lo, encDEK_hi) (PUBLIC)
33
+ // Leg E — saleTermsHash = Poseidon9(DOMAIN_SALE, sellerLo, sellerHi,
34
+ // buyerPubX, buyerPubY, mintLo, mintHi, price, expirySlot);
35
+ // seller/mint are Solana pubkeys split LE 128-bit limbs;
36
+ // price < 2^64; expirySlot < 2^48. The buyer pubkey limbs are
37
+ // bound into the terms (fund-loss fix, Main 2026-08-23): without
38
+ // them a seller could claim a buyer's escrow with a receipt
39
+ // wrapped to a colluder under the SAME terms hash.
40
+ //
41
+ // Public signals (exact order/names): noteRoot, payloadCommitPos,
42
+ // cipherCommit, keyCommit, saleTermsHash, envelopeCommit.
43
+ // Private signals: ownerKey, rho, notePathElements[20], notePathIndices[20]
44
+ // (LSB-first), plaintext[32], ciphertext[32], nonce, dekLo, dekHi,
45
+ // buyerPubX, buyerPubY, esk, sellerLo, sellerHi, mintLo, mintHi, price,
46
+ // expirySlot.
47
+ //
48
+ // All field elements are emitted as DECIMAL STRINGS (snarkjs JSON convention).
49
+
50
+ import { randomBytes } from "node:crypto";
51
+
52
+ import { buildBabyjub } from "circomlibjs";
53
+ import bs58 from "bs58";
54
+
55
+ import { FIELD_R, NoteTree, poseidon } from "./zk-tree.mjs";
56
+
57
+ /** BabyJubJub curve over the BN254 scalar field (circomlibjs). Coordinates
58
+ * live in Fr — identical to the circuit's field, so no modulus translation
59
+ * is ever needed. */
60
+ const babyJub = await buildBabyjub();
61
+ const JF = babyJub.F;
62
+
63
+ /** Note tree depth (matches the shared note tree / NoteTree default). */
64
+ export const NOTE_DEPTH = 20;
65
+
66
+ /**
67
+ * DOMAIN_SALE = 0x656e69676d615f73616c65 ("enigma_sale" ASCII, 11 bytes,
68
+ * big-endian) = 122622912455040968075340901. Confirmed with SaleCircuit.
69
+ */
70
+ export const DOMAIN_SALE = BigInt("0x656e69676d615f73616c65");
71
+ /** Domain separator for the salted plaintext-fold init ("enigma_psalt" ASCII BE). */
72
+ export const DOMAIN_PSALT = BigInt("0x656e69676d615f7073616c74");
73
+
74
+ /** Public signal names in exact on-chain/circuit order (6 publics, IC 7).
75
+ * cipherCommit sits at index 2 (fund-loss fix, Main 2026-08-23): the buyer
76
+ * verifies the delivered ciphertext folds to this PUBLIC commitment before
77
+ * paying — that check is the junk-delivery defense. */
78
+ export const SALE_PUBLIC_SIGNALS = Object.freeze([
79
+ "noteRoot",
80
+ "payloadCommitPos",
81
+ "cipherCommit",
82
+ "keyCommit",
83
+ "saleTermsHash",
84
+ "envelopeCommit",
85
+ ]);
86
+
87
+ /** Fixed plaintext/ciphertext chunk count (circuit shape is fixed at n=32). */
88
+ export const CHUNK_COUNT = 32;
89
+ /** Bytes per chunk: 31-byte little-endian limbs (< 2^248 < r, always canonical). */
90
+ export const CHUNK_BYTES = 31;
91
+ /** Maximum plaintext size: 32 chunks × 31 bytes. */
92
+ export const PAYLOAD_CAPACITY = CHUNK_COUNT * CHUNK_BYTES; // 992
93
+
94
+ const R = FIELD_R; // BN254 scalar field modulus
95
+ const TWO_64 = 1n << 64n;
96
+ const TWO_128 = 1n << 128n;
97
+ const TWO_48 = 1n << 48n;
98
+ const TWO_248 = 1n << 248n;
99
+ const TWO_253 = 1n << 253n;
100
+
101
+ const SUB_ORDER = BigInt(babyJub.subOrder.toString()); // prime subgroup order
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Byte/limb helpers
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /** Little-endian bytes → bigint. */
108
+ export function bytesToBigIntLE(bytes) {
109
+ let v = 0n;
110
+ for (let i = bytes.length - 1; i >= 0; i--) v = (v << 8n) | BigInt(bytes[i]);
111
+ return v;
112
+ }
113
+
114
+ /** bigint → exactly `len` little-endian bytes. */
115
+ export function bigIntToBytesLE(value, len) {
116
+ let v = BigInt(value);
117
+ if (v < 0n) throw new RangeError("bigIntToBytesLE: negative value");
118
+ const out = new Uint8Array(len);
119
+ for (let i = 0; i < len; i++) {
120
+ out[i] = Number(v & 0xffn);
121
+ v >>= 8n;
122
+ }
123
+ if (v !== 0n) throw new RangeError(`bigIntToBytesLE: value does not fit in ${len} bytes`);
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * Plaintext bytes → exactly CHUNK_COUNT field chunks: consecutive 31-byte
129
+ * little-endian limbs, zero-padded. Every chunk < 2^248 < r (canonical).
130
+ * @param {Uint8Array} bytes at most PAYLOAD_CAPACITY (992) bytes
131
+ * @returns {bigint[]} length-CHUNK_COUNT array
132
+ */
133
+ export function bytesToChunks(bytes) {
134
+ if (!(bytes instanceof Uint8Array)) {
135
+ throw new TypeError("bytesToChunks: expected a Uint8Array");
136
+ }
137
+ if (bytes.length > PAYLOAD_CAPACITY) {
138
+ throw new RangeError(
139
+ `payload too large: ${bytes.length} bytes > capacity ${PAYLOAD_CAPACITY}`,
140
+ );
141
+ }
142
+ const chunks = new Array(CHUNK_COUNT);
143
+ for (let i = 0; i < CHUNK_COUNT; i++) {
144
+ chunks[i] = bytesToBigIntLE(bytes.subarray(i * CHUNK_BYTES, (i + 1) * CHUNK_BYTES));
145
+ }
146
+ return chunks;
147
+ }
148
+ /** Alias kept for the spec wording (chunksFromBytes ≡ bytesToChunks). */
149
+ export const chunksFromBytes = bytesToChunks;
150
+
151
+ /**
152
+ * Field chunks → bytes (inverse of bytesToChunks). Because chunks are
153
+ * zero-padded, the ORIGINAL byte length must be supplied to round-trip
154
+ * plaintexts with trailing 0x00 bytes; defaults to the full capacity.
155
+ * @param {bigint[]|string[]|number[]} chunks exactly CHUNK_COUNT limbs, each < 2^248
156
+ * @param {number} [byteLength] bytes to emit (default PAYLOAD_CAPACITY)
157
+ * @returns {Uint8Array}
158
+ */
159
+ export function chunksToBytes(chunks, byteLength = PAYLOAD_CAPACITY) {
160
+ if (!Array.isArray(chunks) || chunks.length !== CHUNK_COUNT) {
161
+ throw new TypeError(`chunksToBytes: expected ${CHUNK_COUNT} chunks`);
162
+ }
163
+ if (!Number.isInteger(byteLength) || byteLength < 0 || byteLength > PAYLOAD_CAPACITY) {
164
+ throw new RangeError(`chunksToBytes: byteLength ${byteLength} out of [0, ${PAYLOAD_CAPACITY}]`);
165
+ }
166
+ const out = new Uint8Array(PAYLOAD_CAPACITY);
167
+ for (let i = 0; i < CHUNK_COUNT; i++) {
168
+ const c = BigInt(chunks[i]);
169
+ if (c < 0n || c >= TWO_248) {
170
+ throw new RangeError(`chunksToBytes: chunk ${i} outside [0, 2^248)`);
171
+ }
172
+ out.set(bigIntToBytesLE(c, CHUNK_BYTES), i * CHUNK_BYTES);
173
+ }
174
+ return out.subarray(0, byteLength);
175
+ }
176
+ /** Alias kept for the spec wording (bytesFromChunks ≡ chunksToBytes). */
177
+ export const bytesFromChunks = chunksToBytes;
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // BabyJubJub helpers (circomlib babyjub — BabyJubJub on BN254; the curve base
181
+ // field IS the circuit scalar field, so coordinates are circuit-native).
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /** Internal: normalize a point given as {x,y} (bigint|string|number) or [x,y]. */
185
+ function toPoint(p, what = "point") {
186
+ let x;
187
+ let y;
188
+ if (Array.isArray(p)) {
189
+ [x, y] = p;
190
+ } else if (p && typeof p === "object") {
191
+ ({ x, y } = p);
192
+ }
193
+ if (x === undefined || y === undefined) {
194
+ throw new TypeError(`${what}: expected {x, y} or [x, y]`);
195
+ }
196
+ return [BigInt(x), BigInt(y)];
197
+ }
198
+
199
+ /**
200
+ * Encode a BabyJub point as decimal-string coordinates.
201
+ * @param {bigint[]|{x:bigint,y:bigint}} p
202
+ * @returns {{x: string, y: string}}
203
+ */
204
+ export function encodePoint(p) {
205
+ const [x, y] = Array.isArray(p) ? p.map((c) => JF.toObject(c)) : toPoint(p);
206
+ return { x: BigInt(x).toString(10), y: BigInt(y).toString(10) };
207
+ }
208
+
209
+ /**
210
+ * Decode decimal-string coordinates → [x, y] bigints. Does NOT validate
211
+ * curve membership (use validateBuyerPublicKey for that).
212
+ */
213
+ export function decodePoint(p) {
214
+ return toPoint(p);
215
+ }
216
+
217
+ /**
218
+ * Buyer public-key validation matching the circuit EXACTLY:
219
+ * 1. coordinates canonical (< r — circuit signals are field elements);
220
+ * 2. on-curve via the BabyJubJub equation (circomlib BabyCheck:
221
+ * a·x² + y² = 1 + d·x²·y² with a=168700, d=168696);
222
+ * 3. not small-order: B8 = 8·B must have B8.x ≠ 0. The only on-curve
223
+ * points with x = 0 are identity (0,1) and the order-2 point (0,−1);
224
+ * since the curve has no order-16 points, 8·B ∈ {(0,1),(0,−1)} iff B
225
+ * lies in the 8-torsion — so x ≠ 0 ⟺ B is not small-order.
226
+ * Throws on any failure; returns the point as [x, y] bigints on success.
227
+ */
228
+ export function validateBuyerPublicKey(pub, what = "buyerPub") {
229
+ const [x, y] = toPoint(pub, what);
230
+ if (x < 0n || x >= R || y < 0n || y >= R) {
231
+ throw new RangeError(`${what}: coordinates must be canonical field elements (< r)`);
232
+ }
233
+ const P = [JF.e(x), JF.e(y)];
234
+ if (!babyJub.inCurve(P)) {
235
+ throw new Error(`${what}: point is not on the BabyJubJub curve`);
236
+ }
237
+ const P8 = babyJub.mulPointEscalar(P, 8n);
238
+ if (JF.isZero(P8[0])) {
239
+ throw new Error(`${what}: small-order point rejected (8·B has x = 0)`);
240
+ }
241
+ return [x, y];
242
+ }
243
+
244
+ /**
245
+ * Generate a buyer BabyJubJub key pair.
246
+ * @returns {{ secret: bigint, publicKey: {x: bigint, y: bigint} }}
247
+ * secret ∈ [1, subOrder); publicKey = secret·Base8.
248
+ */
249
+ export function generateBuyerKeyPair() {
250
+ let secret = 0n;
251
+ while (secret === 0n) {
252
+ secret = bytesToBigIntLE(randomBytes(32)) % SUB_ORDER;
253
+ }
254
+ const P = babyJub.mulPointEscalar(babyJub.Base8, secret);
255
+ return { secret, publicKey: { x: JF.toObject(P[0]), y: JF.toObject(P[1]) } };
256
+ }
257
+
258
+ /**
259
+ * Buyer-side ECDH: shared = buyerSecret · epk; ss = Poseidon2(shared.x, shared.y).
260
+ * Validates epk with the same rules the circuit applies to the buyer key.
261
+ * @returns {{ ss: bigint, shared: {x: bigint, y: bigint} }}
262
+ */
263
+ export function deriveSharedSecret(buyerSecret, epk) {
264
+ const [x, y] = validateBuyerPublicKey(epk, "epk");
265
+ const P = babyJub.mulPointEscalar([JF.e(x), JF.e(y)], BigInt(buyerSecret));
266
+ const shared = { x: JF.toObject(P[0]), y: JF.toObject(P[1]) };
267
+ return { ss: poseidon([shared.x, shared.y]), shared };
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Poseidon-CTR stream cipher (field-add; mirrors circuit Leg B)
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /** Keystream chunk: ks_i = Poseidon4(dekLo, dekHi, nonce, i). */
275
+ export function keystreamChunk(dekLo, dekHi, nonce, i) {
276
+ return poseidon([BigInt(dekLo), BigInt(dekHi), BigInt(nonce), BigInt(i)]);
277
+ }
278
+
279
+ /**
280
+ * Encrypt plaintext chunks: c_i = p_i + Poseidon4(dekLo, dekHi, nonce, i) (mod r).
281
+ * @param {bigint[]|string[]|number[]} plaintextChunks CHUNK_COUNT limbs
282
+ * @returns {bigint[]} CHUNK_COUNT ciphertext chunks
283
+ */
284
+ export function encryptPayload(plaintextChunks, dekLo, dekHi, nonce) {
285
+ if (!Array.isArray(plaintextChunks) || plaintextChunks.length !== CHUNK_COUNT) {
286
+ throw new TypeError(`encryptPayload: expected ${CHUNK_COUNT} plaintext chunks`);
287
+ }
288
+ const out = new Array(CHUNK_COUNT);
289
+ for (let i = 0; i < CHUNK_COUNT; i++) {
290
+ out[i] = (BigInt(plaintextChunks[i]) + keystreamChunk(dekLo, dekHi, nonce, i)) % R;
291
+ }
292
+ return out;
293
+ }
294
+
295
+ /**
296
+ * Decrypt ciphertext chunks: p_i = c_i − Poseidon4(dekLo, dekHi, nonce, i) (mod r).
297
+ * @param {bigint[]|string[]|number[]} ciphertextChunks CHUNK_COUNT limbs
298
+ * @returns {bigint[]} CHUNK_COUNT plaintext chunks
299
+ */
300
+ export function decryptPayload(ciphertextChunks, dekLo, dekHi, nonce) {
301
+ if (!Array.isArray(ciphertextChunks) || ciphertextChunks.length !== CHUNK_COUNT) {
302
+ throw new TypeError(`decryptPayload: expected ${CHUNK_COUNT} ciphertext chunks`);
303
+ }
304
+ const out = new Array(CHUNK_COUNT);
305
+ for (let i = 0; i < CHUNK_COUNT; i++) {
306
+ out[i] = (BigInt(ciphertextChunks[i]) - keystreamChunk(dekLo, dekHi, nonce, i)) % R;
307
+ if (out[i] < 0n) out[i] += R;
308
+ }
309
+ return out;
310
+ }
311
+
312
+ /**
313
+ * Chunk-commitment fold: acc_0 = 0; acc_{i+1} = Poseidon2(acc_i, chunk_i).
314
+ * Used for BOTH payloadCommitPos (plaintext fold, public) and cipherCommit
315
+ * (ciphertext fold, private).
316
+ */
317
+ /** DEK-blinded, domain-separated plaintext fold (mirrors circuit Leg B init).
318
+ * payloadCommitPos = fold(Poseidon2, Poseidon3(DOMAIN_PSALT, dekLo, dekHi), chunks).
319
+ * The seed is secret until the paid claim reveal, so neither observers nor the
320
+ * pre-payment buyer can dictionary-test the public digest. */
321
+ export function foldChunksBlind(chunks, dekLo, dekHi) {
322
+ let acc = poseidon([DOMAIN_PSALT, BigInt(dekLo), BigInt(dekHi)]);
323
+ for (const c of chunks) acc = poseidon([acc, BigInt(c)]);
324
+ return acc;
325
+ }
326
+
327
+ export function foldChunks(chunks) {
328
+ if (!Array.isArray(chunks) || chunks.length !== CHUNK_COUNT) {
329
+ throw new TypeError(`foldChunks: expected ${CHUNK_COUNT} chunks`);
330
+ }
331
+ let acc = 0n;
332
+ for (const c of chunks) acc = poseidon([acc, BigInt(c)]);
333
+ return acc;
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Poseidon commitments (mirror the circuit exactly)
338
+ // ---------------------------------------------------------------------------
339
+
340
+ /** keyCommit = Poseidon2(dekLo, dekHi). */
341
+ export function computeKeyCommit(dekLo, dekHi) {
342
+ return poseidon([BigInt(dekLo), BigInt(dekHi)]);
343
+ }
344
+
345
+ /** Sale note leaf = Poseidon3(ownerKey, rho, payloadCommitPos) — insert into NoteTree. */
346
+ export function computeSaleNote(ownerKey, rho, payloadCommitPos) {
347
+ return poseidon([BigInt(ownerKey), BigInt(rho), BigInt(payloadCommitPos)]);
348
+ }
349
+
350
+ /** base58 → exactly `len` bytes (Solana pubkey / fixed-size buffer). */
351
+ function decodeBase58Fixed(b58, len, what) {
352
+ if (typeof b58 !== "string" || b58.length === 0) {
353
+ throw new TypeError(`${what}: expected a non-empty base58 string`);
354
+ }
355
+ const bytes = bs58.decode(b58);
356
+ if (bytes.length !== len) {
357
+ throw new Error(`${what}: expected ${len} bytes, got ${bytes.length}`);
358
+ }
359
+ return bytes;
360
+ }
361
+
362
+ /**
363
+ * saleTermsHash = Poseidon9(DOMAIN_SALE, sellerLo, sellerHi, buyerPubX,
364
+ * buyerPubY, mintLo, mintHi, price, expirySlot). seller/mint: base58 32-byte
365
+ * Solana pubkeys split into little-endian 128-bit limbs; buyerPub: BabyJub
366
+ * public key (validated exactly as the circuit validates it); price: u64
367
+ * lamports; expirySlot < 2^48.
368
+ * @returns {{ saleTermsHash: bigint, sellerLo: bigint, sellerHi: bigint,
369
+ * buyerPubX: bigint, buyerPubY: bigint, mintLo: bigint, mintHi: bigint,
370
+ * price: bigint, expirySlot: bigint }}
371
+ */
372
+ export function computeSaleTermsHash({ sellerPubkey, buyerPub, mint, priceLamports, expirySlot, payloadLen }) {
373
+ const sBytes = decodeBase58Fixed(sellerPubkey, 32, "sellerPubkey");
374
+ const mBytes = decodeBase58Fixed(mint, 32, "mint");
375
+ const [buyerPubX, buyerPubY] = validateBuyerPublicKey(buyerPub);
376
+ const price = BigInt(priceLamports);
377
+ const expiry = BigInt(expirySlot);
378
+ if (price < 0n || price >= TWO_64) {
379
+ throw new RangeError(`priceLamports ${price} out of u64 range`);
380
+ }
381
+ if (expiry < 0n || expiry >= TWO_48) {
382
+ throw new RangeError(`expirySlot ${expiry} out of 48-bit range`);
383
+ }
384
+ if (payloadLen === undefined || payloadLen === null) {
385
+ throw new TypeError("computeSaleTermsHash: payloadLen required (bound into the terms)");
386
+ }
387
+ const length = BigInt(payloadLen);
388
+ if (length < 1n || length > BigInt(PAYLOAD_CAPACITY)) {
389
+ throw new RangeError(`payloadLen ${length} out of range 1..${PAYLOAD_CAPACITY}`);
390
+ }
391
+ const limbs = {
392
+ sellerLo: bytesToBigIntLE(sBytes.subarray(0, 16)),
393
+ sellerHi: bytesToBigIntLE(sBytes.subarray(16, 32)),
394
+ buyerPubX,
395
+ buyerPubY,
396
+ mintLo: bytesToBigIntLE(mBytes.subarray(0, 16)),
397
+ mintHi: bytesToBigIntLE(mBytes.subarray(16, 32)),
398
+ price,
399
+ expirySlot: expiry,
400
+ payloadLen: length,
401
+ };
402
+ const saleTermsHash = poseidon([
403
+ DOMAIN_SALE,
404
+ limbs.sellerLo,
405
+ limbs.sellerHi,
406
+ limbs.buyerPubX,
407
+ limbs.buyerPubY,
408
+ limbs.mintLo,
409
+ limbs.mintHi,
410
+ limbs.price,
411
+ limbs.expirySlot,
412
+ limbs.payloadLen,
413
+ ]);
414
+ return { saleTermsHash, ...limbs };
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // Envelope (Leg D) — seller-side wrap, buyer-side unwrap
419
+ // ---------------------------------------------------------------------------
420
+
421
+ /** Field add mod r. */
422
+ const fadd = (a, b) => (BigInt(a) + BigInt(b)) % R;
423
+ /** Field sub mod r. */
424
+ const fsub = (a, b) => {
425
+ const v = (BigInt(a) - BigInt(b)) % R;
426
+ return v < 0n ? v + R : v;
427
+ };
428
+
429
+ /**
430
+ * Seller-side DEK wrap to the buyer's public key.
431
+ * @returns {{ epk: {x: bigint, y: bigint}, encDEK_lo: bigint, encDEK_hi: bigint,
432
+ * envelopeCommit: bigint, ss: bigint }}
433
+ */
434
+ function wrapDek({ dekLo, dekHi, nonce, buyerPub, esk }) {
435
+ const [bx, by] = validateBuyerPublicKey(buyerPub);
436
+ if (esk < 0n || esk >= TWO_253) {
437
+ throw new RangeError(`esk out of range: circuit constrains Num2Bits(253)`);
438
+ }
439
+ if (esk % SUB_ORDER === 0n) {
440
+ // esk = 0 or esk = subOrder → epk = identity → shared secret publicly
441
+ // derivable → envelope opens for ANYONE. The circuit rejects this too
442
+ // (epk.x ≠ 0), fail here first with a clear reason.
443
+ throw new RangeError('esk must not be 0 mod subOrder (epk would be the identity point)');
444
+ }
445
+ if (nonce === undefined || nonce === null) {
446
+ throw new TypeError('wrapDek: nonce required — it is bound into envelopeCommit');
447
+ }
448
+ const B = [JF.e(bx), JF.e(by)];
449
+ const epkP = babyJub.mulPointEscalar(babyJub.Base8, esk);
450
+ const sharedP = babyJub.mulPointEscalar(B, esk);
451
+ const shared = { x: JF.toObject(sharedP[0]), y: JF.toObject(sharedP[1]) };
452
+ const ss = poseidon([shared.x, shared.y]);
453
+ const encDEK_lo = fadd(dekLo, poseidon([ss, 0n]));
454
+ const encDEK_hi = fadd(dekHi, poseidon([ss, 1n]));
455
+ const epk = { x: JF.toObject(epkP[0]), y: JF.toObject(epkP[1]) };
456
+ // Poseidon5 binds the CTR nonce: the claim-time reveal is the only (epk,
457
+ // encDEK, nonce) triple the proof could have committed to.
458
+ const envelopeCommit = poseidon([epk.x, epk.y, encDEK_lo, encDEK_hi, BigInt(nonce)]);
459
+ return { epk, encDEK_lo, encDEK_hi, envelopeCommit, ss };
460
+ }
461
+
462
+ /**
463
+ * Buyer-side DEK unwrap: shared = buyerSecret·epk; ss = Poseidon2(shared.x,
464
+ * shared.y); dekLo = encDEK_lo − Poseidon2(ss, 0); dekHi = encDEK_hi −
465
+ * Poseidon2(ss, 1) (all mod r).
466
+ * @param {bigint|string} buyerSecret buyer's BabyJub secret scalar
467
+ * @param {{x,y}|[x,y]} epk ephemeral public key (validated like the circuit
468
+ * validates the buyer key: on-curve, not small-order)
469
+ * @returns {{ dekLo: bigint, dekHi: bigint, ss: bigint }}
470
+ */
471
+ export function openSaleEnvelope(buyerSecret, epk, encDEK_lo, encDEK_hi) {
472
+ const { ss } = deriveSharedSecret(buyerSecret, epk);
473
+ const dekLo = fsub(encDEK_lo, poseidon([ss, 0n]));
474
+ const dekHi = fsub(encDEK_hi, poseidon([ss, 1n]));
475
+ return { dekLo, dekHi, ss };
476
+ }
477
+
478
+ /**
479
+ * Full buyer-side open. Verifies, IN ORDER:
480
+ * 1. JUNK-DELIVERY DEFENSE: the delivered ciphertext chunks fold to the
481
+ * PUBLIC cipherCommit (public input index 2). This is the check the
482
+ * buyer runs against the on-chain listing BEFORE paying — a seller who
483
+ * delivers bytes that do not fold to the committed cipherCommit is
484
+ * caught here, before any decryption.
485
+ * 2. Envelope unwrap (ECDH) → DEK.
486
+ * 3. Decrypt → assert the plaintext fold equals the listed
487
+ * payloadCommitPos. A third party with the wrong buyer secret derives
488
+ * the wrong DEK → wrong plaintext → this assert fails.
489
+ *
490
+ * @param {object} args
491
+ * @param {bigint|string} args.buyerSecret
492
+ * @param {{x,y}|[x,y]} args.epk
493
+ * @param {bigint|string} args.encDEK_lo
494
+ * @param {bigint|string} args.encDEK_hi
495
+ * @param {bigint[]|string[]} args.ciphertext CHUNK_COUNT delivered ciphertext chunks
496
+ * @param {bigint|string} args.cipherCommit PUBLIC ciphertext fold (on-chain listing)
497
+ * @param {bigint|string} args.nonce Poseidon-CTR nonce
498
+ * @param {bigint|string} args.payloadCommitPos listed plaintext commitment
499
+ * @returns {{ dekLo: bigint, dekHi: bigint, plaintextChunks: bigint[],
500
+ * cipherCommit: bigint, payloadCommitPos: bigint }}
501
+ */
502
+ export function openAndVerifySale(args) {
503
+ if (args.cipherCommit === undefined || args.cipherCommit === null) {
504
+ throw new TypeError("openAndVerifySale: missing cipherCommit (public ciphertext fold)");
505
+ }
506
+ const cipherCommit = foldChunks(args.ciphertext);
507
+ if (cipherCommit !== BigInt(args.cipherCommit)) {
508
+ throw new Error(
509
+ "junk delivery: delivered ciphertext fold != public cipherCommit — do not pay",
510
+ );
511
+ }
512
+ const { dekLo, dekHi } = openSaleEnvelope(
513
+ args.buyerSecret,
514
+ args.epk,
515
+ args.encDEK_lo,
516
+ args.encDEK_hi,
517
+ );
518
+ const plaintextChunks = decryptPayload(args.ciphertext, dekLo, dekHi, args.nonce);
519
+ const digest = foldChunksBlind(plaintextChunks, dekLo, dekHi);
520
+ if (digest !== BigInt(args.payloadCommitPos)) {
521
+ throw new Error(
522
+ "sale payload digest mismatch: decrypted blinded fold != listed payloadCommitPos",
523
+ );
524
+ }
525
+ return { dekLo, dekHi, plaintextChunks, cipherCommit, payloadCommitPos: digest };
526
+ }
527
+
528
+ // ---------------------------------------------------------------------------
529
+ // Witness builder
530
+ // ---------------------------------------------------------------------------
531
+
532
+ /** Recursively convert bigints → decimal strings for snarkjs input JSON. */
533
+ function toDec(x) {
534
+ if (typeof x === "bigint") return x.toString(10);
535
+ if (Array.isArray(x)) return x.map(toDec);
536
+ if (typeof x === "number") return String(x);
537
+ if (typeof x === "object" && x !== null) {
538
+ return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, toDec(v)]));
539
+ }
540
+ return x;
541
+ }
542
+
543
+ function need(value, name) {
544
+ if (value === undefined || value === null) {
545
+ throw new TypeError(`buildSaleWitness: missing ${name}`);
546
+ }
547
+ return BigInt(value);
548
+ }
549
+
550
+ /**
551
+ * Build the snarkjs input for one sealed-sale release proof.
552
+ *
553
+ * @param {object} args
554
+ * @param {NoteTree} args.noteTree shared note tree at the current on-chain note_root
555
+ * @param {number} args.noteIndex index of the sale note in noteTree
556
+ * @param {bigint|number|string} args.ownerKey note opening
557
+ * @param {bigint|number|string} args.rho note opening
558
+ * @param {Uint8Array} args.plaintextBytes E2EE memory plaintext (≤ 992 bytes)
559
+ * @param {bigint|number|string} args.dekLo DEK low limb (< 2^128)
560
+ * @param {bigint|number|string} args.dekHi DEK high limb (< 2^128)
561
+ * @param {bigint|number|string} args.nonce Poseidon-CTR nonce (field element)
562
+ * @param {{x,y}|[x,y]} args.buyerPub buyer BabyJub public key (decimal strings or bigints)
563
+ * @param {string} args.sellerPubkey seller base58 32B pubkey
564
+ * @param {string} args.mint payment mint, base58 32B pubkey
565
+ * @param {bigint|number|string} args.priceLamports price (u64)
566
+ * @param {bigint|number|string} args.expirySlot escrow expiry slot (< 2^48)
567
+ * @param {bigint|number|string} [args.esk] ephemeral secret (< 2^253); random if omitted
568
+ * @returns {{
569
+ * input: object, // snarkjs input JSON (decimal strings), exact circuit signal names
570
+ * note: bigint, noteRoot: bigint,
571
+ * plaintextChunks: bigint[], ciphertextChunks: bigint[],
572
+ * payloadCommitPos: bigint, cipherCommit: bigint, keyCommit: bigint,
573
+ * saleTermsHash: bigint, envelopeCommit: bigint,
574
+ * epk: {x: bigint, y: bigint}, encDEK_lo: bigint, encDEK_hi: bigint,
575
+ * esk: bigint,
576
+ * sellerLo: bigint, sellerHi: bigint, buyerPubX: bigint, buyerPubY: bigint,
577
+ * mintLo: bigint, mintHi: bigint, price: bigint, expirySlot: bigint,
578
+ * }}
579
+ */
580
+ export function buildSaleWitness(args) {
581
+ const { noteTree, noteIndex } = args;
582
+ if (!(noteTree instanceof NoteTree)) throw new TypeError("noteTree must be a NoteTree");
583
+ if (!Number.isInteger(noteIndex) || noteIndex < 0 || noteIndex >= noteTree.nextIndex) {
584
+ throw new RangeError(`noteIndex ${noteIndex} not present in note tree`);
585
+ }
586
+
587
+ const ownerKey = need(args.ownerKey, "ownerKey");
588
+ const rho = need(args.rho, "rho");
589
+ const dekLo = need(args.dekLo, "dekLo");
590
+ const dekHi = need(args.dekHi, "dekHi");
591
+ const nonce = need(args.nonce, "nonce");
592
+
593
+ if (dekLo < 0n || dekLo >= TWO_128 || dekHi < 0n || dekHi >= TWO_128) {
594
+ throw new RangeError("dekLo/dekHi must each be < 2^128 (circuit Num2Bits(128))");
595
+ }
596
+ if (nonce < 0n || nonce >= R) throw new RangeError("nonce must be a field element");
597
+ if (ownerKey < 0n || ownerKey >= R || rho < 0n || rho >= R) {
598
+ throw new RangeError("ownerKey/rho must be field elements");
599
+ }
600
+ if (args.plaintextBytes === undefined || args.plaintextBytes === null) {
601
+ throw new TypeError("buildSaleWitness: missing plaintextBytes");
602
+ }
603
+ const payloadLen = BigInt(args.plaintextBytes.length);
604
+ if (payloadLen < 1n || payloadLen > BigInt(PAYLOAD_CAPACITY)) {
605
+ throw new RangeError(
606
+ `buildSaleWitness: payload length ${payloadLen} out of range 1..${PAYLOAD_CAPACITY}`,
607
+ );
608
+ }
609
+ if (args.buyerPub === undefined || args.buyerPub === null) {
610
+ throw new TypeError("buildSaleWitness: missing buyerPub");
611
+ }
612
+
613
+ // ---- Leg B: Poseidon-CTR encryption + folds ----
614
+ // The plaintext fold is blinded by a DEK-derived seed — no separate salt.
615
+ const plaintextChunks = bytesToChunks(args.plaintextBytes);
616
+ const ciphertextChunks = encryptPayload(plaintextChunks, dekLo, dekHi, nonce);
617
+ const payloadCommitPos = foldChunksBlind(plaintextChunks, dekLo, dekHi);
618
+ const cipherCommit = foldChunks(ciphertextChunks);
619
+
620
+ // ---- Leg C: key commitment ----
621
+ const keyCommit = computeKeyCommit(dekLo, dekHi);
622
+
623
+ // ---- Leg A: note membership ----
624
+ const note = computeSaleNote(ownerKey, rho, payloadCommitPos);
625
+ if (noteTree.leaves[noteIndex] !== note) {
626
+ throw new Error("note opening does not match the leaf at noteIndex");
627
+ }
628
+ const noteProof = noteTree.getProof(noteIndex);
629
+ const noteRoot = noteTree.root();
630
+
631
+ // ---- Leg D: recipient-bound envelope ----
632
+ let esk;
633
+ if (args.esk !== undefined && args.esk !== null) {
634
+ esk = BigInt(args.esk);
635
+ } else {
636
+ do {
637
+ esk = bytesToBigIntLE(randomBytes(32)) % TWO_253;
638
+ } while (esk % SUB_ORDER === 0n);
639
+ }
640
+ const [buyerPubX, buyerPubY] = validateBuyerPublicKey(args.buyerPub);
641
+ const { epk, encDEK_lo, encDEK_hi, envelopeCommit } = wrapDek({
642
+ dekLo,
643
+ dekHi,
644
+ nonce,
645
+ buyerPub: [buyerPubX, buyerPubY],
646
+ esk,
647
+ });
648
+
649
+ // ---- Leg E: sale terms (buyer- and length-bound: Poseidon10 over seller,
650
+ // buyer key, mint, price, expiry, payloadLen) ----
651
+ const terms = computeSaleTermsHash({
652
+ sellerPubkey: args.sellerPubkey,
653
+ buyerPub: [buyerPubX, buyerPubY],
654
+ mint: args.mint,
655
+ priceLamports: args.priceLamports,
656
+ expirySlot: args.expirySlot,
657
+ payloadLen,
658
+ });
659
+
660
+ const input = toDec({
661
+ // public (exact contract order/names)
662
+ noteRoot,
663
+ payloadCommitPos,
664
+ cipherCommit,
665
+ keyCommit,
666
+ saleTermsHash: terms.saleTermsHash,
667
+ envelopeCommit,
668
+ // private
669
+ ownerKey,
670
+ rho,
671
+ notePathElements: noteProof.pathElements,
672
+ notePathIndices: noteProof.pathIndices,
673
+ plaintext: plaintextChunks,
674
+ ciphertext: ciphertextChunks,
675
+ nonce,
676
+ dekLo,
677
+ dekHi,
678
+ buyerPubX,
679
+ buyerPubY,
680
+ esk,
681
+ sellerLo: terms.sellerLo,
682
+ sellerHi: terms.sellerHi,
683
+ mintLo: terms.mintLo,
684
+ mintHi: terms.mintHi,
685
+ price: terms.price,
686
+ expirySlot: terms.expirySlot,
687
+ payloadLen,
688
+ });
689
+
690
+ return {
691
+ input,
692
+ note,
693
+ noteRoot,
694
+ payloadLen,
695
+ plaintextChunks,
696
+ ciphertextChunks,
697
+ payloadCommitPos,
698
+ cipherCommit,
699
+ keyCommit,
700
+ saleTermsHash: terms.saleTermsHash,
701
+ envelopeCommit,
702
+ epk,
703
+ encDEK_lo,
704
+ encDEK_hi,
705
+ esk,
706
+ sellerLo: terms.sellerLo,
707
+ sellerHi: terms.sellerHi,
708
+ buyerPubX: terms.buyerPubX,
709
+ buyerPubY: terms.buyerPubY,
710
+ mintLo: terms.mintLo,
711
+ mintHi: terms.mintHi,
712
+ price: terms.price,
713
+ expirySlot: terms.expirySlot,
714
+ };
715
+ }
716
+
717
+ // ---------------------------------------------------------------------------
718
+ // Deterministic demo fixture (proof-of-life / setup-sale.mjs)
719
+ // ---------------------------------------------------------------------------
720
+
721
+ /**
722
+ * Deterministic end-to-end sale fixture: a note tree holding the sale note,
723
+ * a fixed buyer key pair, a fixed DEK/esk, and the full witness input.
724
+ * @returns {Promise<{
725
+ * input: object, // complete circom witness input (decimal strings)
726
+ * expectedPublic: string[], // the 5 public signals, SALE_PUBLIC_SIGNALS order
727
+ * artifacts: object, // full buildSaleWitness result (epk, encDEK_*, ...)
728
+ * buyerSecret: bigint, // demo buyer secret (envelope opening)
729
+ * plaintextBytes: Uint8Array, // demo plaintext
730
+ * }>}
731
+ */
732
+ export async function buildSaleDemoFixture() {
733
+ const buyer = generateBuyerKeyPairDeterministic();
734
+ const plaintextBytes = new TextEncoder().encode(
735
+ "Enigma sealed-sale demo memory: the cortex remembers what the chain never sees.",
736
+ );
737
+ const dekLo = 0x0de0b6b3a7640000f1f2f3f4f5f6f7f8n; // < 2^128
738
+ const dekHi = 0x00112233445566778899aabbccddeeffn; // < 2^128
739
+ const nonce = 424242424242n;
740
+ const esk = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403n; // < 2^253
741
+ const ownerKey = 11111111111111111111111111111111111111111111111111n;
742
+ const rho = 22222222222222222222222222222222222222222222222222n;
743
+
744
+ // Pre-compute the note so the tree holds the exact leaf the witness opens.
745
+ const plaintextChunks = bytesToChunks(plaintextBytes);
746
+ const payloadCommitPos = foldChunksBlind(plaintextChunks, dekLo, dekHi);
747
+ const noteTree = new NoteTree();
748
+ noteTree.insert(555n); // filler note at index 0
749
+ noteTree.insert(computeSaleNote(ownerKey, rho, payloadCommitPos)); // index 1
750
+
751
+ const result = buildSaleWitness({
752
+ noteTree,
753
+ noteIndex: 1,
754
+ ownerKey,
755
+ rho,
756
+ plaintextBytes,
757
+ dekLo,
758
+ dekHi,
759
+ nonce,
760
+ buyerPub: buyer.publicKey,
761
+ sellerPubkey: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint addr as seller
762
+ mint: "So11111111111111111111111111111111111111112", // wSOL mint
763
+ priceLamports: 2_000_000_000n, // 2 SOL
764
+ expirySlot: 300_000_000n,
765
+ esk,
766
+ });
767
+
768
+ const expectedPublic = SALE_PUBLIC_SIGNALS.map((name) => result.input[name]);
769
+ return {
770
+ input: result.input,
771
+ expectedPublic,
772
+ artifacts: result,
773
+ buyerSecret: buyer.secret,
774
+ plaintextBytes,
775
+ };
776
+ }
777
+
778
+ /** Deterministic buyer key pair for the demo fixture (fixed secret). */
779
+ function generateBuyerKeyPairDeterministic() {
780
+ const secret = 0x0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252n % SUB_ORDER;
781
+ const P = babyJub.mulPointEscalar(babyJub.Base8, secret);
782
+ return { secret, publicKey: { x: JF.toObject(P[0]), y: JF.toObject(P[1]) } };
783
+ }