@supercuts-walrus/backend 0.0.0-stage → 0.1.1

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 (155) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +249 -2
  3. package/dist/audit.d.ts +52 -0
  4. package/dist/audit.js +157 -0
  5. package/dist/backend.d.ts +27 -0
  6. package/dist/backend.js +62 -0
  7. package/dist/buyer-setup.d.ts +6 -0
  8. package/dist/buyer-setup.js +241 -0
  9. package/dist/capability.d.ts +32 -0
  10. package/dist/capability.js +87 -0
  11. package/dist/claude.d.ts +44 -0
  12. package/dist/claude.js +98 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +32 -0
  15. package/dist/config.d.ts +20 -0
  16. package/dist/config.js +58 -0
  17. package/dist/dashboard.d.ts +5 -0
  18. package/dist/dashboard.js +21 -0
  19. package/dist/deal-adapter.d.ts +3 -0
  20. package/dist/deal-adapter.js +39 -0
  21. package/dist/deal-engine.d.ts +62 -0
  22. package/dist/deal-engine.js +211 -0
  23. package/dist/deal-wire.d.ts +16 -0
  24. package/dist/deal-wire.js +65 -0
  25. package/dist/deal.d.ts +79 -0
  26. package/dist/deal.js +228 -0
  27. package/dist/demo.d.ts +2 -0
  28. package/dist/demo.js +511 -0
  29. package/dist/errors.d.ts +6 -0
  30. package/dist/errors.js +13 -0
  31. package/dist/events.d.ts +27 -0
  32. package/dist/events.js +68 -0
  33. package/dist/facilitator.d.ts +21 -0
  34. package/dist/facilitator.js +173 -0
  35. package/dist/finality.d.ts +2 -0
  36. package/dist/finality.js +27 -0
  37. package/dist/funding.d.ts +38 -0
  38. package/dist/funding.js +78 -0
  39. package/dist/hosted-exit-cli.d.ts +2 -0
  40. package/dist/hosted-exit-cli.js +37 -0
  41. package/dist/hosted-exit.d.ts +24 -0
  42. package/dist/hosted-exit.js +70 -0
  43. package/dist/hosted-seller-import.d.ts +2 -0
  44. package/dist/hosted-seller-import.js +70 -0
  45. package/dist/hosted-seller-repository.d.ts +31 -0
  46. package/dist/hosted-seller-repository.js +214 -0
  47. package/dist/index.d.ts +37 -0
  48. package/dist/index.js +37 -0
  49. package/dist/indexer.d.ts +63 -0
  50. package/dist/indexer.js +60 -0
  51. package/dist/live-deal-ports.d.ts +41 -0
  52. package/dist/live-deal-ports.js +212 -0
  53. package/dist/live-index.d.ts +28 -0
  54. package/dist/live-index.js +149 -0
  55. package/dist/live-runtime.d.ts +85 -0
  56. package/dist/live-runtime.js +644 -0
  57. package/dist/live-seller-transport.d.ts +30 -0
  58. package/dist/live-seller-transport.js +169 -0
  59. package/dist/live-seller-verifier.d.ts +11 -0
  60. package/dist/live-seller-verifier.js +65 -0
  61. package/dist/mandate.d.ts +34 -0
  62. package/dist/mandate.js +179 -0
  63. package/dist/mcp-bootstrap.d.ts +2 -0
  64. package/dist/mcp-bootstrap.js +61 -0
  65. package/dist/mcp.d.ts +2 -0
  66. package/dist/mcp.js +207 -0
  67. package/dist/memory-worker.d.ts +2 -0
  68. package/dist/memory-worker.js +38 -0
  69. package/dist/memory.d.ts +15 -0
  70. package/dist/memory.js +17 -0
  71. package/dist/memwal.d.ts +35 -0
  72. package/dist/memwal.js +83 -0
  73. package/dist/negotiation.d.ts +23 -0
  74. package/dist/negotiation.js +58 -0
  75. package/dist/notes.d.ts +11 -0
  76. package/dist/notes.js +26 -0
  77. package/dist/onboarding-doctor.d.ts +1 -0
  78. package/dist/onboarding-doctor.js +87 -0
  79. package/dist/phase3.d.ts +100 -0
  80. package/dist/phase3.js +1174 -0
  81. package/dist/procurement.d.ts +8 -0
  82. package/dist/procurement.js +19 -0
  83. package/dist/protocol-engine.d.ts +121 -0
  84. package/dist/protocol-engine.js +149 -0
  85. package/dist/prover.d.ts +28 -0
  86. package/dist/prover.js +70 -0
  87. package/dist/relayer.d.ts +32 -0
  88. package/dist/relayer.js +43 -0
  89. package/dist/remote-buyer.d.ts +85 -0
  90. package/dist/remote-buyer.js +285 -0
  91. package/dist/resource-server.d.ts +2 -0
  92. package/dist/resource-server.js +24 -0
  93. package/dist/resource.d.ts +22 -0
  94. package/dist/resource.js +55 -0
  95. package/dist/role-lock.d.ts +8 -0
  96. package/dist/role-lock.js +54 -0
  97. package/dist/seam.d.ts +2 -0
  98. package/dist/seam.js +117 -0
  99. package/dist/seller-catalog-generate.d.ts +2 -0
  100. package/dist/seller-catalog-generate.js +68 -0
  101. package/dist/seller-database.d.ts +41 -0
  102. package/dist/seller-database.js +80 -0
  103. package/dist/seller-directory-cli.d.ts +2 -0
  104. package/dist/seller-directory-cli.js +24 -0
  105. package/dist/seller-directory.d.ts +5 -0
  106. package/dist/seller-directory.js +50 -0
  107. package/dist/seller-live-cli.d.ts +2 -0
  108. package/dist/seller-live-cli.js +53 -0
  109. package/dist/seller-package.d.ts +9 -0
  110. package/dist/seller-package.js +50 -0
  111. package/dist/seller-provision.d.ts +2 -0
  112. package/dist/seller-provision.js +84 -0
  113. package/dist/seller-register-cli.d.ts +2 -0
  114. package/dist/seller-register-cli.js +40 -0
  115. package/dist/seller-runtime.d.ts +60 -0
  116. package/dist/seller-runtime.js +96 -0
  117. package/dist/seller-service.d.ts +34 -0
  118. package/dist/seller-service.js +422 -0
  119. package/dist/seller-state.d.ts +19 -0
  120. package/dist/seller-state.js +60 -0
  121. package/dist/seller-stdio.d.ts +6 -0
  122. package/dist/seller-stdio.js +29 -0
  123. package/dist/seller-v2-provision.d.ts +2 -0
  124. package/dist/seller-v2-provision.js +71 -0
  125. package/dist/seller-watch.d.ts +2 -0
  126. package/dist/seller-watch.js +45 -0
  127. package/dist/settlement.d.ts +95 -0
  128. package/dist/settlement.js +141 -0
  129. package/dist/store.d.ts +36 -0
  130. package/dist/store.js +193 -0
  131. package/dist/sui.d.ts +49 -0
  132. package/dist/sui.js +77 -0
  133. package/dist/tatum.d.ts +17 -0
  134. package/dist/tatum.js +83 -0
  135. package/dist/transcript.d.ts +31 -0
  136. package/dist/transcript.js +52 -0
  137. package/dist/v2-exit-relayer.d.ts +34 -0
  138. package/dist/v2-exit-relayer.js +102 -0
  139. package/dist/v2-exit-worker.d.ts +2 -0
  140. package/dist/v2-exit-worker.js +97 -0
  141. package/dist/v2-exits.d.ts +30 -0
  142. package/dist/v2-exits.js +78 -0
  143. package/dist/v2-prefund.d.ts +19 -0
  144. package/dist/v2-prefund.js +43 -0
  145. package/dist/v2-relayer-cli.d.ts +2 -0
  146. package/dist/v2-relayer-cli.js +39 -0
  147. package/dist/wallet.d.ts +32 -0
  148. package/dist/wallet.js +90 -0
  149. package/dist/workflow-memory.d.ts +22 -0
  150. package/dist/workflow-memory.js +49 -0
  151. package/dist/x402-resource-service.d.ts +36 -0
  152. package/dist/x402-resource-service.js +173 -0
  153. package/dist/x402.d.ts +46 -0
  154. package/dist/x402.js +136 -0
  155. package/package.json +60 -3
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { resolve, join } from "node:path";
4
+ import { loadStateKey } from "./config.js";
5
+ import { Phase3App } from "./phase3.js";
6
+ import { LiveRuntime } from "./live-runtime.js";
7
+ import { RoleStore } from "./store.js";
8
+ import { V2ExitQueue } from "./v2-exits.js";
9
+ import { V2ExitRelayer } from "./v2-exit-relayer.js";
10
+ const args = new Map();
11
+ for (let index = 2; index < process.argv.length; index += 2) {
12
+ const name = process.argv[index], value = process.argv[index + 1];
13
+ if (!name?.startsWith("--") || !value)
14
+ throw Error("expected --name value arguments");
15
+ args.set(name.slice(2), value);
16
+ }
17
+ const required = (name) => args.get(name) ?? (() => { throw Error(`--${name} is required`); })();
18
+ const sellerHome = resolve(required("seller-home")), relayerHome = resolve(required("relayer-home"));
19
+ const pin = JSON.parse(readFileSync(resolve(required("config")), "utf8"));
20
+ const operationId = required("operation-id"), evidencePath = resolve(required("evidence-file"));
21
+ const pause = (milliseconds) => new Promise((done) => setTimeout(done, milliseconds));
22
+ function requestFromStore(app) {
23
+ const encoded = app.store.getSecret(`v2-exit-request:${operationId}`);
24
+ if (!encoded)
25
+ throw Error("queued exit request is unavailable in the seller store");
26
+ const raw = JSON.parse(encoded.toString());
27
+ const bytes32 = (name) => Buffer.from(String(raw[name] ?? "").replace(/^0x/, ""), "hex");
28
+ return { operationId: String(raw.operationId), root: bytes32("root"), noteNullifier: bytes32("noteNullifier"),
29
+ recipient: String(raw.recipient), amount: BigInt(String(raw.amount)), exactExit: raw.exactExit === true,
30
+ changeCommitment: bytes32("changeCommitment"), newRoot: bytes32("newRoot"), newIndex: BigInt(String(raw.newIndex)),
31
+ proof: Buffer.from(String(raw.proof), "base64") };
32
+ }
33
+ async function attempt() {
34
+ if (existsSync(evidencePath))
35
+ return true;
36
+ const seller = new Phase3App(sellerHome);
37
+ const relayerStore = new RoleStore(join(relayerHome, "state.sqlite"), loadStateKey(join(relayerHome, "state.key")));
38
+ try {
39
+ const queue = new V2ExitQueue(seller.store), scheduled = queue.get(operationId);
40
+ if (!scheduled)
41
+ throw Error("queued exit does not exist");
42
+ if (scheduled.status === "queued")
43
+ queue.assertDue(operationId);
44
+ else if (scheduled.status !== "finalized")
45
+ throw Error("exit is awaiting an unresolved submission");
46
+ if (!seller.profile.live)
47
+ throw Error("seller profile has no live Testnet deployment");
48
+ const relayer = new V2ExitRelayer(relayerStore, pin), runtime = await LiveRuntime.create(seller.profile, seller.store);
49
+ let request = requestFromStore(seller);
50
+ const existing = relayerStore.db.prepare("SELECT digest FROM v2_relay_journal WHERE operation_id=?").get(request.operationId);
51
+ if (!existing) {
52
+ const state = await runtime.index.sync();
53
+ if (Buffer.from(request.root).toString("hex") !== state.root.toString(16).padStart(64, "0")) {
54
+ await runtime.refreshV2Exit(operationId);
55
+ request = requestFromStore(seller);
56
+ }
57
+ }
58
+ const relayed = await relayer.submit(request);
59
+ const parameters = { "exit-id": operationId, digest: relayed.digest };
60
+ const token = seller.approve(seller.preview("wallet.exit.finalize", parameters).challengeId);
61
+ const finalized = (await seller.execute("wallet.exit.finalize", { ...parameters, "approval-token": token.token })).data;
62
+ const evidence = { schema: "supercuts.v2.exit-completion.v1", operationId, relayer: relayed.relayer, digest: relayed.digest,
63
+ checkpoint: relayed.checkpoint, payoutAddress: scheduled.payoutAddress, amount: scheduled.amount.toString(),
64
+ exactExit: scheduled.exactExit, tatumVerified: finalized.tatumVerified, finalized: finalized.finalized, completedAt: new Date().toISOString() };
65
+ const temporary = `${evidencePath}.tmp`;
66
+ writeFileSync(temporary, `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600 });
67
+ renameSync(temporary, evidencePath);
68
+ process.stdout.write(`${JSON.stringify(evidence)}\n`);
69
+ return true;
70
+ }
71
+ finally {
72
+ relayerStore.close();
73
+ seller.close();
74
+ }
75
+ }
76
+ const window = new Phase3App(sellerHome);
77
+ let windowEnd;
78
+ try {
79
+ windowEnd = new V2ExitQueue(window.store).get(operationId)?.windowEnd ?? 0;
80
+ }
81
+ finally {
82
+ window.close();
83
+ }
84
+ if (!windowEnd)
85
+ throw Error("queued exit does not exist");
86
+ while (Date.now() < windowEnd) {
87
+ try {
88
+ if (await attempt())
89
+ process.exit(0);
90
+ }
91
+ catch (error) {
92
+ const message = error instanceof Error ? error.message : "unknown error";
93
+ process.stderr.write(`${JSON.stringify({ operationId, error: message, at: new Date().toISOString() })}\n`);
94
+ }
95
+ await pause(Math.min(20_000, Math.max(1_000, windowEnd - Date.now())));
96
+ }
97
+ throw Error("approved exit window closed before finality was recorded");
@@ -0,0 +1,30 @@
1
+ import type { RoleStore } from "./store.js";
2
+ export interface QueuedExit {
3
+ operationId: string;
4
+ commitment: string;
5
+ payoutAddress: string;
6
+ amount: bigint;
7
+ exactExit: boolean;
8
+ windowStart: number;
9
+ executeAt: number;
10
+ windowEnd: number;
11
+ status: "queued" | "submitted" | "finalized";
12
+ }
13
+ /** Owner-side queue. Only the address and proof are sent to a separate relayer. */
14
+ export declare class V2ExitQueue {
15
+ private readonly store;
16
+ constructor(store: RoleStore);
17
+ queue(request: {
18
+ operationId: string;
19
+ commitment: string;
20
+ amount: bigint;
21
+ exactExit: boolean;
22
+ windowStart: number;
23
+ windowEnd: number;
24
+ }, approved: boolean, now?: number): QueuedExit;
25
+ get(operationId: string): QueuedExit | undefined;
26
+ due(now?: number): QueuedExit[];
27
+ assertDue(operationId: string, now?: number): QueuedExit;
28
+ recordSubmission(operationId: string, digest: string): void;
29
+ recordFinalized(operationId: string, digest: string): void;
30
+ }
@@ -0,0 +1,78 @@
1
+ import { randomInt } from "node:crypto";
2
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
3
+ import { fail } from "./errors.js";
4
+ const MAX_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
5
+ /** Owner-side queue. Only the address and proof are sent to a separate relayer. */
6
+ export class V2ExitQueue {
7
+ store;
8
+ constructor(store) {
9
+ this.store = store;
10
+ store.db.exec(`CREATE TABLE IF NOT EXISTS v2_exit_queue (
11
+ operation_id TEXT PRIMARY KEY, commitment TEXT NOT NULL UNIQUE, payout_address TEXT NOT NULL UNIQUE,
12
+ amount TEXT NOT NULL, exact_exit INTEGER NOT NULL, window_start INTEGER NOT NULL, execute_at INTEGER NOT NULL,
13
+ window_end INTEGER NOT NULL, status TEXT NOT NULL, transaction_digest TEXT UNIQUE
14
+ )`);
15
+ }
16
+ queue(request, approved, now = Date.now()) {
17
+ if (!approved)
18
+ fail("OWNER_APPROVAL_REQUIRED", "owner approval is required to schedule a withdrawal");
19
+ if (!request.operationId || request.operationId.length > 256 || !/^[0-9a-f]{64}$/i.test(request.commitment))
20
+ fail("EXIT_REQUEST_INVALID", "operation ID or note commitment is invalid");
21
+ if (request.amount <= 0n || request.amount > (1n << 64n) - 1n || (!request.exactExit && request.amount !== 1000000n))
22
+ fail("EXIT_AMOUNT_INVALID", "standard exit must be 1,000,000 MIST; exact exit reveals its amount");
23
+ if (!Number.isSafeInteger(request.windowStart) || !Number.isSafeInteger(request.windowEnd) || request.windowStart < now || request.windowEnd <= request.windowStart || request.windowEnd - request.windowStart > MAX_WINDOW_MS)
24
+ fail("EXIT_WINDOW_INVALID", "exit window must be in the future and no longer than seven days");
25
+ const prior = this.get(request.operationId);
26
+ if (prior) {
27
+ if (prior.commitment !== request.commitment || prior.amount !== request.amount || prior.exactExit !== request.exactExit || prior.windowStart !== request.windowStart || prior.windowEnd !== request.windowEnd)
28
+ fail("OPERATION_ID_CONFLICT", "exit operation ID is already bound to another request");
29
+ return prior;
30
+ }
31
+ // Node's randomInt uses a cryptographic random source and avoids modulo bias.
32
+ const executeAt = randomInt(request.windowStart, request.windowEnd);
33
+ const payout = Ed25519Keypair.generate();
34
+ const payoutAddress = payout.toSuiAddress();
35
+ this.store.db.exec("BEGIN IMMEDIATE");
36
+ try {
37
+ this.store.putSecretOnce(`v2-exit-payout:${request.operationId}`, Buffer.from(payout.getSecretKey()));
38
+ this.store.db.prepare("INSERT INTO v2_exit_queue(operation_id,commitment,payout_address,amount,exact_exit,window_start,execute_at,window_end,status) VALUES(?,?,?,?,?,?,?,?,'queued')")
39
+ .run(request.operationId, request.commitment, payoutAddress, request.amount.toString(), Number(request.exactExit), request.windowStart, executeAt, request.windowEnd);
40
+ this.store.db.exec("COMMIT");
41
+ }
42
+ catch (error) {
43
+ this.store.db.exec("ROLLBACK");
44
+ throw error;
45
+ }
46
+ return this.get(request.operationId);
47
+ }
48
+ get(operationId) {
49
+ const row = this.store.db.prepare("SELECT operation_id,commitment,payout_address,amount,exact_exit,window_start,execute_at,window_end,status FROM v2_exit_queue WHERE operation_id=?").get(operationId);
50
+ return row && { operationId: row.operation_id, commitment: row.commitment, payoutAddress: row.payout_address, amount: BigInt(row.amount), exactExit: Boolean(row.exact_exit), windowStart: row.window_start, executeAt: row.execute_at, windowEnd: row.window_end, status: row.status };
51
+ }
52
+ due(now = Date.now()) {
53
+ const rows = this.store.db.prepare("SELECT operation_id FROM v2_exit_queue WHERE status='queued' AND execute_at<=? AND window_end>? ORDER BY execute_at").all(now, now);
54
+ return rows.map((row) => this.get(row.operation_id));
55
+ }
56
+ assertDue(operationId, now = Date.now()) {
57
+ const exit = this.get(operationId) ?? fail("OPERATION_ID_CONFLICT", "exit has no journal entry");
58
+ if (exit.status !== "queued" || now < exit.executeAt || now >= exit.windowEnd)
59
+ fail("EXIT_WINDOW_CLOSED", "exit can only be submitted inside its scheduled window");
60
+ return exit;
61
+ }
62
+ recordSubmission(operationId, digest) {
63
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,64}$/.test(digest))
64
+ fail("EXIT_DIGEST_INVALID", "relayer transaction digest is invalid");
65
+ const row = this.store.db.prepare("SELECT status,transaction_digest FROM v2_exit_queue WHERE operation_id=?").get(operationId);
66
+ if (!row)
67
+ return fail("OPERATION_ID_CONFLICT", "exit submission has no journal entry");
68
+ if (row.transaction_digest && row.transaction_digest !== digest)
69
+ fail("OPERATION_ID_CONFLICT", "exit submission conflicts with the journal");
70
+ this.store.db.prepare("UPDATE v2_exit_queue SET status='submitted',transaction_digest=? WHERE operation_id=? AND status='queued'").run(digest, operationId);
71
+ }
72
+ recordFinalized(operationId, digest) {
73
+ const row = this.store.db.prepare("SELECT transaction_digest FROM v2_exit_queue WHERE operation_id=?").get(operationId);
74
+ if (!row || row.transaction_digest !== digest)
75
+ fail("OPERATION_ID_CONFLICT", "finality does not match the journaled relayer transaction");
76
+ this.store.db.prepare("UPDATE v2_exit_queue SET status='finalized' WHERE operation_id=?").run(operationId);
77
+ }
78
+ }
@@ -0,0 +1,19 @@
1
+ import type { RoleStore } from "./store.js";
2
+ export interface PrefundPlan {
3
+ operationId: string;
4
+ amount: bigint;
5
+ scheduledAt: number;
6
+ status: "queued" | "finalized";
7
+ transactionDigest?: string;
8
+ }
9
+ /** Buyer-side scheduling; the callback must be the live, finalized deposit path. */
10
+ export declare class V2PrefundQueue {
11
+ private readonly store;
12
+ constructor(store: RoleStore);
13
+ queue(operationId: string, amount: bigint, scheduledAt: number, approved: boolean, now?: number): PrefundPlan;
14
+ get(operationId: string): PrefundPlan | undefined;
15
+ runDue(now: number, deposit: (amount: bigint, operationId: string) => Promise<{
16
+ transactionDigest: string;
17
+ finalized: boolean;
18
+ }>): Promise<PrefundPlan[]>;
19
+ }
@@ -0,0 +1,43 @@
1
+ import { fail } from "./errors.js";
2
+ /** Buyer-side scheduling; the callback must be the live, finalized deposit path. */
3
+ export class V2PrefundQueue {
4
+ store;
5
+ constructor(store) {
6
+ this.store = store;
7
+ store.db.exec(`CREATE TABLE IF NOT EXISTS v2_prefund_queue (
8
+ operation_id TEXT PRIMARY KEY, amount TEXT NOT NULL, scheduled_at INTEGER NOT NULL,
9
+ status TEXT NOT NULL, transaction_digest TEXT UNIQUE
10
+ )`);
11
+ }
12
+ queue(operationId, amount, scheduledAt, approved, now = Date.now()) {
13
+ if (!approved)
14
+ fail("OWNER_APPROVAL_REQUIRED", "owner approval is required to schedule prefunding");
15
+ if (!operationId || operationId.length > 256 || (amount !== 10000000n && amount !== 100000000n) || !Number.isSafeInteger(scheduledAt) || scheduledAt < now)
16
+ fail("PREFUND_PLAN_INVALID", "v2 prefunding needs a standard amount and future time");
17
+ const prior = this.get(operationId);
18
+ if (prior) {
19
+ if (prior.amount !== amount || prior.scheduledAt !== scheduledAt)
20
+ fail("OPERATION_ID_CONFLICT", "prefunding operation ID identifies another plan");
21
+ return prior;
22
+ }
23
+ this.store.db.prepare("INSERT INTO v2_prefund_queue(operation_id,amount,scheduled_at,status) VALUES(?,?,?,'queued')").run(operationId, amount.toString(), scheduledAt);
24
+ return this.get(operationId);
25
+ }
26
+ get(operationId) {
27
+ const row = this.store.db.prepare("SELECT operation_id,amount,scheduled_at,status,transaction_digest FROM v2_prefund_queue WHERE operation_id=?").get(operationId);
28
+ return row && { operationId: row.operation_id, amount: BigInt(row.amount), scheduledAt: row.scheduled_at, status: row.status, ...(row.transaction_digest ? { transactionDigest: row.transaction_digest } : {}) };
29
+ }
30
+ async runDue(now, deposit) {
31
+ const rows = this.store.db.prepare("SELECT operation_id FROM v2_prefund_queue WHERE status='queued' AND scheduled_at<=? ORDER BY scheduled_at").all(now);
32
+ const finalized = [];
33
+ for (const row of rows) {
34
+ const plan = this.get(row.operation_id);
35
+ const receipt = await deposit(plan.amount, plan.operationId);
36
+ if (!receipt.finalized || !receipt.transactionDigest)
37
+ fail("PREFUND_NOT_FINAL", "prefunding callback did not return a finalized live deposit");
38
+ this.store.db.prepare("UPDATE v2_prefund_queue SET status='finalized',transaction_digest=? WHERE operation_id=? AND status='queued'").run(receipt.transactionDigest, plan.operationId);
39
+ finalized.push(this.get(plan.operationId));
40
+ }
41
+ return finalized;
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import { resolve, join } from "node:path";
4
+ import { loadStateKey } from "./config.js";
5
+ import { RoleStore } from "./store.js";
6
+ import { V2ExitRelayer } from "./v2-exit-relayer.js";
7
+ const [command, ...words] = process.argv.slice(2);
8
+ const args = new Map();
9
+ for (let index = 0; index < words.length; index += 2) {
10
+ if (!words[index]?.startsWith("--") || !words[index + 1])
11
+ throw Error("expected --name value arguments");
12
+ args.set(words[index].slice(2), words[index + 1]);
13
+ }
14
+ const required = (name) => args.get(name) ?? (() => { throw Error(`--${name} is required`); })();
15
+ const stateDir = resolve(required("state-dir"));
16
+ const config = JSON.parse(readFileSync(resolve(required("config")), "utf8"));
17
+ const store = new RoleStore(join(stateDir, "state.sqlite"), loadStateKey(join(stateDir, "state.key")));
18
+ try {
19
+ const relayer = new V2ExitRelayer(store, config);
20
+ if (command === "doctor")
21
+ console.log(JSON.stringify(await relayer.doctor()));
22
+ else if (command === "submit") {
23
+ const requestPath = resolve(required("request-file"));
24
+ if ((statSync(requestPath).mode & 0o077) !== 0)
25
+ throw Error("request file must be owner-only");
26
+ const raw = JSON.parse(readFileSync(requestPath, "utf8"));
27
+ const bytes32 = (name) => Buffer.from(String(raw[name] ?? "").replace(/^0x/, ""), "hex");
28
+ const result = await relayer.submit({ operationId: String(raw.operationId), root: bytes32("root"), noteNullifier: bytes32("noteNullifier"),
29
+ recipient: String(raw.recipient), amount: BigInt(String(raw.amount)), exactExit: raw.exactExit === true,
30
+ changeCommitment: bytes32("changeCommitment"), newRoot: bytes32("newRoot"), newIndex: BigInt(String(raw.newIndex)),
31
+ proof: Buffer.from(String(raw.proof), "base64") });
32
+ console.log(JSON.stringify(result));
33
+ }
34
+ else
35
+ throw Error("use doctor or submit");
36
+ }
37
+ finally {
38
+ store.close();
39
+ }
@@ -0,0 +1,32 @@
1
+ import { PoolEvent } from "./indexer.js";
2
+ import { RoleStore, StoredOperation } from "./store.js";
3
+ import { NoteOpening } from "./notes.js";
4
+ export interface SettlementEvidence {
5
+ digest: string;
6
+ finalized: boolean;
7
+ successful: boolean;
8
+ packageId: string;
9
+ poolId: string;
10
+ coinType: string;
11
+ events: PoolEvent[];
12
+ }
13
+ export type EvidenceReader = (digest: string) => Promise<SettlementEvidence | undefined>;
14
+ export interface SettlementExpectation {
15
+ packageId: string;
16
+ poolId: string;
17
+ coinType: string;
18
+ inputNullifier: string;
19
+ dealNullifier: string;
20
+ paymentCommitment: string;
21
+ changeCommitment: string;
22
+ }
23
+ export declare class PrivateWallet {
24
+ private readonly store;
25
+ constructor(store: RoleStore);
26
+ prepare(operationId: string, payloadHash: string, inputCommitment: string, expected: SettlementExpectation, proof: Uint8Array): void;
27
+ recordBroadcast(operationId: string, digest: string): void;
28
+ reconcile(operationId: string, read: EvidenceReader): Promise<StoredOperation>;
29
+ recoverOutput(operationId: string, role: "payment" | "change", opening: NoteOpening, event: Extract<PoolEvent, {
30
+ kind: "transfer";
31
+ }>): Promise<string>;
32
+ }
package/dist/wallet.js ADDED
@@ -0,0 +1,90 @@
1
+ import { fail } from "./errors.js";
2
+ import { encodeNoteOpening, noteCommitment } from "./notes.js";
3
+ function encodeExpectation(expected) {
4
+ const entries = Object.entries(expected);
5
+ if (entries.some(([, value]) => !value || typeof value !== "string" || value.length > 512))
6
+ fail("SETTLEMENT_EXPECTATION_INVALID", "settlement expectation is incomplete or too large");
7
+ return Buffer.from(JSON.stringify(Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)))));
8
+ }
9
+ function rejectEvidence(store, operationId, code, message) {
10
+ const status = store.getOperation(operationId)?.status;
11
+ if (status && status !== "unresolved" && status !== "finalized" && status !== "confirmed" && status !== "delivered")
12
+ store.transitionOperation(operationId, "unresolved");
13
+ return fail(code, message);
14
+ }
15
+ export class PrivateWallet {
16
+ store;
17
+ constructor(store) {
18
+ this.store = store;
19
+ }
20
+ prepare(operationId, payloadHash, inputCommitment, expected, proof) {
21
+ this.store.beginOperation(operationId, payloadHash);
22
+ this.store.reserveNote(inputCommitment, operationId);
23
+ this.store.putSecretOnce(`settlement:${operationId}`, encodeExpectation(expected));
24
+ this.store.putNote(inputCommitment, "input", operationId, undefined, expected.inputNullifier);
25
+ const operation = this.store.getOperation(operationId);
26
+ if (operation.status === "reserved") {
27
+ this.store.putSecretOnce(`proof:${operationId}`, proof);
28
+ this.store.transitionOperation(operationId, "proof_persisted");
29
+ }
30
+ else if (!this.store.getSecret(`proof:${operationId}`)?.equals(Buffer.from(proof))) {
31
+ fail("PROOF_RETRY_CONFLICT", "retry proof differs from the persisted proof");
32
+ }
33
+ }
34
+ recordBroadcast(operationId, digest) {
35
+ const operation = this.store.getOperation(operationId) ?? fail("OPERATION_NOT_FOUND", `operation ${operationId} does not exist`);
36
+ if (operation.txDigest && operation.txDigest !== digest)
37
+ fail("OPERATION_DIGEST_CONFLICT", "operation cannot be rebound to another transaction");
38
+ if (operation.status === "proof_persisted" || (operation.status === "unresolved" && this.store.getSecret(`proof:${operationId}`)))
39
+ this.store.transitionOperation(operationId, "broadcast", digest);
40
+ else if (operation.status !== "broadcast" && operation.status !== "finalized" && operation.status !== "confirmed" && operation.status !== "delivered")
41
+ fail("OPERATION_NOT_READY", "proof must be persisted before broadcast");
42
+ }
43
+ async reconcile(operationId, read) {
44
+ const operation = this.store.getOperation(operationId) ?? fail("OPERATION_NOT_FOUND", `operation ${operationId} does not exist`);
45
+ const encodedExpectation = this.store.getSecret(`settlement:${operationId}`) ?? fail("SETTLEMENT_EXPECTATION_MISSING", "operation has no persisted settlement expectation");
46
+ let expected;
47
+ try {
48
+ expected = JSON.parse(encodedExpectation.toString());
49
+ }
50
+ catch (error) {
51
+ return fail("SETTLEMENT_EXPECTATION_INVALID", "persisted settlement expectation is invalid", error);
52
+ }
53
+ if (!operation.txDigest)
54
+ return operation;
55
+ const evidence = await read(operation.txDigest);
56
+ if (!evidence || !evidence.finalized) {
57
+ if (operation.status !== "unresolved")
58
+ this.store.transitionOperation(operationId, "unresolved");
59
+ return this.store.getOperation(operationId);
60
+ }
61
+ if (evidence.digest !== operation.txDigest)
62
+ rejectEvidence(this.store, operationId, "SETTLEMENT_DIGEST_MISMATCH", "evidence reader returned another transaction");
63
+ if (!evidence.successful)
64
+ rejectEvidence(this.store, operationId, "SETTLEMENT_FAILED", "finalized transaction did not execute successfully");
65
+ if (evidence.packageId !== expected.packageId || evidence.poolId !== expected.poolId || evidence.coinType !== expected.coinType)
66
+ rejectEvidence(this.store, operationId, "SETTLEMENT_TARGET_MISMATCH", "transaction does not settle against the pinned pool and asset");
67
+ const transfer = evidence.events.find((event) => event.kind === "transfer" && event.inputNullifier === expected.inputNullifier && event.dealNullifier === expected.dealNullifier && event.paymentCommitment === expected.paymentCommitment && event.changeCommitment === expected.changeCommitment);
68
+ if (!transfer)
69
+ rejectEvidence(this.store, operationId, "SETTLEMENT_EVENT_MISSING", "finalized transaction does not contain the expected transfer");
70
+ if (operation.status === "broadcast" || operation.status === "unresolved")
71
+ this.store.transitionOperation(operationId, "finalized", evidence.digest);
72
+ this.store.markNoteSpent(expected.inputNullifier);
73
+ return this.store.getOperation(operationId);
74
+ }
75
+ async recoverOutput(operationId, role, opening, event) {
76
+ const operation = this.store.getOperation(operationId) ?? fail("OPERATION_NOT_FOUND", `operation ${operationId} does not exist`);
77
+ if (!["finalized", "confirmed", "delivered"].includes(operation.status))
78
+ fail("NOTE_RECOVERY_NOT_FINAL", "output notes can only be recovered from a finalized operation");
79
+ const expected = JSON.parse((this.store.getSecret(`settlement:${operationId}`) ?? fail("SETTLEMENT_EXPECTATION_MISSING", "operation has no persisted settlement expectation")).toString());
80
+ if (event.inputNullifier !== expected.inputNullifier || event.dealNullifier !== expected.dealNullifier || event.paymentCommitment !== expected.paymentCommitment || event.changeCommitment !== expected.changeCommitment)
81
+ fail("NOTE_RECOVERY_EVENT_MISMATCH", "recovery event does not match the persisted settlement");
82
+ const commitment = await noteCommitment(opening);
83
+ const expectedCommitment = role === "payment" ? event.paymentCommitment : event.changeCommitment;
84
+ if (commitment !== expectedCommitment)
85
+ fail("NOTE_RECOVERY_MISMATCH", `${role} opening does not match the finalized commitment`);
86
+ this.store.putSecretOnce(`note:${commitment}`, encodeNoteOpening(opening));
87
+ this.store.putNote(commitment, role, operationId, event.firstLeafIndex + (role === "change" ? 1n : 0n));
88
+ return commitment;
89
+ }
90
+ }
@@ -0,0 +1,22 @@
1
+ import { MemoryFact, RailPolicy } from "./memory.js";
2
+ import { RoleStore } from "./store.js";
3
+ export interface MemoryWriter {
4
+ remember(fact: MemoryFact): Promise<string>;
5
+ }
6
+ export interface RecalledRule {
7
+ hiddenAmountRequired?: boolean;
8
+ seller?: string;
9
+ maxAmount?: bigint;
10
+ }
11
+ export declare class WorkflowMemory {
12
+ private readonly store;
13
+ private readonly memory;
14
+ constructor(store: RoleStore, memory: MemoryWriter);
15
+ record(fact: MemoryFact): Promise<string>;
16
+ completed(): Array<{
17
+ operationId: string;
18
+ blobId: string;
19
+ }>;
20
+ }
21
+ export declare function policyFromRecall(rules: RecalledRule[]): RailPolicy;
22
+ export declare function decideRailFromRecall(rules: RecalledRule[], requested: "shielded" | "x402"): "shielded" | "x402";
@@ -0,0 +1,49 @@
1
+ import { createHash } from "node:crypto";
2
+ import { fail } from "./errors.js";
3
+ import { selectRail, validateMemoryFact } from "./memory.js";
4
+ export class WorkflowMemory {
5
+ store;
6
+ memory;
7
+ constructor(store, memory) {
8
+ this.store = store;
9
+ this.memory = memory;
10
+ }
11
+ async record(fact) {
12
+ validateMemoryFact(fact);
13
+ const factHash = createHash("sha256").update(JSON.stringify({ kind: fact.kind, subject: fact.subject, observation: fact.observation, operationId: fact.operationId, createdAt: fact.createdAt })).digest("hex");
14
+ const existing = this.store.db.prepare("SELECT fact_hash, blob_id, status FROM memory_jobs WHERE operation_id = ?").get(fact.operationId);
15
+ if (existing && existing.fact_hash !== factHash)
16
+ fail("MEMORY_OPERATION_CONFLICT", "memory operation ID already identifies another fact");
17
+ if (existing?.status === "complete" && existing.blob_id)
18
+ return existing.blob_id;
19
+ if (existing)
20
+ fail(existing.status === "unresolved" ? "MEMORY_JOB_UNRESOLVED" : "MEMORY_JOB_PENDING", "memory write cannot be repeated without reconciling its prior job");
21
+ const inserted = this.store.db.prepare("INSERT OR IGNORE INTO memory_jobs(operation_id, kind, fact_hash, blob_id, status, created_at) VALUES (?, ?, ?, NULL, 'pending', ?)").run(fact.operationId, fact.kind, factHash, Date.now());
22
+ if (inserted.changes !== 1)
23
+ fail("MEMORY_JOB_PENDING", "another process started this memory write");
24
+ try {
25
+ const blobId = await this.memory.remember(fact);
26
+ if (!blobId)
27
+ fail("MEMORY_BLOB_MISSING", "completed memory write returned no blob ID");
28
+ this.store.db.prepare("UPDATE memory_jobs SET blob_id = ?, status = 'complete' WHERE operation_id = ? AND status = 'pending'").run(blobId, fact.operationId);
29
+ return blobId;
30
+ }
31
+ catch (error) {
32
+ this.store.db.prepare("UPDATE memory_jobs SET status = 'unresolved' WHERE operation_id = ? AND status = 'pending'").run(fact.operationId);
33
+ throw error;
34
+ }
35
+ }
36
+ completed() {
37
+ return this.store.db.prepare("SELECT operation_id, blob_id FROM memory_jobs WHERE status = 'complete' ORDER BY created_at").all()
38
+ .map((row) => { const value = row; return { operationId: value.operation_id, blobId: value.blob_id }; });
39
+ }
40
+ }
41
+ export function policyFromRecall(rules) {
42
+ if (!rules.length)
43
+ return { hiddenAmountRequired: false, recalledRulesAvailable: false, contradictory: false };
44
+ const privacy = rules.map((rule) => rule.hiddenAmountRequired).filter((value) => value !== undefined);
45
+ return { hiddenAmountRequired: privacy.includes(true), recalledRulesAvailable: true, contradictory: privacy.includes(true) && privacy.includes(false) };
46
+ }
47
+ export function decideRailFromRecall(rules, requested) {
48
+ return selectRail(policyFromRecall(rules), requested);
49
+ }
@@ -0,0 +1,36 @@
1
+ import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
2
+ import type { RoleStore } from "./store.js";
3
+ export interface X402ResourceServiceConfig {
4
+ origin: string;
5
+ pathPrefix?: string;
6
+ facilitatorUrl: string;
7
+ /** Owner-only credential for the facilitator. Never put this in a manifest or buyer profile. */
8
+ facilitatorKey: string;
9
+ network: "sui:testnet";
10
+ asset: string;
11
+ payTo: string;
12
+ amount: string;
13
+ maxTimeoutSeconds?: number;
14
+ /** Bounded retry schedule for transient facilitator cold starts. */
15
+ facilitatorRetryDelaysMs?: number[];
16
+ }
17
+ /** Seller-owned standard x402 v2 exact resource with additional request-binding replay protection. */
18
+ export declare class X402ResourceService {
19
+ #private;
20
+ readonly config: X402ResourceServiceConfig;
21
+ private readonly http;
22
+ private readonly persist?;
23
+ private readonly claimExternal?;
24
+ constructor(store: RoleStore, config: X402ResourceServiceConfig, http?: typeof fetch, persist?: (() => Promise<void>) | undefined, claimExternal?: ((digest: string, requestId: string) => Promise<void>) | undefined);
25
+ register(requestId: string, operationId: string, body: Uint8Array): {
26
+ url: string;
27
+ artifactSha256: string;
28
+ };
29
+ private requirements;
30
+ private resourceUrl;
31
+ private challenge;
32
+ handle(request: IncomingMessage, response: ServerResponse): Promise<void>;
33
+ createServer(): Server;
34
+ private facilitator;
35
+ private send;
36
+ }