@supercuts-walrus/backend 0.0.0-stage → 0.1.0

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 (149) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +249 -2
  3. package/dist/audit.d.ts +52 -0
  4. package/dist/audit.js +157 -0
  5. package/dist/backend.d.ts +27 -0
  6. package/dist/backend.js +62 -0
  7. package/dist/buyer-setup.d.ts +6 -0
  8. package/dist/buyer-setup.js +241 -0
  9. package/dist/capability.d.ts +32 -0
  10. package/dist/capability.js +87 -0
  11. package/dist/claude.d.ts +44 -0
  12. package/dist/claude.js +98 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +29 -0
  15. package/dist/config.d.ts +20 -0
  16. package/dist/config.js +58 -0
  17. package/dist/dashboard.d.ts +5 -0
  18. package/dist/dashboard.js +21 -0
  19. package/dist/deal-adapter.d.ts +3 -0
  20. package/dist/deal-adapter.js +39 -0
  21. package/dist/deal-engine.d.ts +62 -0
  22. package/dist/deal-engine.js +211 -0
  23. package/dist/deal-wire.d.ts +16 -0
  24. package/dist/deal-wire.js +65 -0
  25. package/dist/deal.d.ts +79 -0
  26. package/dist/deal.js +228 -0
  27. package/dist/demo.d.ts +2 -0
  28. package/dist/demo.js +449 -0
  29. package/dist/errors.d.ts +6 -0
  30. package/dist/errors.js +13 -0
  31. package/dist/events.d.ts +27 -0
  32. package/dist/events.js +68 -0
  33. package/dist/facilitator.d.ts +21 -0
  34. package/dist/facilitator.js +173 -0
  35. package/dist/finality.d.ts +2 -0
  36. package/dist/finality.js +27 -0
  37. package/dist/hosted-exit-cli.d.ts +2 -0
  38. package/dist/hosted-exit-cli.js +37 -0
  39. package/dist/hosted-exit.d.ts +24 -0
  40. package/dist/hosted-exit.js +70 -0
  41. package/dist/hosted-seller-import.d.ts +2 -0
  42. package/dist/hosted-seller-import.js +70 -0
  43. package/dist/hosted-seller-repository.d.ts +31 -0
  44. package/dist/hosted-seller-repository.js +214 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.js +37 -0
  47. package/dist/indexer.d.ts +63 -0
  48. package/dist/indexer.js +60 -0
  49. package/dist/live-deal-ports.d.ts +41 -0
  50. package/dist/live-deal-ports.js +212 -0
  51. package/dist/live-index.d.ts +28 -0
  52. package/dist/live-index.js +149 -0
  53. package/dist/live-runtime.d.ts +82 -0
  54. package/dist/live-runtime.js +570 -0
  55. package/dist/live-seller-transport.d.ts +30 -0
  56. package/dist/live-seller-transport.js +169 -0
  57. package/dist/live-seller-verifier.d.ts +11 -0
  58. package/dist/live-seller-verifier.js +65 -0
  59. package/dist/mandate.d.ts +34 -0
  60. package/dist/mandate.js +178 -0
  61. package/dist/mcp-bootstrap.d.ts +2 -0
  62. package/dist/mcp-bootstrap.js +39 -0
  63. package/dist/mcp.d.ts +2 -0
  64. package/dist/mcp.js +187 -0
  65. package/dist/memory-worker.d.ts +2 -0
  66. package/dist/memory-worker.js +38 -0
  67. package/dist/memory.d.ts +15 -0
  68. package/dist/memory.js +17 -0
  69. package/dist/memwal.d.ts +35 -0
  70. package/dist/memwal.js +83 -0
  71. package/dist/negotiation.d.ts +23 -0
  72. package/dist/negotiation.js +58 -0
  73. package/dist/notes.d.ts +11 -0
  74. package/dist/notes.js +26 -0
  75. package/dist/onboarding-doctor.d.ts +1 -0
  76. package/dist/onboarding-doctor.js +76 -0
  77. package/dist/phase3.d.ts +100 -0
  78. package/dist/phase3.js +1144 -0
  79. package/dist/procurement.d.ts +8 -0
  80. package/dist/procurement.js +19 -0
  81. package/dist/protocol-engine.d.ts +121 -0
  82. package/dist/protocol-engine.js +149 -0
  83. package/dist/prover.d.ts +28 -0
  84. package/dist/prover.js +70 -0
  85. package/dist/relayer.d.ts +32 -0
  86. package/dist/relayer.js +43 -0
  87. package/dist/remote-buyer.d.ts +67 -0
  88. package/dist/remote-buyer.js +231 -0
  89. package/dist/resource-server.d.ts +2 -0
  90. package/dist/resource-server.js +24 -0
  91. package/dist/resource.d.ts +22 -0
  92. package/dist/resource.js +55 -0
  93. package/dist/seam.d.ts +2 -0
  94. package/dist/seam.js +117 -0
  95. package/dist/seller-catalog-generate.d.ts +2 -0
  96. package/dist/seller-catalog-generate.js +68 -0
  97. package/dist/seller-database.d.ts +41 -0
  98. package/dist/seller-database.js +80 -0
  99. package/dist/seller-directory-cli.d.ts +2 -0
  100. package/dist/seller-directory-cli.js +24 -0
  101. package/dist/seller-directory.d.ts +5 -0
  102. package/dist/seller-directory.js +50 -0
  103. package/dist/seller-live-cli.d.ts +2 -0
  104. package/dist/seller-live-cli.js +53 -0
  105. package/dist/seller-provision.d.ts +2 -0
  106. package/dist/seller-provision.js +84 -0
  107. package/dist/seller-register-cli.d.ts +2 -0
  108. package/dist/seller-register-cli.js +40 -0
  109. package/dist/seller-runtime.d.ts +60 -0
  110. package/dist/seller-runtime.js +96 -0
  111. package/dist/seller-service.d.ts +34 -0
  112. package/dist/seller-service.js +416 -0
  113. package/dist/seller-state.d.ts +19 -0
  114. package/dist/seller-state.js +60 -0
  115. package/dist/seller-stdio.d.ts +6 -0
  116. package/dist/seller-stdio.js +29 -0
  117. package/dist/seller-v2-provision.d.ts +2 -0
  118. package/dist/seller-v2-provision.js +71 -0
  119. package/dist/seller-watch.d.ts +2 -0
  120. package/dist/seller-watch.js +45 -0
  121. package/dist/settlement.d.ts +95 -0
  122. package/dist/settlement.js +141 -0
  123. package/dist/store.d.ts +36 -0
  124. package/dist/store.js +193 -0
  125. package/dist/sui.d.ts +49 -0
  126. package/dist/sui.js +77 -0
  127. package/dist/tatum.d.ts +17 -0
  128. package/dist/tatum.js +83 -0
  129. package/dist/transcript.d.ts +31 -0
  130. package/dist/transcript.js +52 -0
  131. package/dist/v2-exit-relayer.d.ts +34 -0
  132. package/dist/v2-exit-relayer.js +102 -0
  133. package/dist/v2-exit-worker.d.ts +2 -0
  134. package/dist/v2-exit-worker.js +97 -0
  135. package/dist/v2-exits.d.ts +30 -0
  136. package/dist/v2-exits.js +78 -0
  137. package/dist/v2-prefund.d.ts +19 -0
  138. package/dist/v2-prefund.js +43 -0
  139. package/dist/v2-relayer-cli.d.ts +2 -0
  140. package/dist/v2-relayer-cli.js +39 -0
  141. package/dist/wallet.d.ts +32 -0
  142. package/dist/wallet.js +90 -0
  143. package/dist/workflow-memory.d.ts +22 -0
  144. package/dist/workflow-memory.js +49 -0
  145. package/dist/x402-resource-service.d.ts +34 -0
  146. package/dist/x402-resource-service.js +154 -0
  147. package/dist/x402.d.ts +46 -0
  148. package/dist/x402.js +136 -0
  149. package/package.json +60 -3
@@ -0,0 +1,212 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
5
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
6
+ import { verifySignedCapability } from "./capability.js";
7
+ import { WalrusDealBlobs, wireDecrypt, wireEncrypt, wireHash, wireJsonHash, wireKey } from "./deal-wire.js";
8
+ import { fail } from "./errors.js";
9
+ import { LiveRuntime } from "./live-runtime.js";
10
+ import { executeMandated } from "./mandate.js";
11
+ import { canonicalJson, settlementMandateHash, validateSettlementPlan } from "./settlement.js";
12
+ import { validateDirectoryOrigin } from "./seller-directory.js";
13
+ const halves = (hash) => [BigInt(`0x${hash.slice(0, 32)}`).toString(), BigInt(`0x${hash.slice(32)}`).toString()];
14
+ const scalar = () => BigInt(`0x${randomBytes(16).toString("hex")}`).toString();
15
+ const offerSignatureBytes = (sessionId, capabilityId, buyerPublicKey, ciphertext) => Buffer.from(`${sessionId}:${capabilityId}:${buyerPublicKey}:${wireHash(Buffer.from(ciphertext, "base64"))}`);
16
+ /** Built-in live buyer adapter. The owner supplies only seller catalog origins;
17
+ * signing, payment, Walrus and memory stay in the buyer role home. */
18
+ export class LiveDealPorts {
19
+ app;
20
+ mandate;
21
+ http;
22
+ #blobs;
23
+ #origins;
24
+ #directory;
25
+ constructor(app, mandate, http = fetch) {
26
+ this.app = app;
27
+ this.mandate = mandate;
28
+ this.http = http;
29
+ if (app.profile.role !== "buyer" || app.profile.mock)
30
+ fail("DEAL_LIVE_REQUIRED", "built-in deal adapter requires a live buyer role");
31
+ const path = join(app.home, "deal-sellers.json");
32
+ let source;
33
+ try {
34
+ if ((statSync(path).mode & 0o077) !== 0)
35
+ fail("DEAL_SELLERS_PERMISSIONS", "seller catalog configuration must be owner-only");
36
+ source = JSON.parse(readFileSync(path, "utf8"));
37
+ }
38
+ catch (error) {
39
+ return fail("DEAL_SELLERS_NOT_CONFIGURED", "buyer requires owner-only deal-sellers.json containing seller origins", error);
40
+ }
41
+ const config = Array.isArray(source) ? { origins: source } : source;
42
+ if (!config || !Array.isArray(config.origins) || config.origins.length > 32 || !config.origins.length && !config.directoryUrl)
43
+ return fail("DEAL_SELLERS_INVALID", "seller configuration requires a directory or 1..32 origins");
44
+ this.#origins = config.origins.map((value) => {
45
+ const url = new URL(String(value));
46
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(url.hostname)) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
47
+ fail("DEAL_SELLERS_INVALID", "seller origin must be HTTPS or loopback HTTP without path or credentials");
48
+ return url;
49
+ });
50
+ if (config.directoryUrl) {
51
+ const url = new URL(config.directoryUrl);
52
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(url.hostname)) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
53
+ fail("DEAL_SELLERS_INVALID", "directory URL must be an HTTPS or loopback origin");
54
+ this.#directory = url;
55
+ }
56
+ this.#blobs = new WalrusDealBlobs(app, http);
57
+ }
58
+ readBlob(blobId) { return this.#blobs.read(blobId); }
59
+ signer() {
60
+ const encoded = this.app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "buyer signer is missing");
61
+ const decoded = decodeSuiPrivateKey(encoded);
62
+ if (decoded.scheme !== "ED25519")
63
+ fail("SIGNER_UNSUPPORTED", "buyer requires an ED25519 signer");
64
+ return Ed25519Keypair.fromSecretKey(decoded.secretKey);
65
+ }
66
+ async request(origin, path, body) {
67
+ const response = await this.http(new URL(path, origin), { ...(body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}), redirect: "error", signal: AbortSignal.timeout(90_000) });
68
+ let value;
69
+ try {
70
+ value = await response.json();
71
+ }
72
+ catch (error) {
73
+ return fail("DEAL_SELLER_RESPONSE_INVALID", "seller response is not JSON", error);
74
+ }
75
+ if (!response.ok || !value || typeof value !== "object" || Array.isArray(value))
76
+ fail("DEAL_SELLER_RESPONSE_INVALID", `seller returned HTTP ${response.status}`);
77
+ return value;
78
+ }
79
+ async catalog() {
80
+ const found = [];
81
+ const sources = this.#origins.map((origin) => ({ origin, path: "/v2/catalog" }));
82
+ if (this.#directory)
83
+ sources.push({ origin: this.#directory, path: "/v2/directory/catalog" });
84
+ for (const { origin, path } of sources) {
85
+ let offset = 0;
86
+ do {
87
+ const catalog = await this.request(origin.toString(), path === "/v2/directory/catalog" ? `${path}?offset=${offset}` : path);
88
+ if (!Array.isArray(catalog.capabilities) || catalog.capabilities.length > 100)
89
+ fail("SELLER_CATALOG_INVALID", "seller catalog is invalid");
90
+ for (const item of catalog.capabilities) {
91
+ const registration = item;
92
+ await verifySignedCapability(registration);
93
+ if (path === "/v2/catalog" && new URL(registration.manifest.endpoint).origin !== origin.origin)
94
+ fail("SELLER_CATALOG_INVALID", "signed capability points outside catalog seller origin");
95
+ if (!this.mandate.allowedCounterparties.includes(registration.manifest.sellerId) || this.mandate.allowedCapabilityIds && !this.mandate.allowedCapabilityIds.includes(registration.manifest.capabilityId) || this.mandate.allowedCapabilityClasses && !this.mandate.allowedCapabilityClasses.includes(registration.manifest.capability))
96
+ continue;
97
+ if (path === "/v2/directory/catalog") {
98
+ const endpoint = await validateDirectoryOrigin(registration.manifest.endpoint);
99
+ const served = await this.request(endpoint.origin, "/v2/catalog");
100
+ if (!Array.isArray(served.capabilities) || !served.capabilities.some((item) => canonicalJson(item) === canonicalJson(registration)))
101
+ fail("SELLER_CATALOG_INVALID", "directory listing is no longer served by the seller");
102
+ }
103
+ const readback = await this.readBlob(registration.manifestBlobId);
104
+ if (canonicalJson(JSON.parse(Buffer.from(readback).toString())) !== canonicalJson(registration.manifest))
105
+ fail("CAPABILITY_BLOB_MISMATCH", "Walrus capability blob differs from signed manifest");
106
+ found.push({ origin: new URL(registration.manifest.endpoint).origin, registration });
107
+ }
108
+ if (path !== "/v2/directory/catalog" || catalog.nextOffset === undefined)
109
+ break;
110
+ if (catalog.nextOffset !== offset + 100 || catalog.capabilities.length !== 100)
111
+ fail("SELLER_CATALOG_INVALID", "directory pagination is invalid");
112
+ offset = catalog.nextOffset;
113
+ if (offset > 100_000)
114
+ fail("SELLER_CATALOG_INVALID", "directory contains too many listings");
115
+ } while (true);
116
+ }
117
+ return found;
118
+ }
119
+ async discover(input) {
120
+ const candidates = (await this.catalog()).map(({ registration }) => registration).filter((registration) => (input.capabilityId && registration.manifest.capabilityId === input.capabilityId || input.capabilityClass && registration.manifest.capability === input.capabilityClass) && registration.manifest.asset === input.budget.asset);
121
+ if (!candidates.length)
122
+ fail("DEAL_CAPABILITY_NOT_FOUND", "configured sellers have no matching signed capability");
123
+ candidates.sort((a, b) => BigInt(a.manifest.priceBand.minimum) < BigInt(b.manifest.priceBand.minimum) ? -1 : BigInt(a.manifest.priceBand.minimum) > BigInt(b.manifest.priceBand.minimum) ? 1 : a.manifest.capabilityId.localeCompare(b.manifest.capabilityId));
124
+ return candidates[0];
125
+ }
126
+ async plan(input, registration, paymentOperationId) {
127
+ const existing = this.app.store.db.prepare("SELECT plan_json FROM settlement_plans WHERE operation_id=?").get(paymentOperationId);
128
+ if (existing) {
129
+ const saved = JSON.parse(existing.plan_json);
130
+ validateSettlementPlan(saved);
131
+ if (saved.mandateHash !== settlementMandateHash(this.mandate) || saved.manifestHash !== registration.manifest.manifestHash || saved.operationId !== paymentOperationId)
132
+ fail("DEAL_PLAN_MISMATCH", "persisted plan differs from the owner mandate or seller capability");
133
+ return saved;
134
+ }
135
+ const result = await executeMandated(this.app, this.mandate, "settlement.plan", { "operation-id": paymentOperationId, manifest: registration.manifest, "requested-amount": registration.manifest.priceBand.minimum, ...(input.privacyHint ? { "privacy-hint": input.privacyHint } : {}), ...(input.requestedRail ? { "requested-rail": input.requestedRail } : {}) });
136
+ return result;
137
+ }
138
+ async negotiate(input, registration, plan, agreementOperationId) {
139
+ const sessionId = agreementOperationId, capabilityId = registration.manifest.capabilityId, buyerPublicKey = this.app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "buyer transport key is missing");
140
+ const key = wireKey(this.app, registration.transportPublicKey, sessionId), taskHash = wireJsonHash(input.input), amount = registration.manifest.priceBand.minimum;
141
+ const transcript = wireJsonHash({ domain: "supercuts-deal-transcript-v1", sessionId, taskHash, planHash: plan.planHash, amount, rail: plan.rail });
142
+ const service = wireJsonHash({ domain: "supercuts-deal-service-v1", taskHash, planHash: plan.planHash, capabilityId, rail: plan.rail, buyerPublicKey });
143
+ const [transcriptRootLo, transcriptRootHi] = halves(transcript), [serviceDigestLo, serviceDigestHi] = halves(service);
144
+ const secretKey = `deal:wire:offer:${sessionId}`;
145
+ let ciphertext = this.app.store.getSecret(secretKey)?.toString();
146
+ if (!ciphertext) {
147
+ const runtime = await LiveRuntime.create(this.app.profile, this.app.store);
148
+ const buyer = runtime.engine.identity(this.app.store.getSecret("identity:babyjub-seed") ?? fail("IDENTITY_MISSING", "buyer settlement identity is missing"));
149
+ const draft = { sessionId, dealOperationId: input.operationId, capabilityId, manifestHash: registration.manifest.manifestHash, counterparty: registration.manifest.sellerId, paymentAmount: amount, rail: plan.rail, taskHash, planHash: plan.planHash, dealId: scalar(), revision: "1", transcriptRootLo, transcriptRootHi, serviceDigestLo, serviceDigestHi, settlementNonceLo: scalar(), settlementNonceHi: scalar(), blinding: scalar(), expiry: String(Math.min(plan.expiresAt, Date.now() + 30 * 60_000)), buyerPublicKey: buyer.publicKey.map(String) };
150
+ ciphertext = wireEncrypt(key, sessionId, "offer", { input: input.input, planHash: plan.planHash, paymentOperationId: plan.operationId, amount, maximum: plan.maximumAmount, rail: plan.rail, draft });
151
+ this.app.store.putSecretOnce(secretKey, Buffer.from(ciphertext));
152
+ }
153
+ const signer = this.signer(), signature = (await signer.signPersonalMessage(offerSignatureBytes(sessionId, capabilityId, buyerPublicKey, ciphertext))).signature;
154
+ const origin = new URL(registration.manifest.endpoint).origin;
155
+ const reply = await this.request(origin, "/v2/prepare", { sessionId, capabilityId, dealOperationId: input.operationId, buyerAddress: signer.toSuiAddress(), buyerPublicKey, buyerSignature: signature, ciphertext });
156
+ if (reply.sessionId !== sessionId || reply.capabilityId !== capabilityId || typeof reply.ciphertext !== "string")
157
+ return fail("DEAL_SELLER_RESPONSE_INVALID", "seller changed the prepared deal identity");
158
+ const offerBlob = reply.offerBlob, replyBlob = reply.replyBlob, agreementBlob = reply.agreementBlob;
159
+ if (wireHash(await this.readBlob(offerBlob.blobId)) !== offerBlob.ciphertextHash || offerBlob.ciphertextHash !== wireHash(Buffer.from(ciphertext, "base64")) || wireHash(await this.readBlob(replyBlob.blobId)) !== replyBlob.ciphertextHash || replyBlob.ciphertextHash !== wireHash(Buffer.from(reply.ciphertext, "base64")) || agreementBlob.blobId !== replyBlob.blobId || agreementBlob.ciphertextHash !== replyBlob.ciphertextHash)
160
+ fail("WALRUS_BLOB_MISMATCH", "seller transcript differs from encrypted Walrus evidence");
161
+ const decoded = wireDecrypt(key, sessionId, "reply", reply.ciphertext);
162
+ if (plan.rail === "x402" && decoded.resourceUrl !== `${registration.manifest.endpoint.replace(/\/$/, "")}/${encodeURIComponent(sessionId)}` || plan.rail === "shielded" && decoded.resourceUrl)
163
+ fail("DEAL_RESOURCE_MISMATCH", "seller returned a resource outside the signed capability endpoint");
164
+ const rawAgreement = Buffer.from(decoded.agreement, "base64url").toString(), agreement = JSON.parse(rawAgreement);
165
+ if (wireHash(rawAgreement) !== decoded.agreementHash || agreement.sessionId !== sessionId || agreement.dealOperationId !== input.operationId || agreement.capabilityId !== capabilityId || agreement.manifestHash !== registration.manifest.manifestHash || agreement.counterparty !== registration.manifest.sellerId || agreement.paymentAmount !== amount || agreement.rail !== plan.rail || agreement.taskHash !== taskHash || agreement.planHash !== plan.planHash || agreement.transcriptRootLo !== transcriptRootLo || agreement.transcriptRootHi !== transcriptRootHi || agreement.serviceDigestLo !== serviceDigestLo || agreement.serviceDigestHi !== serviceDigestHi)
166
+ fail("AGREEMENT_BINDING_MISMATCH", "seller signed different deal terms");
167
+ const accepted = await executeMandated(this.app, this.mandate, "offer.accept", { "operation-id": `accept-${sessionId}`, session: sessionId, agreement: decoded.agreement, ...(plan.rail === "shielded" ? { "plan-hash": plan.planHash } : {}) });
168
+ if (accepted.dealCommitment !== decoded.dealCommitment)
169
+ fail("AGREEMENT_BINDING_MISMATCH", "seller commitment differs from accepted agreement");
170
+ return { sessionId, agreementHash: decoded.agreementHash, agreedAmount: amount, sellerOrigin: origin, sellerTransportPublicKey: registration.transportPublicKey, ...(decoded.resourceUrl ? { resourceUrl: decoded.resourceUrl } : {}), transcriptBlobs: [offerBlob, replyBlob], agreementBlobId: agreementBlob.blobId, agreementCiphertextHash: agreementBlob.ciphertextHash };
171
+ }
172
+ async pay(input, plan, negotiation, paymentOperationId) {
173
+ const prepared = negotiation;
174
+ if (!prepared.sessionId || !prepared.agreementHash || prepared.agreedAmount !== plan.manifestMinimum)
175
+ fail("DEAL_AGREEMENT_INVALID", "saved negotiation is missing exact payment terms");
176
+ const result = plan.rail === "shielded" ? await executeMandated(this.app, this.mandate, "pay.shielded", { "operation-id": paymentOperationId, session: prepared.sessionId, "plan-hash": plan.planHash }) : await executeMandated(this.app, this.mandate, "pay.x402", { "operation-id": paymentOperationId, "resource-url": prepared.resourceUrl ?? fail("DEAL_RESOURCE_MISSING", "seller did not provide an x402 protected resource"), "plan-hash": plan.planHash });
177
+ if (result.finalized !== true || result.tatumVerified !== true || result.rail !== plan.rail || result.amount !== prepared.agreedAmount || result.counterparty !== plan.sellerId || result.asset !== plan.asset || typeof result.transactionDigest !== "string")
178
+ return fail("DEAL_PAYMENT_UNVERIFIED", "live payment differs from the accepted agreement and settlement plan");
179
+ if (plan.rail === "x402") {
180
+ const response = JSON.parse(Buffer.from(String(result.response ?? ""), "base64").toString());
181
+ if (response.status !== "paid" || response.requestId !== prepared.sessionId)
182
+ fail("DEAL_PAYMENT_UNVERIFIED", "seller-owned x402 resource returned a different receipt");
183
+ }
184
+ return { transactionDigest: result.transactionDigest, finalized: true, tatumVerified: true, ...(typeof result.proveMs === "number" ? { proveMs: result.proveMs } : {}) };
185
+ }
186
+ async deliver(input, plan, negotiation, payment, _deliveryOperationId) {
187
+ const prepared = negotiation, key = wireKey(this.app, prepared.sellerTransportPublicKey, prepared.sessionId);
188
+ const request = { dealId: input.operationId, capabilityId: plan.capabilityId, input: input.input, requestId: prepared.sessionId, paymentDigest: payment.transactionDigest, agreementHash: prepared.agreementHash, agreedAmount: prepared.agreedAmount, rail: plan.rail };
189
+ const ciphertext = wireEncrypt(key, prepared.sessionId, "fulfill", request), signer = this.signer();
190
+ const signature = (await signer.signPersonalMessage(Buffer.from(`${prepared.sessionId}:${wireHash(Buffer.from(ciphertext, "base64"))}`))).signature;
191
+ const reply = await this.request(prepared.sellerOrigin, "/v2/fulfill", { sessionId: prepared.sessionId, sellerId: plan.sellerId, ciphertext, buyerSignature: signature });
192
+ if (reply.sessionId !== prepared.sessionId || typeof reply.ciphertext !== "string")
193
+ return fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery response is invalid");
194
+ const receipt = wireDecrypt(key, prepared.sessionId, "receipt", reply.ciphertext);
195
+ if (receipt.dealId !== input.operationId || receipt.requestId !== prepared.sessionId)
196
+ fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery belongs to another deal");
197
+ const encrypted = await this.readBlob(receipt.blobId);
198
+ if (wireHash(encrypted) !== receipt.ciphertextHash)
199
+ fail("WALRUS_BLOB_MISMATCH", "seller delivery ciphertext differs from Walrus");
200
+ const artifact = wireDecrypt(key, prepared.sessionId, "delivery", Buffer.from(encrypted).toString("base64"));
201
+ if (wireHash(Buffer.from(artifact.artifactBase64, "base64")) !== receipt.artifactSha256 || artifact.contentType !== receipt.contentType || canonicalJson(artifact.metadata) !== canonicalJson(receipt.metadata))
202
+ fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery bytes or metadata differ from the receipt");
203
+ return { blobId: receipt.blobId, ciphertextHash: receipt.ciphertextHash, artifactSha256: receipt.artifactSha256, contentType: receipt.contentType, metadata: receipt.metadata, artifactBase64: artifact.artifactBase64 };
204
+ }
205
+ async rememberOutcome(input, plan, payment, artifact, _outcomeOperationId) {
206
+ return this.app.recordVerifiedDealOutcome(input.operationId, plan.planHash, payment.transactionDigest, artifact.artifactSha256);
207
+ }
208
+ async inventory(blobId) {
209
+ const result = (await this.app.execute("memory.inventory", { "blob-id": blobId })).data;
210
+ return result.present === true;
211
+ }
212
+ }
@@ -0,0 +1,28 @@
1
+ import { SuiGrpcClient } from "@mysten/sui/grpc";
2
+ import { ProtocolEngine } from "./protocol-engine.js";
3
+ import { RoleStore } from "./store.js";
4
+ export interface LiveIndexState {
5
+ leaves: bigint[];
6
+ root: bigint;
7
+ nextLeaf: bigint;
8
+ lastCheckpoint: bigint;
9
+ transactions: string[];
10
+ activity?: {
11
+ deposits: number;
12
+ exits: number;
13
+ distinctDepositingAddresses: number;
14
+ reducedPrivacy: boolean;
15
+ };
16
+ }
17
+ export declare class LivePoolIndex {
18
+ private readonly store;
19
+ private readonly client;
20
+ private readonly engine;
21
+ private readonly packageId;
22
+ private readonly poolId;
23
+ private readonly deploymentCheckpoint;
24
+ private readonly moduleName;
25
+ constructor(store: RoleStore, client: SuiGrpcClient, engine: ProtocolEngine, packageId: string, poolId: string, deploymentCheckpoint: bigint, moduleName?: "pool" | "pool_v2");
26
+ cached(): LiveIndexState | undefined;
27
+ sync(): Promise<LiveIndexState>;
28
+ }
@@ -0,0 +1,149 @@
1
+ import { fail } from "./errors.js";
2
+ const hexVector = (value, name) => {
3
+ if (typeof value !== "string")
4
+ return fail("CHAIN_EVENT_INVALID", `${name} is missing from a pool event`);
5
+ const bytes = /^(?:0x)?[0-9a-f]{64}$/i.test(value) ? Buffer.from(value.replace(/^0x/, ""), "hex") : Buffer.from(value, "base64");
6
+ if (bytes.length !== 32)
7
+ return fail("CHAIN_EVENT_INVALID", `${name} is not 32 bytes`);
8
+ return bytes.toString("hex");
9
+ };
10
+ const integer = (value, name) => { if (typeof value !== "string" && typeof value !== "number")
11
+ return fail("CHAIN_EVENT_INVALID", `${name} is missing`); try {
12
+ return BigInt(value);
13
+ }
14
+ catch (error) {
15
+ return fail("CHAIN_EVENT_INVALID", `${name} is invalid`, error);
16
+ } };
17
+ const transaction = (result) => {
18
+ const value = result;
19
+ return value.Transaction ?? value.FailedTransaction ?? fail("CHAIN_TRANSACTION_INVALID", "transaction response is empty");
20
+ };
21
+ export class LivePoolIndex {
22
+ store;
23
+ client;
24
+ engine;
25
+ packageId;
26
+ poolId;
27
+ deploymentCheckpoint;
28
+ moduleName;
29
+ constructor(store, client, engine, packageId, poolId, deploymentCheckpoint, moduleName = "pool") {
30
+ this.store = store;
31
+ this.client = client;
32
+ this.engine = engine;
33
+ this.packageId = packageId;
34
+ this.poolId = poolId;
35
+ this.deploymentCheckpoint = deploymentCheckpoint;
36
+ this.moduleName = moduleName;
37
+ store.db.exec(`
38
+ CREATE TABLE IF NOT EXISTS live_pool_leaves (leaf_index TEXT PRIMARY KEY, commitment TEXT NOT NULL, transaction_digest TEXT NOT NULL);
39
+ CREATE TABLE IF NOT EXISTS live_pool_sync (singleton INTEGER PRIMARY KEY CHECK(singleton=1), root TEXT NOT NULL, next_leaf TEXT NOT NULL, checkpoint TEXT NOT NULL, synced_at INTEGER NOT NULL);
40
+ `);
41
+ }
42
+ cached() {
43
+ const sync = this.store.db.prepare("SELECT root,next_leaf,checkpoint FROM live_pool_sync WHERE singleton=1").get();
44
+ if (!sync)
45
+ return undefined;
46
+ const rows = this.store.db.prepare("SELECT leaf_index,commitment,transaction_digest FROM live_pool_leaves ORDER BY CAST(leaf_index AS INTEGER)").all();
47
+ const leaves = [];
48
+ for (const row of rows) {
49
+ if (BigInt(row.leaf_index) !== BigInt(leaves.length))
50
+ fail("POOL_CACHE_INVALID", "cached leaves are not contiguous");
51
+ leaves.push(BigInt(`0x${row.commitment}`));
52
+ }
53
+ return { leaves, root: BigInt(`0x${sync.root}`), nextLeaf: BigInt(sync.next_leaf), lastCheckpoint: BigInt(sync.checkpoint), transactions: rows.map((row) => row.transaction_digest) };
54
+ }
55
+ async sync() {
56
+ const leaves = [], transactions = [], leafTransactions = [];
57
+ const depositingAddresses = new Set(), seenTransactions = new Set();
58
+ let deposits = 0, exits = 0;
59
+ let cursor = null, checkpoint = this.deploymentCheckpoint, expectedRoot = BigInt(`0x${this.engine.deployment.initialRoot.replace(/^0x/, "")}`), guard = 0;
60
+ do {
61
+ if (++guard > 100_000)
62
+ fail("CHAIN_PAGINATION_INVALID", "pool history pagination did not terminate");
63
+ const page = await this.client.listTransactions({ filter: { function: `${this.packageId}::${this.moduleName}` }, limit: 50, after: cursor, order: "ascending", include: { effects: true, events: true, transaction: true } });
64
+ for (const result of page.transactions) {
65
+ const tx = transaction(result);
66
+ if (!tx.checkpoint || BigInt(tx.checkpoint) < this.deploymentCheckpoint || !tx.status.success)
67
+ continue;
68
+ if (!tx.effects?.changedObjects.some((object) => object.objectId.toLowerCase() === this.poolId.toLowerCase()))
69
+ continue;
70
+ if (!tx.digest || seenTransactions.has(tx.digest) || BigInt(tx.checkpoint) < checkpoint)
71
+ fail("POOL_HISTORY_INVALID", "pool transaction history contains a duplicate or is out of checkpoint order");
72
+ seenTransactions.add(tx.digest);
73
+ let touched = false;
74
+ for (const event of tx.events ?? []) {
75
+ if (event.packageId.toLowerCase() !== this.packageId.toLowerCase() || event.module !== this.moduleName || !event.json)
76
+ continue;
77
+ const value = event.json, name = event.eventType.split("::").pop();
78
+ if (name === "DepositEvent") {
79
+ const oldRoot = BigInt(`0x${hexVector(value.old_root, "old_root")}`), leafIndex = integer(value.leaf_index, "leaf_index");
80
+ if (oldRoot !== expectedRoot || leafIndex !== BigInt(leaves.length))
81
+ fail("POOL_TRANSITION_INVALID", "deposit event does not continue the pinned pool history");
82
+ deposits++;
83
+ if (tx.transaction?.sender)
84
+ depositingAddresses.add(tx.transaction.sender.toLowerCase());
85
+ leaves.push(BigInt(`0x${hexVector(value.note_commitment, "note_commitment")}`));
86
+ leafTransactions.push(tx.digest);
87
+ expectedRoot = BigInt(`0x${hexVector(value.new_root, "new_root")}`);
88
+ touched = true;
89
+ }
90
+ else if (name === "TransferEvent") {
91
+ const oldRoot = BigInt(`0x${hexVector(value.old_root, "old_root")}`), first = integer(value.first_leaf_index, "first_leaf_index");
92
+ if (oldRoot !== expectedRoot || first !== BigInt(leaves.length))
93
+ fail("POOL_TRANSITION_INVALID", "transfer event does not continue the pinned pool history");
94
+ leaves.push(BigInt(`0x${hexVector(value.payment_commitment, "payment_commitment")}`), BigInt(`0x${hexVector(value.change_commitment, "change_commitment")}`));
95
+ leafTransactions.push(tx.digest, tx.digest);
96
+ expectedRoot = BigInt(`0x${hexVector(value.new_root, "new_root")}`);
97
+ touched = true;
98
+ }
99
+ else if (name === "WithdrawEvent" && this.moduleName === "pool_v2") {
100
+ exits++;
101
+ const oldRoot = BigInt(`0x${hexVector(value.old_root, "old_root")}`), leafIndex = integer(value.leaf_index, "leaf_index");
102
+ if (oldRoot !== expectedRoot || leafIndex !== BigInt(leaves.length))
103
+ fail("POOL_TRANSITION_INVALID", "v2 withdrawal event does not continue the pinned pool history");
104
+ leaves.push(BigInt(`0x${hexVector(value.change_commitment, "change_commitment")}`));
105
+ leafTransactions.push(tx.digest);
106
+ expectedRoot = BigInt(`0x${hexVector(value.new_root, "new_root")}`);
107
+ touched = true;
108
+ }
109
+ else if (name === "WithdrawEvent") {
110
+ exits++;
111
+ touched = true;
112
+ }
113
+ }
114
+ if (touched) {
115
+ checkpoint = BigInt(tx.checkpoint);
116
+ transactions.push(tx.digest);
117
+ }
118
+ }
119
+ cursor = page.hasNextPage ? page.endCursor : null;
120
+ if (page.hasNextPage && !cursor)
121
+ fail("CHAIN_PAGINATION_INVALID", "pool history has another page without a cursor");
122
+ } while (cursor);
123
+ const computed = this.engine.tree(leaves).root;
124
+ if (computed !== expectedRoot)
125
+ fail("POOL_STATE_MISMATCH", "event leaves do not reproduce the event root");
126
+ const object = (await this.client.getObject({ objectId: this.poolId, include: { json: true } })).object;
127
+ const state = object.json;
128
+ if (!state || typeof state !== "object")
129
+ fail("POOL_STATE_MISMATCH", "pool object has no JSON state");
130
+ const poolState = state;
131
+ const chainRoot = BigInt(`0x${hexVector(poolState.root, "pool.root")}`), chainIndex = integer(poolState.next_leaf, "pool.next_leaf");
132
+ if (chainRoot !== computed || chainIndex !== BigInt(leaves.length))
133
+ fail("POOL_STATE_MISMATCH", "event-derived tree differs from the live pool object");
134
+ this.store.db.exec("BEGIN IMMEDIATE");
135
+ try {
136
+ this.store.db.exec("DELETE FROM live_pool_leaves");
137
+ const insert = this.store.db.prepare("INSERT INTO live_pool_leaves(leaf_index,commitment,transaction_digest) VALUES(?,?,?)");
138
+ leaves.forEach((leaf, index) => insert.run(String(index), leaf.toString(16).padStart(64, "0"), leafTransactions[index] ?? "deployment"));
139
+ this.store.db.prepare("INSERT INTO live_pool_sync(singleton,root,next_leaf,checkpoint,synced_at) VALUES(1,?,?,?,?) ON CONFLICT(singleton) DO UPDATE SET root=excluded.root,next_leaf=excluded.next_leaf,checkpoint=excluded.checkpoint,synced_at=excluded.synced_at").run(computed.toString(16).padStart(64, "0"), String(leaves.length), String(checkpoint), Date.now());
140
+ this.store.db.exec("COMMIT");
141
+ }
142
+ catch (error) {
143
+ this.store.db.exec("ROLLBACK");
144
+ throw error;
145
+ }
146
+ return { leaves, root: computed, nextLeaf: BigInt(leaves.length), lastCheckpoint: checkpoint, transactions,
147
+ activity: { deposits, exits, distinctDepositingAddresses: depositingAddresses.size, reducedPrivacy: depositingAddresses.size < 5 } };
148
+ }
149
+ }
@@ -0,0 +1,82 @@
1
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
2
+ import { LivePoolIndex } from "./live-index.js";
3
+ import { ProtocolEngine } from "./protocol-engine.js";
4
+ import { Groth16Prover, type ProverConfig } from "./prover.js";
5
+ import { type QueuedExit } from "./v2-exits.js";
6
+ import { TatumLedger } from "./tatum.js";
7
+ import type { RoleStore } from "./store.js";
8
+ export interface LiveRuntimeProfile {
9
+ role: "buyer" | "seller" | "auditor";
10
+ packageId: string;
11
+ poolId: string;
12
+ coinType: string;
13
+ verifierVersion: number;
14
+ live: {
15
+ coinTypeHash: string;
16
+ deploymentDomain: string;
17
+ initialRoot: string;
18
+ deploymentCheckpoint: string;
19
+ writeGrpcEndpoint: string;
20
+ tatumGrpcEndpoint: string;
21
+ artifactRoot: string;
22
+ snarkjsPath: string;
23
+ converterPath: string;
24
+ rapidsnarkPath?: string;
25
+ artifacts: ProverConfig["expected"];
26
+ walrusPublisherUrl?: string;
27
+ walrusAggregatorUrl?: string;
28
+ };
29
+ policy: {
30
+ maxAmount: string;
31
+ allowedAssets: string[];
32
+ allowedCounterparties: string[];
33
+ requiredPrivacy: "any" | "shielded";
34
+ expiresAt: number;
35
+ };
36
+ }
37
+ export declare class LiveRuntime {
38
+ readonly profile: LiveRuntimeProfile;
39
+ readonly store: RoleStore;
40
+ readonly tatum: TatumLedger;
41
+ readonly signer: Ed25519Keypair;
42
+ readonly client: import("@mysten/sui/grpc").SuiGrpcClient;
43
+ readonly engine: ProtocolEngine;
44
+ readonly index: LivePoolIndex;
45
+ readonly prover: Groth16Prover;
46
+ private constructor();
47
+ static create(profile: LiveRuntimeProfile, store: RoleStore): Promise<LiveRuntime>;
48
+ doctor(): Promise<Record<string, unknown>>;
49
+ balance(): Promise<Record<string, unknown>>;
50
+ private signed;
51
+ private operationContext;
52
+ private existingSigned;
53
+ private submit;
54
+ private confirmTatum;
55
+ private prepareOpening;
56
+ deposit(amount: bigint, operationId: string): Promise<Record<string, unknown>>;
57
+ recover(): Promise<Record<string, unknown>>;
58
+ withdraw(commitment: string, recipient: string, operationId: string): Promise<Record<string, unknown>>;
59
+ queueV2Exit(commitment: string, operationId: string, amount: bigint, exactExit: boolean, windowStart: number, windowEnd: number, ownerApproved: boolean): Promise<QueuedExit>;
60
+ refreshV2Exit(operationId: string): Promise<{
61
+ refreshed: boolean;
62
+ requestOperationId: string;
63
+ }>;
64
+ finalizeV2Exit(operationId: string, digest: string, relayerAddress: string): Promise<Record<string, unknown>>;
65
+ acceptAgreement(sessionId: string, encoded: string): Record<string, unknown>;
66
+ prepareSellerAgreement(sessionId: string, encoded: string): Record<string, unknown>;
67
+ settleShielded(sessionId: string, operationId: string, bind?: (actual: {
68
+ amount: string;
69
+ counterparty: string;
70
+ asset: string;
71
+ requestId: string;
72
+ session: string;
73
+ }) => void): Promise<Record<string, unknown>>;
74
+ payX402Resource(operationId: string, resourceUrl: string, bind?: (actual: {
75
+ amount: string;
76
+ counterparty: string;
77
+ asset: string;
78
+ endpoint: string;
79
+ requestId: string;
80
+ requirementsHash: string;
81
+ }) => void): Promise<Record<string, unknown>>;
82
+ }