import { createHash } from "node:crypto"; import { VaultfireError } from "../core/errors.js"; import type { Hex, Hex20, JsonValue } from "../core/types.js"; import { asHex20, assertJson, deepFreeze, toSafeUint } from "../core/validate.js"; import type { V3Client } from "../v3/client.js"; export type ProtocolModule = | "identity-kya" | "mandates" | "evidence-consensus" | "accountability-bonds" | "partnership-bonds" | "task-milestone-escrow" | "mutual-aid" | "coverage" | "vault" | "flourishing-rewards" | "governance" | "recovery"; export type PreparedAction = Readonly<{ schema: "vaultfire.sdk.prepared-action.v1"; module: ProtocolModule; action: string; input: Readonly>; actionDigest: `0x${string}`; signing: "caller-required"; broadcasting: "not-supported"; deployment: "independently-verified-deployment-required"; }>; function canonical(value: JsonValue): string { if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key] as JsonValue)}`).join(",")}}`; return JSON.stringify(value); } function required(value: string, name: string): string { if (!value || value.length > 256 || /[\u0000-\u001f]/.test(value)) throw new VaultfireError("INVALID_ARGUMENT", `${name} is required and must be a bounded printable string`); return value; } function positiveInteger(value: number, name: string): number { if (!Number.isSafeInteger(value) || value <= 0) throw new VaultfireError("INVALID_ARGUMENT", `${name} must be a positive safe integer`); return value; } function nonnegativeInteger(value: number, name: string): number { if (!Number.isSafeInteger(value) || value < 0) throw new VaultfireError("INVALID_ARGUMENT", `${name} must be a nonnegative safe integer`); return value; } function prepare(module: ProtocolModule, action: string, input: Readonly>): PreparedAction { assertJson(input, "input"); const body = { schema: "vaultfire.sdk.prepared-action.v1" as const, module, action: required(action, "action"), input: { ...input } }; const actionDigest = `0x${createHash("sha256").update(canonical(body)).digest("hex")}` as const; return deepFreeze({ ...body, actionDigest, signing: "caller-required", broadcasting: "not-supported", deployment: "independently-verified-deployment-required" }); } export class IdentityKyaClient { registerIdentity(input: { identityId: string; controller: string; metadataDigest: string }): PreparedAction { return prepare("identity-kya", "register-erc8004-identity", { identityId: required(input.identityId,"identityId"), controller: required(input.controller,"controller"), metadataDigest: required(input.metadataDigest,"metadataDigest") }); } updateIdentity(input: { identityId: string; metadataDigest: string }): PreparedAction { return prepare("identity-kya", "update-erc8004-identity", { identityId: required(input.identityId,"identityId"), metadataDigest: required(input.metadataDigest,"metadataDigest") }); } revokeIdentity(identityId: string): PreparedAction { return prepare("identity-kya", "revoke-erc8004-identity", { identityId: required(identityId,"identityId") }); } issueKya(input: { identityId: string; issuer: string; claimDigest: string; expiresAt: number }): PreparedAction { return prepare("identity-kya", "issue-kya", { identityId: required(input.identityId,"identityId"), issuer: required(input.issuer,"issuer"), claimDigest: required(input.claimDigest,"claimDigest"), expiresAt: positiveInteger(input.expiresAt,"expiresAt") }); } revokeKya(identityId: string, claimDigest: string): PreparedAction { return prepare("identity-kya", "revoke-kya", { identityId: required(identityId,"identityId"), claimDigest: required(claimDigest,"claimDigest") }); } } export class MandateClient { create(input: { mandateId: string; principal: string; delegate: string; scopeDigest: string; notBefore: number; notAfter: number; uses: number }): PreparedAction { if (!Number.isSafeInteger(input.notBefore) || input.notBefore < 0 || !Number.isSafeInteger(input.notAfter) || input.notAfter <= input.notBefore) throw new VaultfireError("INVALID_ARGUMENT", "mandate validity window is invalid"); return prepare("mandates", "create", { mandateId: required(input.mandateId,"mandateId"), principal: required(input.principal,"principal"), delegate: required(input.delegate,"delegate"), scopeDigest: required(input.scopeDigest,"scopeDigest"), notBefore: input.notBefore, notAfter: input.notAfter, uses: positiveInteger(input.uses,"uses") }); } consume(mandateId: string, operationDigest: string): PreparedAction { return prepare("mandates", "consume", { mandateId: required(mandateId,"mandateId"), operationDigest: required(operationDigest,"operationDigest") }); } revoke(mandateId: string): PreparedAction { return prepare("mandates", "revoke", { mandateId: required(mandateId,"mandateId") }); } } export class EvidenceConsensusClient { submitEvidence(input: { evidenceId: string; subject: string; payloadDigest: string; provenanceDigest: string }): PreparedAction { return prepare("evidence-consensus", "submit-evidence", { evidenceId: required(input.evidenceId,"evidenceId"), subject: required(input.subject,"subject"), payloadDigest: required(input.payloadDigest,"payloadDigest"), provenanceDigest: required(input.provenanceDigest,"provenanceDigest") }); } attest(input: { proposalId: string; evidenceId: string; member: string; approve: boolean }): PreparedAction { return prepare("evidence-consensus", "attest", { proposalId: required(input.proposalId,"proposalId"), evidenceId: required(input.evidenceId,"evidenceId"), member: required(input.member,"member"), approve: input.approve }); } finalize(input: { proposalId: string; approvals: number; members: number; threshold: number }): PreparedAction { positiveInteger(input.members,"members"); positiveInteger(input.threshold,"threshold"); if (!Number.isSafeInteger(input.approvals) || input.approvals < 0 || input.approvals > input.members || input.threshold > input.members) throw new VaultfireError("INVALID_ARGUMENT", "invalid threshold consensus counts"); return prepare("evidence-consensus", "finalize", { proposalId: required(input.proposalId,"proposalId"), approvals: input.approvals, members: input.members, threshold: input.threshold, accepted: input.approvals >= input.threshold }); } } export type BondKind = "accountability" | "partnership"; export type BondBaseLifecycleAction = | "fund-native" | "activate" | "cancel-before-activation" | "expire-unfunded" | "expire-unactivated" | "mark-expired" | "force-exit"; export type UnsignedBondBaseTransaction = Readonly<{ kind: "vaultfire-v3-unsigned-bond-base-transaction"; bondKind: BondKind; chainId: 8453n | 84532n; to: Hex20; action: BondBaseLifecycleAction; bondId: bigint; data: Hex; value: bigint; signing: "caller-required"; broadcasting: "not-supported"; deployment: "source-review-only-target-must-be-independently-verified"; }>; const BOND_LIFECYCLE_SELECTORS: Readonly> = Object.freeze({ "fund-native": "5a4c6373", activate: "b260c42a", "cancel-before-activation": "fb208250", "expire-unfunded": "c8c9a9cc", "expire-unactivated": "42c82b25", "mark-expired": "77fd00c5", "force-exit": "fd12a4d2" }); export class BondLifecycleClient { constructor(readonly kind: BondKind) {} private action(action: string, input: Readonly>): PreparedAction { return prepare(this.kind === "accountability" ? "accountability-bonds" : "partnership-bonds", action, input); } prepareBaseLifecycleTransaction(input: { chainId: bigint; target: string; action: BondBaseLifecycleAction; bondId: bigint; valueWei?: bigint; }): UnsignedBondBaseTransaction { if (input.chainId !== 8453n && input.chainId !== 84532n) { throw new VaultfireError("INVALID_ARGUMENT", "bond transaction preview supports Base or Base Sepolia only"); } const to = asHex20(input.target, "target"); const bondId = toSafeUint(input.bondId, 256, "bondId"); const value = input.valueWei ?? 0n; toSafeUint(value, 256, "valueWei"); if (input.action === "fund-native" ? value === 0n : value !== 0n) { throw new VaultfireError( "INVALID_ARGUMENT", input.action === "fund-native" ? "fund-native requires a positive valueWei" : `${input.action} must not transfer value` ); } const selector = BOND_LIFECYCLE_SELECTORS[input.action]; if (selector === undefined) throw new VaultfireError("UNSUPPORTED_OPERATION", "unsupported bond lifecycle action"); const encodedBondId = bondId.toString(16).padStart(64, "0"); return deepFreeze({ kind: "vaultfire-v3-unsigned-bond-base-transaction", bondKind: this.kind, chainId: input.chainId, to, action: input.action, bondId, data: `0x${selector}${encodedBondId}`, value, signing: "caller-required", broadcasting: "not-supported", deployment: "source-review-only-target-must-be-independently-verified" }); } create(input: { bondId: string; parties: readonly string[]; principalUnits: number; expiresAt: number }): PreparedAction { if (input.parties.length < 2 || new Set(input.parties).size !== input.parties.length) throw new VaultfireError("INVALID_ARGUMENT", "bond requires distinct parties"); return this.action("create", { bondId: required(input.bondId,"bondId"), parties: input.parties.map((x)=>required(x,"party")), principalUnits: positiveInteger(input.principalUnits,"principalUnits"), expiresAt: positiveInteger(input.expiresAt,"expiresAt") }); } fund(bondId: string, party: string, units: number): PreparedAction { return this.action("fund", { bondId: required(bondId,"bondId"), party: required(party,"party"), units: positiveInteger(units,"units") }); } activate(bondId: string): PreparedAction { return this.action("activate", { bondId: required(bondId,"bondId") }); } settle(bondId: string, evidenceId: string): PreparedAction { return this.action("settle", { bondId: required(bondId,"bondId"), evidenceId: required(evidenceId,"evidenceId") }); } dispute(bondId: string, evidenceId: string): PreparedAction { return this.action("dispute", { bondId: required(bondId,"bondId"), evidenceId: required(evidenceId,"evidenceId") }); } expire(bondId: string): PreparedAction { return this.action("expire", { bondId: required(bondId,"bondId") }); } forceExit(bondId: string, reasonDigest: string): PreparedAction { return this.action("force-exit", { bondId: required(bondId,"bondId"), reasonDigest: required(reasonDigest,"reasonDigest") }); } recover(bondId: string): PreparedAction { return this.action("terminal-recovery", { bondId: required(bondId,"bondId") }); } } export type TaskMilestoneKind = "deliverable" | "service" | "outcome"; export type TypedTaskMilestone = Readonly<{ kind: TaskMilestoneKind; specificationHash: string; principalUnits: number; submissionPeriodSeconds: number; reviewPeriodSeconds: number; disputePeriodSeconds: number; }>; export type TypedTaskInput = Readonly<{ taskId: string; worker: string; criteriaRoot: string; acceptDeadline: number; milestones: readonly TypedTaskMilestone[]; adjudicators: readonly string[]; adjudicatorThreshold: number; fallbackWorkerBps: number; }>; export class EconomicModulesClient { task = Object.freeze({ openTyped: (input: TypedTaskInput) => { if (input.milestones.length === 0 || input.milestones.length > 64) throw new VaultfireError("INVALID_ARGUMENT", "typed task requires 1 to 64 milestones"); if (input.adjudicators.length === 0 || input.adjudicators.length > 16 || new Set(input.adjudicators).size !== input.adjudicators.length) throw new VaultfireError("INVALID_ARGUMENT", "typed task requires 1 to 16 distinct adjudicators"); const threshold = positiveInteger(input.adjudicatorThreshold, "adjudicatorThreshold"); if (threshold > input.adjudicators.length) throw new VaultfireError("INVALID_ARGUMENT", "adjudicatorThreshold exceeds the adjudicator count"); const fallbackWorkerBps = nonnegativeInteger(input.fallbackWorkerBps, "fallbackWorkerBps"); if (fallbackWorkerBps > 10_000) throw new VaultfireError("INVALID_ARGUMENT", "fallbackWorkerBps exceeds 10000"); const milestones = input.milestones.map((milestone) => ({ kind: milestone.kind, specificationHash: required(milestone.specificationHash, "specificationHash"), principalUnits: positiveInteger(milestone.principalUnits, "principalUnits"), submissionPeriodSeconds: positiveInteger(milestone.submissionPeriodSeconds, "submissionPeriodSeconds"), reviewPeriodSeconds: positiveInteger(milestone.reviewPeriodSeconds, "reviewPeriodSeconds"), disputePeriodSeconds: positiveInteger(milestone.disputePeriodSeconds, "disputePeriodSeconds") })); return prepare("task-milestone-escrow", "open-typed-task", { taskId: required(input.taskId, "taskId"), worker: required(input.worker, "worker"), criteriaRoot: required(input.criteriaRoot, "criteriaRoot"), acceptDeadline: positiveInteger(input.acceptDeadline, "acceptDeadline"), milestones, adjudicators: input.adjudicators.map((value) => required(value, "adjudicator")), adjudicatorThreshold: threshold, fallbackWorkerBps, principalUnits: milestones.reduce((sum, milestone) => sum + milestone.principalUnits, 0) }); }, accept: (taskId: string) => prepare("task-milestone-escrow", "accept-task", { taskId: required(taskId, "taskId") }), submitMilestone: (taskId: string, submissionCommitment: string, evidenceSchema: string) => prepare("task-milestone-escrow", "submit-typed-milestone", { taskId: required(taskId, "taskId"), submissionCommitment: required(submissionCommitment, "submissionCommitment"), evidenceSchema: required(evidenceSchema, "evidenceSchema") }), approveMilestone: (taskId: string) => prepare("task-milestone-escrow", "approve-milestone", { taskId: required(taskId, "taskId") }), rejectMilestone: (taskId: string, reasonHash: string) => prepare("task-milestone-escrow", "reject-milestone", { taskId: required(taskId, "taskId"), reasonHash: required(reasonHash, "reasonHash") }), openDispute: (taskId: string, reasonHash: string) => prepare("task-milestone-escrow", "open-dispute", { taskId: required(taskId, "taskId"), reasonHash: required(reasonHash, "reasonHash") }), voteDispute: (taskId: string, ruling: "creator" | "worker") => prepare("task-milestone-escrow", "vote-dispute", { taskId: required(taskId, "taskId"), ruling }), forceResolveDispute: (taskId: string) => prepare("task-milestone-escrow", "force-resolve-dispute", { taskId: required(taskId, "taskId") }), forceExit: (taskId: string) => prepare("task-milestone-escrow", "force-exit", { taskId: required(taskId, "taskId") }), defaultApprove: (taskId: string) => prepare("task-milestone-escrow", "default-approve", { taskId: required(taskId, "taskId") }), recoverTimedOut: (taskId: string) => prepare("task-milestone-escrow", "recover-timed-out", { taskId: required(taskId, "taskId") }), cancelOffer: (taskId: string) => prepare("task-milestone-escrow", "cancel-offer", { taskId: required(taskId, "taskId") }), requestMutualCancellation: (taskId: string) => prepare("task-milestone-escrow", "request-mutual-cancellation", { taskId: required(taskId, "taskId") }), create: (_taskId: string, _principalUnits: number): never => { throw new VaultfireError("RETIRED", "Untyped task creation is disabled; use task.openTyped with bounded milestone and adjudication terms."); }, fundMilestone: (_taskId: string, _milestone: number, _units: number): never => { throw new VaultfireError("RETIRED", "Separate milestone funding is not part of the reviewed typed escrow flow; fund the exact aggregate principal when opening the typed task."); }, settleMilestone: (_taskId: string, _milestone: number, _evidenceId: string): never => { throw new VaultfireError("RETIRED", "Generic settlement is disabled; use submitMilestone followed by approveMilestone or the bounded dispute flow."); }, refund: (_taskId: string, _reason: string): never => { throw new VaultfireError("RETIRED", "Generic refunds are disabled; use cancelOffer, requestMutualCancellation, recoverTimedOut, or forceExit as applicable."); } }); mutualAid = Object.freeze({ create: (aidId: string, reserveUnits: number) => prepare("mutual-aid", "create", { aidId: required(aidId,"aidId"), reserveUnits: positiveInteger(reserveUnits,"reserveUnits") }), claim: (aidId: string, evidenceId: string) => prepare("mutual-aid", "claim", { aidId: required(aidId,"aidId"), evidenceId: required(evidenceId,"evidenceId") }), recover: (aidId: string) => prepare("mutual-aid", "recover", { aidId: required(aidId,"aidId") }) }); coverage = Object.freeze({ create: (coverId: string, reserveUnits: number, triggerDigest: string) => prepare("coverage", "create", { coverId: required(coverId,"coverId"), reserveUnits: positiveInteger(reserveUnits,"reserveUnits"), triggerDigest: required(triggerDigest,"triggerDigest") }), claim: (coverId: string, evidenceId: string) => prepare("coverage", "claim", { coverId: required(coverId,"coverId"), evidenceId: required(evidenceId,"evidenceId") }), recover: (coverId: string) => prepare("coverage", "recover", { coverId: required(coverId,"coverId") }) }); vault = Object.freeze({ deposit: (vaultId: string, principalUnits: number) => prepare("vault", "deposit", { vaultId: required(vaultId,"vaultId"), principalUnits: positiveInteger(principalUnits,"principalUnits") }), withdraw: (vaultId: string, principalUnits: number) => prepare("vault", "withdraw", { vaultId: required(vaultId,"vaultId"), principalUnits: positiveInteger(principalUnits,"principalUnits") }) }); rewardSplit(input: { rewardId: string; principalUnits: number; rewardUnits: number; recipient: string }): PreparedAction { return prepare("flourishing-rewards", "record-principal-separated-reward", { rewardId: required(input.rewardId,"rewardId"), principalUnits: positiveInteger(input.principalUnits,"principalUnits"), rewardUnits: positiveInteger(input.rewardUnits,"rewardUnits"), recipient: required(input.recipient,"recipient"), principalMayFundReward: false }); } } export class StatusDiscoveryClient { constructor(private readonly client: V3Client) {} protocol(signal?: AbortSignal) { return this.client.request("route.v3.status", signal === undefined ? {} : { signal }); } contract(name: string, signal?: AbortSignal) { return this.client.request("route.v3.contract", { params: { name: required(name,"name") }, ...(signal === undefined ? {} : { signal }) }); } evidence(evidenceHash: string, signal?: AbortSignal) { return this.client.request("route.v3.evidence", { params: { evidenceHash: required(evidenceHash,"evidenceHash") }, ...(signal === undefined ? {} : { signal }) }); } dispute(disputeId: string, signal?: AbortSignal) { return this.client.request("route.v3.dispute", { params: { disputeId: required(disputeId,"disputeId") }, ...(signal === undefined ? {} : { signal }) }); } } export class RecoveryClient { guidance(code: string): Readonly<{ retry: boolean; action: string }> { const table: Record = { TRANSPORT_ERROR: { retry: true, action: "Retry bounded reads only; reconcile externally broadcast writes before any retry." }, NOT_CONFIGURED: { retry: false, action: "Wait for attributable deployment or service evidence; do not use V2 as fallback." }, VERIFICATION_FAILED: { retry: false, action: "Fail closed and investigate the exact digest, owner, policy, and finality evidence." }, PAYMENTS_DISABLED: { retry: false, action: "Stop; V3 has no authorized payment or settlement path." }, RETIRED: { retry: false, action: "Use the named safe successor; never call the retired write surface." } }; return Object.freeze(table[code] ?? { retry: false, action: "Fail closed and inspect the typed VaultfireError before changing state." }); } } export const retiredApiSuccessors = deepFreeze({ TaskEscrowV3: { status: "retired", successor: "EconomicModulesClient.task.openTyped", rationale: "Typed milestone terms, evidence schemas, bounded disputes, and explicit timeout recovery replace the retired monolithic and untyped task flows." }, MutualAidPoolV3: { status: "retired", successor: "EconomicModulesClient.mutualAid", rationale: "VoluntaryMutualAidV3 uses explicit reserve, claim, and recovery actions." }, "route.v3.retired-partnership-write": { status: "retired", successor: "BondLifecycleClient('partnership')", rationale: "Prepare caller-signed actions only; no paid or privileged public write route." } }); export class VaultfireV3ProtocolClients { readonly identity = new IdentityKyaClient(); readonly mandates = new MandateClient(); readonly evidenceConsensus = new EvidenceConsensusClient(); readonly accountabilityBonds = new BondLifecycleClient("accountability"); readonly partnershipBonds = new BondLifecycleClient("partnership"); readonly economic = new EconomicModulesClient(); readonly recovery = new RecoveryClient(); readonly status: StatusDiscoveryClient; constructor(client: V3Client) { this.status = new StatusDiscoveryClient(client); } }