@supercuts-walrus/backend 0.1.0 → 0.1.2

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 (43) hide show
  1. package/README.md +28 -5
  2. package/config/phase1.testnet.json +20 -0
  3. package/config/proving-manifest.json +11 -0
  4. package/dist/buyer-setup.js +6 -6
  5. package/dist/cli.js +9 -3
  6. package/dist/deal-adapter.js +0 -2
  7. package/dist/deal-engine.js +5 -5
  8. package/dist/deal.js +2 -2
  9. package/dist/demo.js +70 -8
  10. package/dist/funding.d.ts +38 -0
  11. package/dist/funding.js +78 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/live-deal-ports.js +8 -1
  15. package/dist/live-runtime.d.ts +4 -1
  16. package/dist/live-runtime.js +83 -11
  17. package/dist/live-seller-transport.js +5 -1
  18. package/dist/mandate.js +10 -9
  19. package/dist/mcp-bootstrap.js +24 -2
  20. package/dist/mcp.js +39 -19
  21. package/dist/onboarding-doctor.js +21 -5
  22. package/dist/phase3.js +38 -6
  23. package/dist/remote-buyer.d.ts +18 -0
  24. package/dist/remote-buyer.js +59 -5
  25. package/dist/role-lock.d.ts +8 -0
  26. package/dist/role-lock.js +54 -0
  27. package/dist/seller-catalog-generate.js +4 -4
  28. package/dist/seller-directory-cli.js +2 -1
  29. package/dist/seller-live-cli.js +10 -1
  30. package/dist/seller-package.d.ts +9 -0
  31. package/dist/seller-package.js +50 -0
  32. package/dist/seller-provision.js +15 -1
  33. package/dist/seller-research.d.ts +50 -0
  34. package/dist/seller-research.js +210 -0
  35. package/dist/seller-scaffold.d.ts +1 -0
  36. package/dist/seller-scaffold.js +35 -0
  37. package/dist/seller-service.d.ts +1 -1
  38. package/dist/seller-service.js +14 -21
  39. package/dist/seller-setup.d.ts +3 -0
  40. package/dist/seller-setup.js +107 -0
  41. package/dist/x402-resource-service.d.ts +2 -0
  42. package/dist/x402-resource-service.js +36 -17
  43. package/package.json +2 -1
package/README.md CHANGED
@@ -77,7 +77,7 @@ bytes under that operation ID; retrying another payload with the same ID fails.
77
77
 
78
78
  ## Procurement demo
79
79
 
80
- For the Codex buyer with three remote Claude-backed sellers, Supabase-backed seller state, and the separate Render facilitator, follow [the remote demo guide](../docs/remote-codex-demo.md). The remote path is live-only and gives Codex a buyer role MCP server; the three seller panes use the safe `supercuts-seller-watch` event stream.
80
+ For the Codex buyer with three remote sellers, Claude-researched and owner-reviewed artifacts, Supabase-backed seller state, and the separate Render facilitator, follow [the remote demo guide](../docs/remote-codex-demo.md). The remote path is live-only and gives Codex a buyer role MCP server; the three seller panes use the safe `supercuts-seller-watch` event stream.
81
81
 
82
82
  `supercuts-demo` is the operator entry point:
83
83
 
@@ -116,9 +116,11 @@ For a Claude-agent recording, export `CLAUDE_API_KEY` or provide
116
116
  `live --claude-key-file <owner-only-file>`; a repository `.env` file is not
117
117
  loaded automatically. Validate the credential against Anthropic before starting
118
118
  a funded take.
119
- The live path calls Claude for bounded buyer and seller decisions and records
120
- only the model, action, and token counts. The backend still enforces capability,
121
- amount, rail, and agreement bindings. Validate that recording with
119
+ The recorded workflow can call Claude for bounded buyer decisions and records
120
+ only the model, action, and token counts. Hosted seller price and agreement
121
+ decisions are deterministic after backend validation; Claude web research runs
122
+ before provisioning so model availability cannot interrupt a paid request. The
123
+ backend still enforces capability, amount, rail, and agreement bindings. Validate that recording with
122
124
  `supercuts-demo check --require-claude`; an earlier funded run without these
123
125
  agent-decision events does not satisfy that gate.
124
126
 
@@ -185,7 +187,28 @@ signer, pool notes, Walrus and Walrus Memory configuration. A missing file
185
187
  returns `DEAL_SELLERS_NOT_CONFIGURED`. An owner-only `deal-ports.mjs` exporting
186
188
  `createDealPorts(app, mandate)` remains available as an advanced override.
187
189
 
188
- The seller service is `supercuts-seller-live <seller-home> <config.json>`.
190
+ Create a seller home with `supercuts onboard seller --home <seller-home>` or
191
+ its noninteractive `--config <owner-only-setup.json>` form. It generates a
192
+ fresh seller signer by default, writes a seller MCP stanza, and explicitly
193
+ reports that the seller should not receive faucet funds. The seller service is
194
+ `supercuts-seller-live <seller-home> <config.json>`.
195
+ Use `supercuts seller scaffold` to generate the offer spec, replaceable handler,
196
+ future expiry, environment-based facilitator config, and exact next commands.
197
+ The npm tarball includes pinned metadata for x402-only sellers; shielded sellers
198
+ still install the proof artifacts required for recovery and exits.
199
+
200
+ Research sellers can create a source-backed draft before packaging:
201
+
202
+ ```sh
203
+ CLAUDE_API_KEY=... supercuts seller research --kind market-brief \
204
+ --topic "Current Sui ecosystem market structure and risks" \
205
+ --output <owner-only-draft.json>
206
+ ```
207
+
208
+ The command uses Anthropic web search, rejects citations absent from the search
209
+ evidence, and writes a mode-0600 draft marked `draft-needs-owner-review`.
210
+ Optional `--allowed-domains` constrains sources. It never publishes or changes
211
+ live seller state; an owner must review and approve the immutable bytes first.
189
212
  Register each capability first with
190
213
  `supercuts-seller-register <seller-home> <spec.json> <registration.json>`.
191
214
  The spec contains `capabilityId`, `capability`, `description`, `inputSchema`,
@@ -0,0 +1,20 @@
1
+ {
2
+ "network": "testnet",
3
+ "sender": "0x66fdfe84ff521627c93910c93ec0bc26934ee1fef3a1a0a5ce14b7dc4567ec00",
4
+ "coinType": "0x2::sui::SUI",
5
+ "coinTypeHash": "be4507e04f555448ca05c0c9f8345c5ea9f3e40e8616a20f5d5c18b40ef399eb",
6
+ "packageId": "0x32d104c1ab422d497df4ae3a6861258694949c86428f886ca338e07bd055cf76",
7
+ "poolId": "0x8fd22140c906495bb5334a497dcdd9112479c638e21c5df4b4cfb66f633a106b",
8
+ "poolCreateTransaction": "BnzMqokgQVWwSkRYCCrMTyWwHN6HVdDKWwVDJXTZqC6b",
9
+ "deploymentCheckpoint": "401762744",
10
+ "deploymentDomain": "3082f331660eb38075e250a810a701bc4b0de2ca329eda70b2d76e3795517305",
11
+ "initialRoot": "2134e76ac5d21aab186c2be1dd8f84ee880a1e46eaf712f9d371b6df22191f3e",
12
+ "verifierVersion": 2,
13
+ "testOnly": true,
14
+ "artifactManifest": "config/proving-manifest.json",
15
+ "artifacts": {
16
+ "deposit": { "r1csSha256": "f36a1a18bf9d243e089e6ab14436e5a921d2b839d81bea95db319a4cdb80a197", "zkeySha256": "d398776a54628c390e8bf822007e47ed8fac00e92e8ea95bb17d20db517998dc", "verifyingKeySha256": "838404ff0ae3a9fc82da0856b68a11428cf3ffef335b4c6539fa1860d8e1ba26" },
17
+ "transfer": { "r1csSha256": "055ea2ec8f689c48e59f89fe06db6fb980dc575982214bfe60f675e8f08485c3", "zkeySha256": "6e7bcac98b8d2a78fd1811e2caadd83d0d6a301951f8cf27ac55a67006b001c2", "verifyingKeySha256": "8a6896f3f330769aa69e0dc1251a19fb69c4d6264b5729d0104df8d262e573ba" },
18
+ "withdraw": { "r1csSha256": "85cf1542ab2f2ece4bbd8757def48d526ba0951a832a2e0ebbf97ace2c611308", "zkeySha256": "fd303dd0dd53b9b752dac0de546dabdd03198a7a4defde5e8d6018bccd419c56", "verifyingKeySha256": "445c1982bea5e465fb0f5665023bb279637d820a9425f16a4c2f2ee7460e7e4a" }
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "supercuts-pool-v2",
3
+ "testOnly": true,
4
+ "warning": "Per-circuit contributions are locally generated and unaudited; never use these keys with production assets.",
5
+ "powersOfTauSha256": "3f428d1a407e4704ef906960e000b03089e5e6ec29bf65b07bb5e3de005f4700",
6
+ "circuits": {
7
+ "deposit": { "r1csSha256": "f36a1a18bf9d243e089e6ab14436e5a921d2b839d81bea95db319a4cdb80a197", "zkeySha256": "d398776a54628c390e8bf822007e47ed8fac00e92e8ea95bb17d20db517998dc", "verificationKeySha256": "838404ff0ae3a9fc82da0856b68a11428cf3ffef335b4c6539fa1860d8e1ba26", "proofSha256": "9b7835852cd9ced7c02828e5c563191ac597f7175b04fa9bb01c66cc93a93e58", "publicInputsSha256": "23569e5994ddd08b555dcbcf12c735ec303e000f6e7e90c9b63667dec60b6aeb" },
8
+ "transfer": { "r1csSha256": "055ea2ec8f689c48e59f89fe06db6fb980dc575982214bfe60f675e8f08485c3", "zkeySha256": "6e7bcac98b8d2a78fd1811e2caadd83d0d6a301951f8cf27ac55a67006b001c2", "verificationKeySha256": "8a6896f3f330769aa69e0dc1251a19fb69c4d6264b5729d0104df8d262e573ba", "proofSha256": "ff9a902e91e7d7b64e278cc7e17c22ad3f031f350aacb43f610771adafbf3f35", "publicInputsSha256": "903861e7ee9f792a17194f96649736d4916f45605918acac78e5d40a5b8f6170" },
9
+ "withdraw": { "r1csSha256": "85cf1542ab2f2ece4bbd8757def48d526ba0951a832a2e0ebbf97ace2c611308", "zkeySha256": "fd303dd0dd53b9b752dac0de546dabdd03198a7a4defde5e8d6018bccd419c56", "verificationKeySha256": "445c1982bea5e465fb0f5665023bb279637d820a9425f16a4c2f2ee7460e7e4a", "proofSha256": "4746ee54c04389929aad48c0c5a0cf18985b9dd6bcd7a3b83aa717d8847973d3", "publicInputsSha256": "d18d5852ac3c636c7b081bc52d367c896c95bd5a89feacc91014afefe3dcff54" }
10
+ }
11
+ }
@@ -48,10 +48,10 @@ function validateConfig(config) {
48
48
  fail("SETUP_INVALID", "configure a directoryUrl or sellerOrigins");
49
49
  if (config.directoryUrl)
50
50
  origins([config.directoryUrl]);
51
- for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator", "memoryConfigFile"])
51
+ for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator"])
52
52
  if (typeof config[key] !== "string" || !config[key])
53
53
  fail("SETUP_INVALID", `${key} is required`);
54
- for (const path of [config.tatumKeyFile, config.memoryConfigFile, ...(config.suiKeyFile ? [config.suiKeyFile] : [])])
54
+ for (const path of [config.tatumKeyFile, ...(config.memoryConfigFile ? [config.memoryConfigFile] : []), ...(config.suiKeyFile ? [config.suiKeyFile] : [])])
55
55
  ownerFile(resolve(path));
56
56
  const deployment = JSON.parse(readFileSync(resolve(config.deploymentConfig), "utf8"));
57
57
  if (deployment.network !== "testnet" || deployment.verifierVersion !== 2)
@@ -69,7 +69,7 @@ function writePolicy(home, config) {
69
69
  const base = { ...mandate };
70
70
  delete base.allowedCapabilityIds;
71
71
  delete base.allowedCapabilityClasses;
72
- const next = { ...base, version: "supercuts.mandate.v2", allowedCommands: [...new Set([...mandate.allowedCommands, "deal.execute", "deal.get", "deal.resume", "deal.evidence", "deal.delivery", "deal.catalog", "deal.policy", "settlement.plan", "memory.inventory"])], allowedCounterparties: config.allowedCounterparties ?? [], perDealCeiling: config.perDealCeiling, totalRunBudget: config.totalBudget, expiresAt: config.expiresAt, requiredPrivacy: "any", requireSettlementPlan: true, allowedRails: config.allowPublicX402 === true ? ["shielded", "x402"] : ["shielded"], allowPublicX402: config.allowPublicX402 === true, ...(config.allowedCapabilityIds ? { allowedCapabilityIds: config.allowedCapabilityIds } : {}), ...(config.allowedCapabilityClasses ? { allowedCapabilityClasses: config.allowedCapabilityClasses } : {}) };
72
+ const next = { ...base, version: "supercuts.mandate.v2", allowedCommands: [...new Set([...mandate.allowedCommands, "deal.execute", "deal.get", "deal.resume", "deal.evidence", "deal.delivery", "deal.catalog", "deal.policy", "settlement.plan", "memory.inventory", "remote.research"])], allowedCounterparties: config.allowedCounterparties ?? [], perDealCeiling: config.perDealCeiling, totalRunBudget: config.totalBudget, expiresAt: config.expiresAt, requiredPrivacy: "any", requireSettlementPlan: true, allowedRails: config.allowPublicX402 === true ? ["shielded", "x402"] : ["shielded"], allowPublicX402: config.allowPublicX402 === true, ...(config.allowedCapabilityIds ? { allowedCapabilityIds: config.allowedCapabilityIds } : {}), ...(config.allowedCapabilityClasses ? { allowedCapabilityClasses: config.allowedCapabilityClasses } : {}) };
73
73
  writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
74
74
  chmodSync(path, 0o600);
75
75
  writeFileSync(join(home, "deal-sellers.json"), `${JSON.stringify(config.directoryUrl ? { directoryUrl: origins([config.directoryUrl])[0], origins: config.sellerOrigins?.length ? origins(config.sellerOrigins) : [] } : origins(config.sellerOrigins ?? []), null, 2)}\n`, { mode: 0o600 });
@@ -94,7 +94,7 @@ export async function promptBuyerSetup(home) {
94
94
  deploymentConfig: await ask("Pinned Testnet deployment config path"), tatumKeyFile: await ask("Tatum key file path"),
95
95
  writeGrpc: await ask("Sui write gRPC URL"), tatumGrpc: await ask("Tatum read gRPC URL"),
96
96
  walrusPublisher: await ask("Walrus publisher URL"), walrusAggregator: await ask("Walrus aggregator URL"),
97
- memoryConfigFile: await ask("Owner-only Walrus Memory config path"),
97
+ memoryConfigFile: await ask("Owner-only Walrus Memory config path (blank to skip Memory)"),
98
98
  directoryUrl: await ask("Shared directory origin (blank for direct sellers)"),
99
99
  sellerOrigins: (await ask("Additional seller HTTPS origins, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
100
100
  allowedCounterparties: (await ask("Initially approved seller Sui addresses, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
@@ -118,7 +118,7 @@ function setupBuyerConfig(home, config) {
118
118
  if (!config.suiKeyFile)
119
119
  writeFileSync(keyPath, Ed25519Keypair.generate().getSecretKey(), { mode: 0o600, flag: "wx" });
120
120
  ownerFile(keyPath);
121
- initialize(home, { role: "buyer", "deployment-config": resolve(config.deploymentConfig), "sui-key-file": keyPath, "tatum-key-file": resolve(config.tatumKeyFile), "write-grpc": config.writeGrpc, "tatum-grpc": config.tatumGrpc, "walrus-publisher": config.walrusPublisher, "walrus-aggregator": config.walrusAggregator, "memory-config-file": resolve(config.memoryConfigFile), "allow-counterparty": (config.allowedCounterparties ?? []).join(","), "max-amount": config.perDealCeiling, ...(config.rapidsnark ? { rapidsnark: resolve(config.rapidsnark) } : {}) });
121
+ initialize(home, { role: "buyer", "deployment-config": resolve(config.deploymentConfig), "sui-key-file": keyPath, "tatum-key-file": resolve(config.tatumKeyFile), "write-grpc": config.writeGrpc, "tatum-grpc": config.tatumGrpc, "walrus-publisher": config.walrusPublisher, "walrus-aggregator": config.walrusAggregator, ...(config.memoryConfigFile ? { "memory-config-file": resolve(config.memoryConfigFile) } : {}), "allow-counterparty": (config.allowedCounterparties ?? []).join(","), "max-amount": config.perDealCeiling, ...(config.rapidsnark ? { rapidsnark: resolve(config.rapidsnark) } : {}) });
122
122
  writePolicy(home, config);
123
123
  const session = createBuyerSession(home);
124
124
  const decoded = decodeSuiPrivateKey(readFileSync(keyPath, "utf8").trim());
@@ -206,7 +206,7 @@ export async function approveBuyerSeller(home, capabilityId) {
206
206
  profile.policy.allowedCounterparties = updated.allowedCounterparties;
207
207
  writeFileSync(path, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
208
208
  writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
209
- return { capabilityId, sellerId: registration.manifest.sellerId, approved: true };
209
+ return { capabilityId, sellerId: registration.manifest.sellerId, approved: true, manifestHash: registration.manifest.manifestHash, description: registration.manifest.description, priceBand: registration.manifest.priceBand, asset: registration.manifest.asset, supportedRails: registration.manifest.supportedRails, expiresAt: registration.manifest.expiresAt };
210
210
  }
211
211
  export async function addBuyerPrivacyRule(home, capabilityId) {
212
212
  const app = new Phase3App(home);
package/dist/cli.js CHANGED
@@ -2,18 +2,24 @@
2
2
  import { Phase3App, errorOutput, homePath, initialize, parseArgs } from "./phase3.js";
3
3
  import { addBuyerPrivacyRule, approveBuyerSeller, createBuyerSession, promptBuyerSetup, renewBuyer, setupBuyer } from "./buyer-setup.js";
4
4
  import { onboardingDoctor } from "./onboarding-doctor.js";
5
+ import { executeFunding, prepareFunding } from "./funding.js";
6
+ import { packageHostedSeller } from "./seller-package.js";
7
+ import { promptSellerSetup, setupSeller } from "./seller-setup.js";
8
+ import { scaffoldSeller } from "./seller-scaffold.js";
9
+ import { generateSellerResearch } from "./seller-research.js";
5
10
  const words = process.argv.slice(2);
6
11
  let command = words.shift() ?? "";
7
- if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding"].includes(command))
12
+ if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding", "onboard", "funding", "seller"].includes(command))
8
13
  command += `.${words.shift() ?? ""}`;
9
14
  try {
10
15
  if (!command || command === "help" || command === "--help" || command === "setup.") {
11
- process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["setup buyer", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts setup buyer --home <private-directory> for guided live Testnet setup. Run supercuts init --role <buyer|seller|auditor> --mock --home <private-directory> for a local non-funded profile. Keep each role in a separate owner-only home.", live: "Supply the pinned Testnet deployment config, owner-only credential files, Sui and Tatum RPC endpoints, Walrus endpoints, local proving artifacts, and RapidSnark. Fund only the buyer address printed by supercuts-demo setup." })}\n`);
16
+ process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "onboard seller", "seller scaffold", "seller research", "seller package", "funding prepare", "funding execute", "setup buyer", "setup seller", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts onboard buyer or supercuts onboard seller with an isolated --home. Then use seller scaffold to generate a capability. Research sellers can use seller research to create a cited owner-review draft before packaging.", live: "Fund only the generated buyer address. A seller does not need faucet funds to publish or receive a capability payment." })}\n`);
12
17
  process.exit(0);
13
18
  }
14
19
  const args = parseArgs(words);
15
20
  const home = homePath(args);
16
- const result = command === "init" ? initialize(home, args) : command === "setup.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
21
+ const fundingInput = { publicPayment: String(args["public-payment"] ?? "0"), shieldedPayment: String(args["shielded-payment"] ?? "0"), ...(typeof args["gas-reserve"] === "string" ? { gasReserve: args["gas-reserve"] } : {}), ...(typeof args["change-reserve"] === "string" ? { changeReserve: args["change-reserve"] } : {}) };
22
+ const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.seller" || command === "onboard.seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupSeller(home, args.config) : await promptSellerSetup(home) } : command === "seller.scaffold" ? { schemaVersion: "supercuts.seller-scaffold.v1", ok: true, command, data: scaffoldSeller({ ...args, home }) } : command === "seller.research" ? { schemaVersion: "supercuts.seller-research.v1", ok: true, command, data: await generateSellerResearch({ kind: String(args.kind ?? ""), topic: String(args.topic ?? ""), output: String(args.output ?? ""), ...(typeof args["context-file"] === "string" ? { contextFile: args["context-file"] } : {}), ...(typeof args["allowed-domains"] === "string" ? { allowedDomains: args["allowed-domains"] } : {}), ...(typeof args.model === "string" ? { model: args.model } : {}) }) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
17
23
  return await app.execute(command, args);
18
24
  }
19
25
  finally {
@@ -16,8 +16,6 @@ export async function createConfiguredDealEngine(app) {
16
16
  fail("MANDATE_COMMAND_FORBIDDEN", "owner mandate does not allow composite deal execution");
17
17
  const path = join(app.home, "deal-ports.mjs");
18
18
  if (!existsSync(path)) {
19
- if (!app.store.getSecret("live:memory-config"))
20
- fail("LIVE_MEMORY_CONFIG_MISSING", "live composite deals require an owner-only Walrus Memory configuration");
21
19
  return new DealEngine(new DealJournal(app.store), new LiveDealPorts(app, mandate), mandate);
22
20
  }
23
21
  if ((statSync(path).mode & 0o077) !== 0)
@@ -179,10 +179,10 @@ export class DealEngine {
179
179
  if (existing?.blobId) {
180
180
  const indexed = await this.ports.inventory(existing.blobId);
181
181
  this.journal.recordOutcome(operationId, existing.payloadHash, existing.blobId, indexed);
182
- return indexed ? this.journal.advance(operationId, "outcome_pending", "complete") : this.journal.get(operationId);
182
+ return this.journal.advance(operationId, "outcome_pending", "complete");
183
183
  }
184
184
  if (existing)
185
- return view;
185
+ return this.journal.advance(operationId, "outcome_pending", "complete");
186
186
  const plan = this.journal.step(operationId, "planning") ?? fail("DEAL_PLAN_MISSING", "saved deal plan is missing");
187
187
  const payment = this.journal.step(operationId, "payment") ?? fail("DEAL_PAYMENT_UNVERIFIED", "saved deal payment is missing");
188
188
  const artifact = this.journal.step(operationId, "delivery") ?? fail("DEAL_DELIVERY_UNVERIFIED", "saved deal delivery is missing");
@@ -193,11 +193,11 @@ export class DealEngine {
193
193
  this.journal.recordTiming(operationId, "outcome_queue", Date.now() - started);
194
194
  if ("unresolved" in candidate) {
195
195
  this.journal.setOutcomeUnresolved(operationId);
196
- return this.journal.get(operationId);
196
+ return this.journal.advance(operationId, "outcome_pending", "complete");
197
197
  }
198
198
  if ("queued" in candidate) {
199
199
  this.journal.queueOutcome(operationId, candidate.payloadHash);
200
- return this.journal.get(operationId);
200
+ return this.journal.advance(operationId, "outcome_pending", "complete");
201
201
  }
202
202
  if (!candidate.blobId || !/^[0-9a-f]{64}$/.test(candidate.payloadHash))
203
203
  fail("DEAL_MEMORY_INVALID", "outcome memory is incomplete");
@@ -206,6 +206,6 @@ export class DealEngine {
206
206
  }
207
207
  const indexed = await this.ports.inventory(outcome.blobId);
208
208
  this.journal.recordOutcome(operationId, outcome.payloadHash, outcome.blobId, indexed);
209
- return indexed ? this.journal.advance(operationId, "outcome_pending", "complete") : this.journal.get(operationId);
209
+ return this.journal.advance(operationId, "outcome_pending", "complete");
210
210
  }
211
211
  }
package/dist/deal.js CHANGED
@@ -213,8 +213,8 @@ export class DealJournal {
213
213
  fail("DEAL_PAYMENT_UNVERIFIED", "confirmed payment evidence is required");
214
214
  if (next === "delivered" && !(this.store.db.prepare("SELECT 1 FROM deal_blobs WHERE operation_id=? AND kind='delivery' AND status='verified'").get(operationId)))
215
215
  fail("DEAL_DELIVERY_UNVERIFIED", "verified delivery evidence is required");
216
- if (next === "complete" && !(this.store.db.prepare("SELECT 1 FROM deal_memory_outbox WHERE operation_id=? AND event_kind='outcome' AND status='indexed'").get(operationId)))
217
- fail("DEAL_MEMORY_UNVERIFIED", "indexed outcome memory is required");
216
+ if (next === "complete" && (this.get(operationId).evidence.payment !== "finalized" || this.get(operationId).evidence.delivery !== "verified"))
217
+ fail("DEAL_DELIVERY_UNVERIFIED", "finalized payment and verified delivery are required before deal completion");
218
218
  this.store.db.prepare("UPDATE deal_operations SET state=?,rail=COALESCE(?,rail),plan_hash=COALESCE(?,plan_hash),updated_at=? WHERE operation_id=?")
219
219
  .run(next, result?.rail ?? null, result?.planHash ?? null, Date.now(), operationId);
220
220
  this.store.db.exec("COMMIT");
package/dist/demo.js CHANGED
@@ -23,6 +23,7 @@ const value = (name, fallback) => typeof args[name] === "string" ? args[name] :
23
23
  const stateDir = () => resolve(value("state-dir", ".supercuts-demo"));
24
24
  const homes = (root) => ({ buyer: join(root, "buyer"), publicSeller: join(root, "public-seller"), secondPublicSeller: join(root, "second-public-seller"), confidentialSeller: join(root, "confidential-seller"), auditor: join(root, "auditor") });
25
25
  const send = (data) => process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.demo.v1", ok: true, command, data })}\n`);
26
+ const help = () => ({ commands: ["setup", "doctor", "seed-policy", "reuse-memories", "live", "replay", "check"], setup: "Run supercuts-demo setup --state-dir <owner-only-directory> with the pinned live configuration. Fund only the buyer address in funding[0].address.", live: "The live demo uses Sui Testnet and Mainnet-backed Walrus services; mock receipts are never settlement evidence." });
26
27
  function keyAddress(path) { const decoded = decodeSuiPrivateKey(readFileSync(path, "utf8").trim()); if (decoded.scheme !== "ED25519")
27
28
  fail("SIGNER_UNSUPPORTED", "demo requires ED25519 Sui keys"); return Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress(); }
28
29
  function createKey(path) { if (!existsSync(path))
@@ -48,20 +49,51 @@ async function setup() {
48
49
  fail("DEMO_ROLE_PROFILE_MISMATCH", "demo setup needs fresh isolated live v2 role homes; choose a new --state-dir instead of reusing v1 or mock profiles");
49
50
  }
50
51
  const workflowPath = typeof args.workflow === "string" ? resolve(args.workflow) : !mock && typeof args["seller-url"] !== "string" ? resolve(value("workflow")) : undefined;
52
+ let activeCapabilities = [];
51
53
  if (workflowPath) {
52
- const workflow = JSON.parse(readFileSync(workflowPath, "utf8")), capabilities = workflow.capabilities ?? [];
53
- if (!capabilities.length)
54
+ const workflow = JSON.parse(readFileSync(workflowPath, "utf8")), configured = workflow.capabilities ?? [];
55
+ if (!configured.length)
54
56
  fail("DEMO_CAPABILITIES_REQUIRED", "live workflow must provide capability manifests for the buyer mandate");
55
- for (const manifest of capabilities)
57
+ for (const manifest of configured)
56
58
  validateCapabilityManifest(manifest);
59
+ let capabilities = configured;
60
+ if (!mock && typeof args["seller-url"] === "string") {
61
+ const response = await fetch(new URL("/v1/catalog", new URL(args["seller-url"])), { signal: AbortSignal.timeout(90_000) });
62
+ if (!response.ok)
63
+ fail("SELLER_SERVICE_UNAVAILABLE", `remote seller catalog returned ${response.status}`);
64
+ const served = await response.json();
65
+ const allowedIds = new Set(configured.map((item) => item.capabilityId)), entries = (served.capabilities ?? []).filter((item) => item.manifest && allowedIds.has(item.manifest.capabilityId));
66
+ if (!entries.length || entries.some((item) => !item.manifest || typeof item.blobId !== "string"))
67
+ fail("SELLER_CATALOG_INVALID", "remote seller catalog has no configured capabilities with Walrus identities");
68
+ const app = new Phase3App(roles.buyer);
69
+ try {
70
+ const discovered = (await app.execute("capability.discover", { "blob-ids": Buffer.from(JSON.stringify(entries.map((item) => item.blobId))).toString("base64url") })).data.capabilities;
71
+ const verified = new Map(discovered.map((item) => [`${item.capabilityId}:${item.blobId}`, item.manifestHash]));
72
+ if (entries.some((item) => verified.get(`${item.manifest.capabilityId}:${item.blobId}`) !== item.manifest.manifestHash))
73
+ fail("SELLER_CATALOG_INVALID", "remote seller catalog differs from its Walrus manifests");
74
+ }
75
+ finally {
76
+ app.close();
77
+ }
78
+ capabilities = entries.map((item) => item.manifest);
79
+ }
80
+ activeCapabilities = capabilities;
57
81
  const mandatePath = join(roles.buyer, "mandate.json"), mandate = JSON.parse(readFileSync(mandatePath, "utf8"));
58
82
  mandate.requireSettlementPlan = true;
59
83
  mandate.allowedCommands = [...new Set([...(Array.isArray(mandate.allowedCommands) ? mandate.allowedCommands : []), "settlement.plan"])];
84
+ mandate.allowedCounterparties = [...new Set(capabilities.map((item) => item.sellerId))];
60
85
  mandate.allowedCapabilityIds = [...new Set(capabilities.map((item) => item.capabilityId))];
61
86
  mandate.allowedCapabilityClasses = [...new Set(capabilities.map((item) => item.capability))];
62
87
  mandate.allowedRails = [...new Set(capabilities.flatMap((item) => item.supportedRails))];
63
88
  writeFileSync(mandatePath, `${JSON.stringify(mandate, null, 2)}\n`, { mode: 0o600 });
64
89
  chmodSync(mandatePath, 0o600);
90
+ const buyerProfilePath = join(roles.buyer, "profile.json"), buyerProfile = JSON.parse(readFileSync(buyerProfilePath, "utf8"));
91
+ if (!buyerProfile.policy)
92
+ fail("DEMO_ROLE_PROFILE_MISMATCH", "buyer profile has no payment policy");
93
+ const buyerPolicy = buyerProfile.policy;
94
+ buyerPolicy.allowedCounterparties = [...new Set(capabilities.map((item) => item.sellerId))];
95
+ writeFileSync(buyerProfilePath, `${JSON.stringify(buyerProfile, null, 2)}\n`, { mode: 0o600 });
96
+ chmodSync(buyerProfilePath, 0o600);
65
97
  }
66
98
  const mcpScript = join(dirname(fileURLToPath(import.meta.url)), "mcp.js");
67
99
  const mcp = { mcpServers: Object.fromEntries(Object.entries(roles).map(([name, home]) => [`supercuts-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`, { command: process.execPath, args: [mcpScript], env: { SUPERCUTS_HOME: home } }])) };
@@ -75,8 +107,10 @@ async function setup() {
75
107
  writeFileSync(join(root, "buyer-codex.toml"), codex, { mode: 0o600 });
76
108
  }
77
109
  const remote = typeof args["seller-url"] === "string";
78
- writeFileSync(join(root, "run.json"), `${JSON.stringify({ schema: "supercuts.demo.run.v1", createdAt: new Date().toISOString(), runId, ...(remote ? { eventPath } : {}), mock, homes: roles, addresses }, null, 2)}\n`, { mode: 0o600 });
79
- return { stateDir: root, mock, funding: mock ? [] : [{ address: addresses.buyer, needs: ["Testnet SUI gas", "configured x402 asset", "shielded deposit value"] }], sellerPayTo: addresses.publicSeller, sellerAddresses: { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller }, files: { mcp: join(root, "mcp.json"), run: join(root, "run.json"), events: remote ? eventPath : join(root, "events.jsonl"), ...(remote ? { codex: join(root, "buyer-codex.toml") } : {}) } };
110
+ writeFileSync(join(root, "run.json"), `${JSON.stringify({ schema: "supercuts.demo.run.v1", createdAt: new Date().toISOString(), runId, ...(remote ? { eventPath, sellerUrl: new URL(String(args["seller-url"])).toString() } : {}), mock, homes: roles, addresses }, null, 2)}\n`, { mode: 0o600 });
111
+ const publicSellers = activeCapabilities.filter((item) => item.capability === "sui.market-brief"), analysisSeller = activeCapabilities.find((item) => item.capability === "sui.competitor-analysis");
112
+ const settlementSellers = activeCapabilities.length ? { publicA: publicSellers[0]?.sellerId, publicB: publicSellers[1]?.sellerId, analysis: analysisSeller?.sellerId } : { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller };
113
+ return { stateDir: root, mock, funding: mock ? [] : [{ address: addresses.buyer, needs: ["Testnet SUI gas", "configured x402 asset", "shielded deposit value"] }], sellerPayTo: settlementSellers.publicA, sellerAddresses: settlementSellers, localRoleAddresses: { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller, auditor: addresses.auditor }, files: { mcp: join(root, "mcp.json"), run: join(root, "run.json"), events: remote ? eventPath : join(root, "events.jsonl"), ...(remote ? { codex: join(root, "buyer-codex.toml") } : {}) } };
80
114
  }
81
115
  async function doctor() {
82
116
  if (typeof args["buyer-home"] === "string") {
@@ -103,6 +137,11 @@ async function doctor() {
103
137
  const report = (await app.execute("doctor", {})).data;
104
138
  if (report.readyForDemo === false)
105
139
  fail("DEMO_PREREQUISITES_MISSING", `${name}: ${report.fixes.join(" ")}`);
140
+ if (name === "buyer") {
141
+ const mandate = loadMandate(home), profileSellers = [...app.profile.policy.allowedCounterparties].sort(), mandateSellers = [...mandate.allowedCounterparties].sort();
142
+ if (JSON.stringify(profileSellers) !== JSON.stringify(mandateSellers))
143
+ fail("POLICY_COUNTERPARTY_MISMATCH", "buyer mandate and payment profile allow different sellers; rerun setup refresh before funding");
144
+ }
106
145
  reports[name] = report;
107
146
  }
108
147
  finally {
@@ -114,16 +153,24 @@ async function doctor() {
114
153
  const base = new URL(args["seller-url"]), health = await fetch(new URL("/health", base), { signal: AbortSignal.timeout(90_000) }), catalog = await fetch(new URL("/v1/catalog", base), { signal: AbortSignal.timeout(30_000) });
115
154
  if (!health.ok || !catalog.ok)
116
155
  fail("SELLER_SERVICE_UNAVAILABLE", "remote seller health or catalog check failed");
156
+ const healthBody = await health.json(), buyerSigner = String(reports.buyer?.signer ?? ""), accessUrl = new URL("/v1/access", base);
157
+ accessUrl.searchParams.set("buyerAddress", buyerSigner);
158
+ const access = await fetch(accessUrl, { signal: AbortSignal.timeout(30_000) });
159
+ if (!access.ok)
160
+ fail("SELLER_SERVICE_UNAVAILABLE", "remote seller access preflight failed");
161
+ const accessBody = await access.json();
162
+ if (accessBody.allowed !== true)
163
+ fail("SELLER_BUYER_FORBIDDEN", "remote seller does not currently accept this buyer; use open access or complete owner enrollment before funding");
117
164
  const body = await catalog.json(), capabilities = body.capabilities ?? [];
118
165
  if (capabilities.filter((item) => item.manifest?.capability === "sui.market-brief").length < 2 || !capabilities.some((item) => item.manifest?.capability === "sui.competitor-analysis"))
119
166
  fail("SELLER_CATALOG_INVALID", "remote catalog needs two briefs and one competitor analysis");
120
- remote = { healthy: true, origin: base.origin, capabilities: capabilities.length };
167
+ remote = { healthy: true, origin: base.origin, capabilities: capabilities.length, buyerAccess: accessBody.mode ?? healthBody.buyerAccess ?? "unknown", buyerAllowed: true };
121
168
  }
122
169
  return { healthy: true, roles: reports, isolation: new Set(Object.values(roles)).size === 5, ...(remote ? { remote } : {}) };
123
170
  }
124
171
  async function seedPolicy() {
125
172
  const source = JSON.parse(readFileSync(resolve(value("workflow")), "utf8"));
126
- const manifest = source.capabilities?.find((item) => item.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "workflow has no competitor analysis capability");
173
+ let manifest = source.capabilities?.find((item) => item.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "workflow has no competitor analysis capability");
127
174
  validateCapabilityManifest(manifest);
128
175
  if (manifest.capability !== "sui.competitor-analysis")
129
176
  fail("DEMO_POLICY_SCOPE_INVALID", "privacy policy must target the competitor analysis capability");
@@ -131,6 +178,21 @@ async function seedPolicy() {
131
178
  try {
132
179
  if (app.profile.mock)
133
180
  fail("LIVE_PROFILE_REQUIRED", "saved demo privacy policy requires a live buyer profile");
181
+ const runPath = join(stateDir(), "run.json"), run = existsSync(runPath) ? JSON.parse(readFileSync(runPath, "utf8")) : {};
182
+ if (run.sellerUrl) {
183
+ const response = await fetch(new URL("/v1/catalog", run.sellerUrl), { signal: AbortSignal.timeout(90_000) });
184
+ if (!response.ok)
185
+ fail("SELLER_SERVICE_UNAVAILABLE", `remote seller catalog returned ${response.status}`);
186
+ const body = await response.json();
187
+ const entry = body.capabilities?.find((item) => item.manifest?.capabilityId === manifest.capabilityId && item.manifest.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "remote catalog has no matching competitor analysis capability");
188
+ if (!entry.manifest || typeof entry.blobId !== "string")
189
+ fail("SELLER_CATALOG_INVALID", "remote analysis capability has no Walrus identity");
190
+ const liveManifest = entry.manifest, liveBlobId = entry.blobId;
191
+ const discovered = (await app.execute("capability.discover", { "blob-ids": Buffer.from(JSON.stringify([liveBlobId])).toString("base64url") })).data.capabilities;
192
+ if (discovered.length !== 1 || discovered[0].blobId !== liveBlobId || discovered[0].manifestHash !== liveManifest.manifestHash)
193
+ fail("SELLER_CATALOG_INVALID", "remote analysis capability differs from its Walrus manifest");
194
+ manifest = liveManifest;
195
+ }
134
196
  const memory = { version: "supercuts.settlement-memory.v1", kind: "privacy_rule", scope: { capabilityId: manifest.capabilityId, counterparty: manifest.sellerId, network: "sui:testnet", asset: manifest.asset }, createdAt: Date.now(), signal: { requiresShielded: true } };
135
197
  const parameters = { "operation-id": value("operation-id", `privacy-${manifest.capabilityId}`), memory: Buffer.from(JSON.stringify(memory)).toString("base64url") };
136
198
  const token = app.approve(app.preview("memory.policy", parameters).challengeId);
@@ -440,7 +502,7 @@ async function replay() {
440
502
  return { label: "recorded funded run", freshTransaction: false, events };
441
503
  }
442
504
  try {
443
- const data = command === "setup" ? await setup() : command === "doctor" ? await doctor() : command === "seed-policy" ? await seedPolicy() : command === "reuse-memories" ? reuseMemories() : command === "check" ? await check() : command === "live" ? await live() : command === "replay" ? await replay() : fail("COMMAND_UNKNOWN", "use setup, doctor, seed-policy, reuse-memories, live, replay, or check");
505
+ const data = ["help", "--help", "-h"].includes(command) ? help() : command === "setup" ? await setup() : command === "doctor" ? await doctor() : command === "seed-policy" ? await seedPolicy() : command === "reuse-memories" ? reuseMemories() : command === "check" ? await check() : command === "live" ? await live() : command === "replay" ? await replay() : fail("COMMAND_UNKNOWN", "use setup, doctor, seed-policy, reuse-memories, live, replay, or check");
444
506
  send(data);
445
507
  }
446
508
  catch (error) {
@@ -0,0 +1,38 @@
1
+ export interface FundingPlan {
2
+ address: string;
3
+ publicBalanceMist: string;
4
+ shieldedBalanceMist: string;
5
+ publicPaymentMist: string;
6
+ shieldedPaymentMist: string;
7
+ gasReserveMist: string;
8
+ changeReserveMist: string;
9
+ poolDepositMist: string;
10
+ requiredPublicMist: string;
11
+ publicShortfallMist: string;
12
+ faucetRequired: boolean;
13
+ depositRequired: boolean;
14
+ }
15
+ export declare function calculateFundingPlan(input: {
16
+ address: string;
17
+ publicBalance: bigint;
18
+ shieldedBalance: bigint;
19
+ publicPayment: bigint;
20
+ shieldedPayment: bigint;
21
+ gasReserve: bigint;
22
+ changeReserve: bigint;
23
+ }): FundingPlan;
24
+ export declare function prepareFunding(home: string, values: {
25
+ publicPayment: string;
26
+ shieldedPayment: string;
27
+ gasReserve?: string;
28
+ changeReserve?: string;
29
+ }): Promise<FundingPlan>;
30
+ export declare function executeFunding(home: string, values: {
31
+ publicPayment: string;
32
+ shieldedPayment: string;
33
+ gasReserve?: string;
34
+ changeReserve?: string;
35
+ operationId: string;
36
+ requestFaucet: boolean;
37
+ confirmed: boolean;
38
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,78 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { fail } from "./errors.js";
4
+ import { Phase3App } from "./phase3.js";
5
+ import { writeClient } from "./sui.js";
6
+ const amount = (value, name) => /^[0-9]+$/.test(value) ? BigInt(value) : fail("FUNDING_INPUT_INVALID", `${name} must be unsigned MIST`);
7
+ const POOL_DEPOSIT_DENOMINATIONS = [10000000n, 100000000n];
8
+ const standardPoolDeposit = (shortfall) => {
9
+ if (shortfall === 0n)
10
+ return 0n;
11
+ const denomination = POOL_DEPOSIT_DENOMINATIONS.find((candidate) => candidate >= shortfall);
12
+ return denomination ?? fail("FUNDING_PLAN_UNSUPPORTED", `shielded funding needs ${shortfall} MIST, above the largest supported single deposit of ${POOL_DEPOSIT_DENOMINATIONS.at(-1)} MIST`);
13
+ };
14
+ export function calculateFundingPlan(input) {
15
+ const shieldedTarget = input.shieldedPayment > 0n ? input.shieldedPayment + input.changeReserve : 0n;
16
+ const poolShortfall = shieldedTarget > input.shieldedBalance ? shieldedTarget - input.shieldedBalance : 0n;
17
+ const poolDeposit = standardPoolDeposit(poolShortfall);
18
+ const requiredPublic = input.publicPayment + poolDeposit + input.gasReserve;
19
+ const publicShortfall = requiredPublic > input.publicBalance ? requiredPublic - input.publicBalance : 0n;
20
+ return { address: input.address, publicBalanceMist: input.publicBalance.toString(), shieldedBalanceMist: input.shieldedBalance.toString(), publicPaymentMist: input.publicPayment.toString(), shieldedPaymentMist: input.shieldedPayment.toString(), gasReserveMist: input.gasReserve.toString(), changeReserveMist: input.changeReserve.toString(), poolDepositMist: poolDeposit.toString(), requiredPublicMist: requiredPublic.toString(), publicShortfallMist: publicShortfall.toString(), faucetRequired: publicShortfall > 0n, depositRequired: poolDeposit > 0n };
21
+ }
22
+ export async function prepareFunding(home, values) {
23
+ const publicPayment = amount(values.publicPayment, "public payment"), shieldedPayment = amount(values.shieldedPayment, "shielded payment");
24
+ const gasReserve = amount(values.gasReserve ?? "100000000", "gas reserve"), changeReserve = amount(values.changeReserve ?? "1000000", "change reserve");
25
+ const app = new Phase3App(home);
26
+ try {
27
+ if (app.profile.role !== "buyer" || app.profile.mock)
28
+ fail("LIVE_PROFILE_REQUIRED", "funding requires a live buyer role home");
29
+ const doctor = await app.execute("doctor", {}), address = String(doctor.data.signer ?? "");
30
+ const endpoint = app.profile.live?.writeGrpcEndpoint ?? fail("LIVE_PROFILE_INVALID", "buyer write RPC is missing");
31
+ const [{ balance }, shielded] = await Promise.all([writeClient(endpoint).getBalance({ owner: address, coinType: "0x2::sui::SUI" }), app.execute("wallet.balance", {})]);
32
+ return calculateFundingPlan({ address, publicBalance: BigInt(balance.balance), shieldedBalance: BigInt(String(shielded.data.available ?? "0")), publicPayment, shieldedPayment, gasReserve, changeReserve });
33
+ }
34
+ finally {
35
+ app.close();
36
+ }
37
+ }
38
+ export async function executeFunding(home, values) {
39
+ if (!values.confirmed)
40
+ fail("FUNDING_CONFIRMATION_REQUIRED", "funding execution requires --confirm after reviewing the plan");
41
+ let plan = await prepareFunding(home, values);
42
+ if (plan.faucetRequired) {
43
+ if (!values.requestFaucet)
44
+ fail("BUYER_FUNDING_REQUIRED", `buyer needs ${plan.publicShortfallMist} additional MIST; rerun with --request-faucet --confirm or fund ${plan.address} manually`);
45
+ execFileSync("sui", ["client", "faucet", "--url", "https://faucet.testnet.sui.io", "--address", plan.address], { stdio: "inherit" });
46
+ for (let attempt = 0; attempt < 15; attempt++) {
47
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
48
+ plan = await prepareFunding(home, values);
49
+ if (!plan.faucetRequired)
50
+ break;
51
+ }
52
+ if (plan.faucetRequired)
53
+ fail("FAUCET_ALLOCATION_INSUFFICIENT", `the public faucet allocation did not reach the required ${plan.requiredPublicMist} MIST; wait for finality and inspect funding status before requesting another allocation`);
54
+ }
55
+ if (!plan.depositRequired)
56
+ return { funded: true, faucetRequested: values.requestFaucet, deposit: "not-required", plan };
57
+ if (!values.operationId)
58
+ fail("ARGUMENT_REQUIRED", "funding execute requires --operation-id for safe retry");
59
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
60
+ try {
61
+ if (process.stdin.isTTY) {
62
+ const answer = (await rl.question(`Deposit ${plan.poolDepositMist} MIST into the experimental Testnet pool? type yes: `)).trim().toLowerCase();
63
+ if (answer !== "yes")
64
+ fail("FUNDING_CANCELLED", "pool deposit was not confirmed");
65
+ }
66
+ }
67
+ finally {
68
+ rl.close();
69
+ }
70
+ const app = new Phase3App(home);
71
+ try {
72
+ const deposit = await app.execute("wallet.deposit", { amount: plan.poolDepositMist, "operation-id": values.operationId, authorize: true });
73
+ return { funded: true, faucetRequested: values.requestFaucet, deposit: deposit.data, plan: await prepareFunding(home, values) };
74
+ }
75
+ finally {
76
+ app.close();
77
+ }
78
+ }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./indexer.js";
15
15
  export * from "./memory.js";
16
16
  export * from "./settlement.js";
17
17
  export * from "./seller-runtime.js";
18
+ export * from "./seller-research.js";
18
19
  export * from "./seller-stdio.js";
19
20
  export * from "./live-index.js";
20
21
  export * from "./live-runtime.js";
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from "./indexer.js";
15
15
  export * from "./memory.js";
16
16
  export * from "./settlement.js";
17
17
  export * from "./seller-runtime.js";
18
+ export * from "./seller-research.js";
18
19
  export * from "./seller-stdio.js";
19
20
  export * from "./live-index.js";
20
21
  export * from "./live-runtime.js";
@@ -64,7 +64,14 @@ export class LiveDealPorts {
64
64
  return Ed25519Keypair.fromSecretKey(decoded.secretKey);
65
65
  }
66
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) });
67
+ const url = new URL(path, origin);
68
+ let response;
69
+ try {
70
+ response = await this.http(url, { ...(body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}), redirect: "error", signal: AbortSignal.timeout(90_000) });
71
+ }
72
+ catch (error) {
73
+ return fail("DEAL_SELLER_UNAVAILABLE", `seller ${url.origin} is unreachable; check its health endpoint and network route`, error);
74
+ }
68
75
  let value;
69
76
  try {
70
77
  value = await response.json();
@@ -5,6 +5,8 @@ import { Groth16Prover, type ProverConfig } from "./prover.js";
5
5
  import { type QueuedExit } from "./v2-exits.js";
6
6
  import { TatumLedger } from "./tatum.js";
7
7
  import type { RoleStore } from "./store.js";
8
+ export declare function x402ResourceFailure(status: number, response: Uint8Array): string;
9
+ export declare function timestampExpirationUnsupported(value: unknown): boolean;
8
10
  export interface LiveRuntimeProfile {
9
11
  role: "buyer" | "seller" | "auditor";
10
12
  packageId: string;
@@ -45,13 +47,14 @@ export declare class LiveRuntime {
45
47
  readonly prover: Groth16Prover;
46
48
  private constructor();
47
49
  static create(profile: LiveRuntimeProfile, store: RoleStore): Promise<LiveRuntime>;
48
- doctor(): Promise<Record<string, unknown>>;
50
+ doctor(proofsRequired?: boolean): Promise<Record<string, unknown>>;
49
51
  balance(): Promise<Record<string, unknown>>;
50
52
  private signed;
51
53
  private operationContext;
52
54
  private existingSigned;
53
55
  private submit;
54
56
  private confirmTatum;
57
+ paymentStatus(operationId: string, reconcile?: boolean): Promise<Record<string, unknown>>;
55
58
  private prepareOpening;
56
59
  deposit(amount: bigint, operationId: string): Promise<Record<string, unknown>>;
57
60
  recover(): Promise<Record<string, unknown>>;