@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.
- package/README.md +28 -5
- package/config/phase1.testnet.json +20 -0
- package/config/proving-manifest.json +11 -0
- package/dist/buyer-setup.js +6 -6
- package/dist/cli.js +9 -3
- package/dist/deal-adapter.js +0 -2
- package/dist/deal-engine.js +5 -5
- package/dist/deal.js +2 -2
- package/dist/demo.js +70 -8
- package/dist/funding.d.ts +38 -0
- package/dist/funding.js +78 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-deal-ports.js +8 -1
- package/dist/live-runtime.d.ts +4 -1
- package/dist/live-runtime.js +83 -11
- package/dist/live-seller-transport.js +5 -1
- package/dist/mandate.js +10 -9
- package/dist/mcp-bootstrap.js +24 -2
- package/dist/mcp.js +39 -19
- package/dist/onboarding-doctor.js +21 -5
- package/dist/phase3.js +38 -6
- package/dist/remote-buyer.d.ts +18 -0
- package/dist/remote-buyer.js +59 -5
- package/dist/role-lock.d.ts +8 -0
- package/dist/role-lock.js +54 -0
- package/dist/seller-catalog-generate.js +4 -4
- package/dist/seller-directory-cli.js +2 -1
- package/dist/seller-live-cli.js +10 -1
- package/dist/seller-package.d.ts +9 -0
- package/dist/seller-package.js +50 -0
- package/dist/seller-provision.js +15 -1
- package/dist/seller-research.d.ts +50 -0
- package/dist/seller-research.js +210 -0
- package/dist/seller-scaffold.d.ts +1 -0
- package/dist/seller-scaffold.js +35 -0
- package/dist/seller-service.d.ts +1 -1
- package/dist/seller-service.js +14 -21
- package/dist/seller-setup.d.ts +3 -0
- package/dist/seller-setup.js +107 -0
- package/dist/x402-resource-service.d.ts +2 -0
- package/dist/x402-resource-service.js +36 -17
- 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-
|
|
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
|
|
120
|
-
only the model, action, and token counts.
|
|
121
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/buyer-setup.js
CHANGED
|
@@ -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"
|
|
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
|
|
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
|
|
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 {
|
package/dist/deal-adapter.js
CHANGED
|
@@ -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)
|
package/dist/deal-engine.js
CHANGED
|
@@ -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
|
|
182
|
+
return this.journal.advance(operationId, "outcome_pending", "complete");
|
|
183
183
|
}
|
|
184
184
|
if (existing)
|
|
185
|
-
return
|
|
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.
|
|
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.
|
|
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
|
|
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" &&
|
|
217
|
-
fail("
|
|
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")),
|
|
53
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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>>;
|
package/dist/funding.js
ADDED
|
@@ -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";
|
package/dist/live-deal-ports.js
CHANGED
|
@@ -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
|
|
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();
|
package/dist/live-runtime.d.ts
CHANGED
|
@@ -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>>;
|