feat: rename dashboard to Capacity Markets with Vector sale proofs

This commit is contained in:
david
2026-08-28 11:59:24 +02:00
parent af405cf378
commit 59cac438f2
11 changed files with 2254 additions and 152 deletions

View File

@@ -4,7 +4,7 @@
* Routes (all gated by <RequireAuth>): * Routes (all gated by <RequireAuth>):
* / → <Dashboard /> * / → <Dashboard />
* /tasks → <TaskHistory /> * /tasks → <TaskHistory />
* /dashboard → <ResaleDashboard /> unified Surplus + Vector metrics * /dashboard → <ResaleDashboard /> Capacity markets and sale proofs
* /pending → <PendingReceipts /> UX-1: buyer-side Accept button * /pending → <PendingReceipts /> UX-1: buyer-side Accept button
* /wallet → <Wallet /> * /wallet → <Wallet />
* /api-keys → <ApiKeys /> self-serve gateway API key generation * /api-keys → <ApiKeys /> self-serve gateway API key generation
@@ -39,7 +39,7 @@ export default function App() {
<div className="min-h-screen bg-gray-50 text-gray-900"> <div className="min-h-screen bg-gray-50 text-gray-900">
<nav className="bg-white border-b border-gray-200 px-4 py-3 sm:px-6 flex flex-wrap items-center gap-x-6 gap-y-2"> <nav className="bg-white border-b border-gray-200 px-4 py-3 sm:px-6 flex flex-wrap items-center gap-x-6 gap-y-2">
<Link to="/" className="font-medium hover:underline">Dashboard</Link> <Link to="/" className="font-medium hover:underline">Dashboard</Link>
<Link to="/dashboard" className="font-medium hover:underline">Resale</Link> <Link to="/dashboard" className="font-medium hover:underline">Capacity</Link>
{/* Book Summarizer tab hidden for now — the /book route and all PDF {/* Book Summarizer tab hidden for now — the /book route and all PDF
summarize code remain intact; restore by re-adding this link: summarize code remain intact; restore by re-adding this link:
<Link to="/book" className="font-medium hover:underline">Book Summarizer</Link> */} <Link to="/book" className="font-medium hover:underline">Book Summarizer</Link> */}

View File

@@ -1,5 +1,23 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
interface VectorProof {
status: "pending" | "confirmed" | "failed";
saleHash: string | null;
batchRoot: string | null;
txHash: string | null;
leafIndex: number | null;
siblings: string[];
}
const PENDING_PROOF: VectorProof = {
status: "pending",
saleHash: null,
batchRoot: null,
txHash: null,
leafIndex: null,
siblings: [],
};
interface DashboardSnapshot { interface DashboardSnapshot {
generatedAt: string; generatedAt: string;
controller: { controller: {
@@ -60,20 +78,10 @@ interface DashboardSnapshot {
outputTokens: number; outputTokens: number;
cacheReadTokens: number; cacheReadTokens: number;
revenueUsd: string; revenueUsd: string;
transactionHash: string | null; vectorProof: VectorProof;
}>; }>;
}; };
vector: {
status: "retired";
model: string;
supplierWallet: string;
advertRef: string;
retirementTransaction: string;
retiredOn: string;
historicalAp3xEarned: string;
historicalSettledJobs: number;
historicalUpstreamSpendUsd: string;
};
} }
function usd(value: string | number | null): string { function usd(value: string | number | null): string {
@@ -110,6 +118,34 @@ function StatusPill({ label, active }: { label: string; active: boolean }) {
); );
} }
function VectorProofState({ proof }: { proof: VectorProof }) {
const pillClass = proof.status === "confirmed"
? "bg-green-100 text-green-700"
: proof.status === "pending"
? "bg-amber-100 text-amber-700"
: "bg-red-100 text-red-700";
const pill = (
<span className={`rounded px-2 py-0.5 text-xs font-medium ${pillClass}`}>
{proof.status}
</span>
);
if (proof.status !== "confirmed" || proof.txHash === null) return pill;
return (
<a
className="inline-flex items-center gap-2 rounded text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2"
href={`https://vector.apexscan.org/en/transaction/${proof.txHash}/summary/`}
target="_blank"
rel="noopener noreferrer"
aria-label={`Confirmed Vector proof transaction ${proof.txHash}`}
>
{pill}
<span className="font-mono text-xs">{compact(proof.txHash)}</span>
</a>
);
}
function Metric({ label, value, detail }: { label: string; value: string; detail: string }) { function Metric({ label, value, detail }: { label: string; value: string; detail: string }) {
return ( return (
<div className="rounded border border-gray-200 bg-gray-50 p-3"> <div className="rounded border border-gray-200 bg-gray-50 p-3">
@@ -170,7 +206,7 @@ export default function ResaleDashboard() {
}, [refresh]); }, [refresh]);
if (loading && data === null) { if (loading && data === null) {
return <p className="text-sm text-gray-500">Loading resale dashboard…</p>; return <p className="text-sm text-gray-500">Loading capacity markets…</p>;
} }
if (data === null) { if (data === null) {
@@ -183,18 +219,20 @@ export default function ResaleDashboard() {
const offer = data.surplus.offer; const offer = data.surplus.offer;
const earnings = data.surplus.earnings; const earnings = data.surplus.earnings;
const vector = data.vector;
const offerLive = offer.status === "active" && offer.available && offer.healthy; const offerLive = offer.status === "active" && offer.available && offer.healthy;
const advertHash = vector.advertRef.split("#")[0] ?? ""; const proofCounts = { confirmed: 0, pending: 0, failed: 0 };
for (const sale of data.surplus.recentSales) {
proofCounts[(sale.vectorProof ?? PENDING_PROOF).status] += 1;
}
return ( return (
<div className="mx-auto max-w-7xl space-y-6" data-testid="resale-dashboard"> <div className="mx-auto max-w-7xl space-y-6" data-testid="resale-dashboard">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<h1 className="text-2xl font-semibold">Resale Dashboard</h1> <h1 className="text-2xl font-semibold">Capacity Markets</h1>
<p className="max-w-3xl text-sm text-gray-600"> <p className="max-w-3xl text-sm text-gray-600">
One OpenRouter capacity pool managed by the seller agent. Surplus is the live Base USDC Surplus turns available OpenRouter capacity into Base USDC sales. Vector anchors
market. Vector preserves the completed AP3X escrow proof. Merkle-batched proofs so every sale has a verifiable transaction record.
</p> </p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -223,8 +261,10 @@ export default function ResaleDashboard() {
<section className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <section className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<div> <div>
<h2 className="font-semibold">Capacity route</h2> <h2 className="font-semibold">Capacity and proof route</h2>
<p className="text-sm text-gray-500">The same upstream capacity moved from Vector proof to Surplus production.</p> <p className="text-sm text-gray-500">
Capacity sells through Surplus, and each sale joins a shared Vector anchor.
</p>
</div> </div>
<span className="text-xs text-gray-400">Updated {timestamp(data.generatedAt)}</span> <span className="text-xs text-gray-400">Updated {timestamp(data.generatedAt)}</span>
</div> </div>
@@ -244,86 +284,47 @@ export default function ResaleDashboard() {
/> />
<span className="self-center text-lg text-gray-400">→</span> <span className="self-center text-lg text-gray-400">→</span>
<FlowNode <FlowNode
label="Live settlement" label="Live market"
title="Surplus / Base USDC" title="Surplus / Base USDC"
detail={`${offerLive ? "Available" : "Unavailable"} · ${offer.trusted ? "trusted" : "untrusted"}`} detail={`${offerLive ? "Available" : "Unavailable"} · ${offer.trusted ? "trusted" : "untrusted"}`}
mono={offer.id} mono={offer.id}
/> />
<span className="self-center text-lg text-gray-400">→</span>
<FlowNode
label="Proof layer"
title="Vector anchors"
detail="Commits each sale through a shared Merkle-batch transaction"
/>
</div> </div>
</section> </section>
<section className="grid gap-6 xl:grid-cols-2"> <section className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<article className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="flex items-start justify-between gap-3">
<div className="flex items-start justify-between gap-3"> <div>
<div> <p className="text-xs font-medium uppercase tracking-wide text-gray-500">Current market</p>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">Current market</p> <h2 className="mt-1 text-lg font-semibold">Surplus Intelligence</h2>
<h2 className="mt-1 text-lg font-semibold">Surplus Intelligence</h2> <p className="mt-1 text-sm text-gray-600">Discounted inference with USDC settlement on Base.</p>
<p className="mt-1 text-sm text-gray-600">Discounted inference with USDC settlement on Base.</p>
</div>
<StatusPill label={offer.status} active={offerLive} />
</div> </div>
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-2 2xl:grid-cols-4"> <StatusPill label={offer.status} active={offerLive} />
<Metric label="Lifetime revenue" value={usd(earnings.totalUsd)} detail={`${usd(earnings.pendingUsd)} pending`} /> </div>
<Metric label="Requests sold" value={count(earnings.requests)} detail={`${count(earnings.tokens)} tokens`} /> <div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Metric label="Market rank" value={offer.rank === null ? "—" : `#${offer.rank}`} detail={`${offer.trades24h} trades / 24h`} /> <Metric label="Lifetime revenue" value={usd(earnings.totalUsd)} detail={`${usd(earnings.pendingUsd)} pending`} />
<Metric label="Daily seller cap" value={usd(offer.dailyCapUsd)} detail={`${usd(offer.capRemainingUsd)} remaining`} /> <Metric label="Requests sold" value={count(earnings.requests)} detail={`${count(earnings.tokens)} tokens`} />
<Metric label="Market rank" value={offer.rank === null ? "—" : `#${offer.rank}`} detail={`${offer.trades24h} trades / 24h`} />
<Metric label="Daily seller cap" value={usd(offer.dailyCapUsd)} detail={`${usd(offer.capRemainingUsd)} remaining`} />
</div>
<dl className="mt-4 grid gap-x-6 gap-y-2 border-t border-gray-100 pt-4 text-sm sm:grid-cols-2">
<div><dt className="text-gray-500">Marketplace model</dt><dd className="font-mono text-xs text-gray-900">{offer.model ?? "—"}</dd></div>
<div><dt className="text-gray-500">Provider model</dt><dd className="font-mono text-xs text-gray-900">{offer.providerModel ?? "—"}</dd></div>
<div><dt className="text-gray-500">Input / 1M</dt><dd>{usd(offer.inputUsdPer1m)}</dd></div>
<div><dt className="text-gray-500">Output / 1M</dt><dd>{usd(offer.outputUsdPer1m)}</dd></div>
</dl>
{earnings.payoutHoldReason && (
<div className="mt-4 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Payout hold: {earnings.payoutHoldReason}
{earnings.payoutHoldReleasesAt && ` · releases ${timestamp(earnings.payoutHoldReleasesAt)}`}
</div> </div>
<dl className="mt-4 grid gap-x-6 gap-y-2 border-t border-gray-100 pt-4 text-sm sm:grid-cols-2"> )}
<div><dt className="text-gray-500">Marketplace model</dt><dd className="font-mono text-xs text-gray-900">{offer.model ?? "—"}</dd></div>
<div><dt className="text-gray-500">Provider model</dt><dd className="font-mono text-xs text-gray-900">{offer.providerModel ?? "—"}</dd></div>
<div><dt className="text-gray-500">Input / 1M</dt><dd>{usd(offer.inputUsdPer1m)}</dd></div>
<div><dt className="text-gray-500">Output / 1M</dt><dd>{usd(offer.outputUsdPer1m)}</dd></div>
</dl>
{earnings.payoutHoldReason && (
<div className="mt-4 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Payout hold: {earnings.payoutHoldReason}
{earnings.payoutHoldReleasesAt && ` · releases ${timestamp(earnings.payoutHoldReleasesAt)}`}
</div>
)}
</article>
<article className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">Migration record</p>
<h2 className="mt-1 text-lg font-semibold">Vector marketplace</h2>
<p className="mt-1 text-sm text-gray-600">AP3X escrow route retired after the cross-market proof.</p>
</div>
<StatusPill label="retired" active={false} />
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-2 2xl:grid-cols-4">
<Metric label="AP3X earned" value={`${vector.historicalAp3xEarned} AP3X`} detail="historical" />
<Metric label="Settled jobs" value={count(vector.historicalSettledJobs)} detail="Vector escrows" />
<Metric label="Upstream spend" value={usd(vector.historicalUpstreamSpendUsd)} detail="proof route" />
<Metric label="Retired on" value={vector.retiredOn} detail="advert closed" />
</div>
<div className="mt-4 space-y-3 border-t border-gray-100 pt-4 text-sm">
<div>
<p className="text-gray-500">Retired advert</p>
<p className="break-all font-mono text-xs text-gray-900">{vector.advertRef}</p>
<a
className="text-xs text-blue-600 hover:underline"
href={`https://vector.apexscan.org/en/transaction/${advertHash}/summary/`}
target="_blank"
rel="noopener noreferrer"
>
Open advert transaction ↗
</a>
</div>
<div>
<p className="text-gray-500">Retirement proof</p>
<p className="break-all font-mono text-xs text-gray-900">{vector.retirementTransaction}</p>
<a
className="text-xs text-blue-600 hover:underline"
href={`https://vector.apexscan.org/en/transaction/${vector.retirementTransaction}/summary/`}
target="_blank"
rel="noopener noreferrer"
>
Open confirmed transaction ↗
</a>
</div>
</div>
</article>
</section> </section>
<section className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <section className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
@@ -350,8 +351,24 @@ export default function ResaleDashboard() {
</div> </div>
<p className="text-sm text-gray-500">{count(earnings.requests)} lifetime requests · {count(earnings.tokens)} tokens</p> <p className="text-sm text-gray-500">{count(earnings.requests)} lifetime requests · {count(earnings.tokens)} tokens</p>
</div> </div>
<div
className="flex flex-wrap gap-2"
role="group"
aria-label="Vector proof summary"
>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-700">
Confirmed {count(proofCounts.confirmed)}
</span>
<span className="rounded bg-amber-100 px-2 py-1 text-xs font-medium text-amber-700">
Pending {count(proofCounts.pending)}
</span>
<span className="rounded bg-red-100 px-2 py-1 text-xs font-medium text-red-700">
Failed {count(proofCounts.failed)}
</span>
</div>
<div className="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm"> <div className="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
<table className="min-w-full divide-y divide-gray-200 text-sm"> <table className="min-w-full divide-y divide-gray-200 text-sm">
<caption className="sr-only">Recent Surplus sales with Vector proof status</caption>
<thead className="bg-gray-50 text-left text-xs font-medium uppercase tracking-wide text-gray-500"> <thead className="bg-gray-50 text-left text-xs font-medium uppercase tracking-wide text-gray-500">
<tr> <tr>
<th className="px-4 py-3">Time</th> <th className="px-4 py-3">Time</th>
@@ -361,7 +378,7 @@ export default function ResaleDashboard() {
<th className="px-4 py-3 text-right">Output</th> <th className="px-4 py-3 text-right">Output</th>
<th className="px-4 py-3 text-right">Cache read</th> <th className="px-4 py-3 text-right">Cache read</th>
<th className="px-4 py-3 text-right">Revenue</th> <th className="px-4 py-3 text-right">Revenue</th>
<th className="px-4 py-3">Base settlement</th> <th className="px-4 py-3">Vector proof</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-gray-100">
@@ -384,17 +401,8 @@ export default function ResaleDashboard() {
<td className="px-4 py-3 text-right tabular-nums">{count(sale.outputTokens)}</td> <td className="px-4 py-3 text-right tabular-nums">{count(sale.outputTokens)}</td>
<td className="px-4 py-3 text-right tabular-nums">{count(sale.cacheReadTokens)}</td> <td className="px-4 py-3 text-right tabular-nums">{count(sale.cacheReadTokens)}</td>
<td className="px-4 py-3 text-right tabular-nums">{usd(sale.revenueUsd)}</td> <td className="px-4 py-3 text-right tabular-nums">{usd(sale.revenueUsd)}</td>
<td className="px-4 py-3 font-mono text-xs"> <td className="whitespace-nowrap px-4 py-3">
{sale.transactionHash ? ( <VectorProofState proof={sale.vectorProof ?? PENDING_PROOF} />
<a
className="text-blue-600 hover:underline"
href={`https://basescan.org/tx/${sale.transactionHash}`}
target="_blank"
rel="noopener noreferrer"
>
{compact(sale.transactionHash)}
</a>
) : "Accruing"}
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -45,3 +45,16 @@ export type {
WithdrawWalletBuildResult, WithdrawWalletBuildResult,
} from "./wallet/withdrawWallet.js"; } from "./wallet/withdrawWallet.js";
export { buildWithdrawTx } from "./wallet/withdrawWallet.js"; export { buildWithdrawTx } from "./wallet/withdrawWallet.js";
// ── Surplus Merkle-batch anchor (live-only) ────────────────────────────
export type {
AnchorMetadataParams,
SurplusSaleProofAnchorMetadata,
} from "./wallet/anchorMetadata.js";
export {
MAX_TRANSACTION_METADATA_TEXT_BYTES,
SURPLUS_SALE_PROOF_METADATA_LABEL,
SURPLUS_SALE_PROOF_PROTOCOL,
buildAnchorMetadataTx,
validateSurplusSaleProofAnchorMetadata,
} from "./wallet/anchorMetadata.js";

View File

@@ -0,0 +1,166 @@
/**
* Builds and submits the live wallet transaction that anchors one Surplus sale
* Merkle batch. The caller remains responsible for awaiting confirmation.
*/
import type { LiveOgmiosProvider } from "../../chain/LiveOgmiosProvider.js";
import { OgmiosLucidProvider } from "../../chain/OgmiosLucidProvider.js";
import { createLucidContext } from "../internal/lucidContext.js";
import type { BuildResult, WalletKey } from "../types.js";
import { TxConstructionError } from "../types.js";
export const SURPLUS_SALE_PROOF_METADATA_LABEL = 674;
export const SURPLUS_SALE_PROOF_PROTOCOL = "surplus-sale-proof-v1";
export const MAX_TRANSACTION_METADATA_TEXT_BYTES = 64;
export interface SurplusSaleProofAnchorMetadata {
p: typeof SURPLUS_SALE_PROOF_PROTOCOL;
root: string;
count: number;
first: string;
last: string;
}
export interface AnchorMetadataParams {
chain: LiveOgmiosProvider;
walletKey: WalletKey;
metadata: SurplusSaleProofAnchorMetadata;
}
const ROOT_HEX_RE = /^[0-9a-f]{64}$/;
const PRINTABLE_ASCII_RE = /^[\x20-\x7e]+$/;
const METADATA_KEYS = ["p", "root", "count", "first", "last"] as const;
const textEncoder = new TextEncoder();
function metadataTextFits(value: string): boolean {
return textEncoder.encode(value).byteLength <= MAX_TRANSACTION_METADATA_TEXT_BYTES;
}
function validSaleId(value: unknown): value is string {
return (
typeof value === "string" &&
PRINTABLE_ASCII_RE.test(value) &&
metadataTextFits(value)
);
}
/** Validate the exact label-674 payload before Lucid or Ogmios is initialized. */
export function validateSurplusSaleProofAnchorMetadata(
value: unknown,
): asserts value is SurplusSaleProofAnchorMetadata {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new TxConstructionError(
"anchor_metadata_invalid",
"Surplus sale proof metadata must be an object",
);
}
const metadata = value as Record<string, unknown>;
const keys = Object.keys(metadata);
if (
keys.length !== METADATA_KEYS.length ||
METADATA_KEYS.some((key) => !Object.prototype.hasOwnProperty.call(metadata, key))
) {
throw new TxConstructionError(
"anchor_metadata_invalid",
"Surplus sale proof metadata must contain only p, root, count, first, and last",
);
}
if (metadata.p !== SURPLUS_SALE_PROOF_PROTOCOL) {
throw new TxConstructionError(
"anchor_metadata_invalid",
`metadata protocol must be ${SURPLUS_SALE_PROOF_PROTOCOL}`,
);
}
if (!metadataTextFits(SURPLUS_SALE_PROOF_PROTOCOL)) {
throw new TxConstructionError(
"anchor_metadata_oversized",
"metadata protocol exceeds the 64-byte transaction metadata text limit",
);
}
if (typeof metadata.root !== "string" || !ROOT_HEX_RE.test(metadata.root)) {
throw new TxConstructionError(
"anchor_metadata_invalid",
"metadata root must be 64 lowercase hexadecimal characters",
);
}
if (!metadataTextFits(metadata.root)) {
throw new TxConstructionError(
"anchor_metadata_oversized",
"metadata root exceeds the 64-byte transaction metadata text limit",
);
}
if (
typeof metadata.count !== "number" ||
!Number.isSafeInteger(metadata.count) ||
metadata.count <= 0
) {
throw new TxConstructionError(
"anchor_metadata_invalid",
"metadata count must be a positive safe integer",
);
}
for (const field of ["first", "last"] as const) {
const id = metadata[field];
if (!validSaleId(id)) {
const oversized =
typeof id === "string" &&
textEncoder.encode(id).byteLength > MAX_TRANSACTION_METADATA_TEXT_BYTES;
throw new TxConstructionError(
oversized ? "anchor_metadata_oversized" : "anchor_metadata_invalid",
`metadata ${field} must be a non-empty printable ASCII sale ID of at most 64 bytes`,
);
}
}
}
export async function buildAnchorMetadataTx(
params: AnchorMetadataParams,
): Promise<BuildResult> {
const { chain, walletKey, metadata } = params;
validateSurplusSaleProofAnchorMetadata(metadata);
const provider = new OgmiosLucidProvider({
ogmiosUrl: chain.url,
fetch: chain.fetchImpl,
});
const { lucid } = await createLucidContext(
provider,
walletKey,
{ networkId: 1, systemStartUnix: 0, slotLengthMs: 1000 },
{ usePresetProtocolParameters: true },
);
const realWalletUtxos = await lucid.wallet().getUtxos();
const anchorMetadata = {
p: metadata.p,
root: metadata.root,
count: metadata.count,
first: metadata.first,
last: metadata.last,
};
let signed;
try {
const txBuilder = lucid
.newTx()
.attachMetadata(SURPLUS_SALE_PROOF_METADATA_LABEL, anchorMetadata)
.addSignerKey(walletKey.pubKeyHash)
.setMinFee(500_000n);
const completed = await txBuilder.complete({
presetWalletInputs: realWalletUtxos,
localUPLCEval: false,
});
signed = await completed.sign.withWallet().complete();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new TxConstructionError("anchor_metadata_build_failed", message);
}
const txCborHex = signed.toCBOR();
const expectedTxHash = signed.toHash();
await chain.submitTx(txCborHex);
return { txCborHex, expectedTxHash };
}

View File

@@ -69,9 +69,10 @@ export interface SurplusOfferPatch {
export interface SurplusSale { export interface SurplusSale {
id: string; id: string;
model: string; model: string;
offerId: string | null; offerId: string;
settlementStatus: string; settlementStatus: string;
createdAt: string | null; createdAt: string;
createdAtMs: number;
sellerCostMicroUsd: number; sellerCostMicroUsd: number;
inputTokens: number; inputTokens: number;
outputTokens: number; outputTokens: number;
@@ -137,6 +138,15 @@ export class SurplusHttpError extends Error {
} }
} }
export class SurplusHistoricalCompletenessError extends Error {
constructor() {
super(
"Surplus does not provide historical sale pagination; only the observed recent-sales window is available",
);
this.name = "SurplusHistoricalCompletenessError";
}
}
export class SurplusClient { export class SurplusClient {
private readonly apiBaseUrl: string; private readonly apiBaseUrl: string;
private readonly sellerApiKey: string; private readonly sellerApiKey: string;
@@ -337,6 +347,27 @@ export class SurplusClient {
}; };
} }
async listObservedSales(): Promise<SurplusSale[]> {
const salesById = new Map<string, SurplusSale>();
for (const sale of (await this.getEarnings()).recentSales) {
const existing = salesById.get(sale.id);
if (existing === undefined) {
salesById.set(sale.id, sale);
} else if (!sameImmutableSale(existing, sale)) {
throw new Error(
`Surplus recent-sales window contains conflicting immutable fields for sale ${sale.id}`,
);
}
}
return [...salesById.values()].sort((left, right) =>
left.id.localeCompare(right.id)
);
}
async listAllSales(): Promise<SurplusSale[]> {
throw new SurplusHistoricalCompletenessError();
}
private async requestJson( private async requestJson(
path: string, path: string,
init: RequestInit = {}, init: RequestInit = {},
@@ -536,16 +567,21 @@ function parseOrderBookOffer(value: unknown, index: number): SurplusOrderBookOff
function parseSale(value: unknown, index: number): SurplusSale { function parseSale(value: unknown, index: number): SurplusSale {
const item = record(value, `Surplus sale ${index}`); const item = record(value, `Surplus sale ${index}`);
const createdAtMs = timestampMs(
item.created_at,
`Surplus sale ${index}.created_at`,
);
return { return {
id: string(item.id, `Surplus sale ${index}.id`), id: string(item.id, `Surplus sale ${index}.id`),
model: string(item.model, `Surplus sale ${index}.model`), model: string(item.model, `Surplus sale ${index}.model`),
offerId: optionalString(item.offer_id), offerId: string(item.offer_id, `Surplus sale ${index}.offer_id`),
settlementStatus: string(item.settlement_status, `Surplus sale ${index}.settlement_status`), settlementStatus: string(item.settlement_status, `Surplus sale ${index}.settlement_status`),
createdAt: optionalTimestamp(item.created_at, `Surplus sale ${index}.created_at`), createdAt: new Date(createdAtMs).toISOString(),
createdAtMs,
sellerCostMicroUsd: microUsd(item.seller_cost_usdc, `Surplus sale ${index}.seller_cost_usdc`), sellerCostMicroUsd: microUsd(item.seller_cost_usdc, `Surplus sale ${index}.seller_cost_usdc`),
inputTokens: optionalSafeInteger(item.input_tokens, `Surplus sale ${index}.input_tokens`), inputTokens: nonNegativeSafeInteger(item.input_tokens, `Surplus sale ${index}.input_tokens`),
outputTokens: optionalSafeInteger(item.output_tokens, `Surplus sale ${index}.output_tokens`), outputTokens: nonNegativeSafeInteger(item.output_tokens, `Surplus sale ${index}.output_tokens`),
cacheReadTokens: optionalSafeInteger( cacheReadTokens: nonNegativeSafeInteger(
item.cache_read_tokens, item.cache_read_tokens,
`Surplus sale ${index}.cache_read_tokens`, `Surplus sale ${index}.cache_read_tokens`,
), ),
@@ -561,6 +597,19 @@ function parseSale(value: unknown, index: number): SurplusSale {
}; };
} }
function sameImmutableSale(left: SurplusSale, right: SurplusSale): boolean {
return left.id === right.id &&
left.offerId === right.offerId &&
left.model === right.model &&
left.createdAtMs === right.createdAtMs &&
left.inputTokens === right.inputTokens &&
left.outputTokens === right.outputTokens &&
left.cacheReadTokens === right.cacheReadTokens &&
left.sellerCostMicroUsd === right.sellerCostMicroUsd &&
left.effectiveInputUsdPer1m === right.effectiveInputUsdPer1m &&
left.effectiveOutputUsdPer1m === right.effectiveOutputUsdPer1m;
}
function parseDailyEarnings( function parseDailyEarnings(
value: unknown, value: unknown,
index: number, index: number,
@@ -648,15 +697,19 @@ function optionalString(value: unknown): string | null {
function optionalTimestamp(value: unknown, field: string): string | null { function optionalTimestamp(value: unknown, field: string): string | null {
if (value === null || value === undefined) return null; if (value === null || value === undefined) return null;
return new Date(timestampMs(value, field)).toISOString();
}
function timestampMs(value: unknown, field: string): number {
const timestamp = typeof value === "number" const timestamp = typeof value === "number"
? value ? value
: typeof value === "string" : typeof value === "string"
? Date.parse(value) ? Date.parse(value)
: Number.NaN; : Number.NaN;
if (!Number.isFinite(timestamp) || timestamp < 0) { if (!Number.isSafeInteger(timestamp) || timestamp < 0) {
throw new Error(`${field} must be a timestamp`); throw new Error(`${field} must be a timestamp with integer milliseconds`);
} }
return new Date(timestamp).toISOString(); return timestamp;
} }
function boolean(value: unknown, field: string): boolean { function boolean(value: unknown, field: string): boolean {

View File

@@ -5,7 +5,12 @@ import type {
SurplusEarnings, SurplusEarnings,
SurplusOffer, SurplusOffer,
SurplusOrderBook, SurplusOrderBook,
SurplusSale,
} from "./client.js"; } from "./client.js";
import {
hashSurplusSale,
type SurplusDashboardVectorProof,
} from "./vectorProof.js";
import type { import type {
SurplusControllerStatus, SurplusControllerStatus,
SurplusSellerController, SurplusSellerController,
@@ -25,6 +30,19 @@ export interface VectorMigrationSummary {
historicalSettledJobs: number; historicalSettledJobs: number;
historicalUpstreamSpendUsd: string; historicalUpstreamSpendUsd: string;
} }
export interface DashboardSaleProof {
status: "pending" | "confirmed" | "failed";
saleHash: string | null;
batchRoot: string | null;
txHash: string | null;
leafIndex: number | null;
siblings: string[];
}
export interface DashboardProofSource {
dashboardProof(saleId: string): SurplusDashboardVectorProof | null;
}
export interface SurplusDashboardSnapshot { export interface SurplusDashboardSnapshot {
generatedAt: string; generatedAt: string;
@@ -80,6 +98,7 @@ export interface SurplusDashboardSnapshot {
cacheReadTokens: number; cacheReadTokens: number;
revenueUsd: string; revenueUsd: string;
transactionHash: string | null; transactionHash: string | null;
vectorProof: DashboardSaleProof;
}>; }>;
}; };
vector: VectorMigrationSummary; vector: VectorMigrationSummary;
@@ -91,6 +110,7 @@ interface SurplusDashboardOptions {
allowance: Pick<OpenRouterKeyClient, "readAllowance">; allowance: Pick<OpenRouterKeyClient, "readAllowance">;
config: SurplusManagerConfig; config: SurplusManagerConfig;
vector?: VectorMigrationSummary; vector?: VectorMigrationSummary;
proofs?: DashboardProofSource;
cacheMs?: number; cacheMs?: number;
now?: () => number; now?: () => number;
} }
@@ -117,6 +137,7 @@ export class SurplusDashboardService {
>; >;
private readonly allowance: Pick<OpenRouterKeyClient, "readAllowance">; private readonly allowance: Pick<OpenRouterKeyClient, "readAllowance">;
private readonly config: SurplusManagerConfig; private readonly config: SurplusManagerConfig;
private readonly proofs: DashboardProofSource | null;
private readonly vector: VectorMigrationSummary; private readonly vector: VectorMigrationSummary;
private readonly cacheMs: number; private readonly cacheMs: number;
private readonly now: () => number; private readonly now: () => number;
@@ -128,6 +149,7 @@ export class SurplusDashboardService {
this.client = options.client; this.client = options.client;
this.allowance = options.allowance; this.allowance = options.allowance;
this.config = options.config; this.config = options.config;
this.proofs = options.proofs ?? null;
this.vector = options.vector ?? DEFAULT_VECTOR_MIGRATION; this.vector = options.vector ?? DEFAULT_VECTOR_MIGRATION;
this.cacheMs = options.cacheMs ?? 15_000; this.cacheMs = options.cacheMs ?? 15_000;
this.now = options.now ?? Date.now; this.now = options.now ?? Date.now;
@@ -218,11 +240,31 @@ export class SurplusDashboardService {
cacheReadTokens: sale.cacheReadTokens, cacheReadTokens: sale.cacheReadTokens,
revenueUsd: formatMicroUsd(sale.sellerCostMicroUsd), revenueUsd: formatMicroUsd(sale.sellerCostMicroUsd),
transactionHash: sale.transactionHash, transactionHash: sale.transactionHash,
vectorProof: this.proofFor(sale),
})), })),
}, },
vector: this.vector, vector: this.vector,
}; };
} }
private proofFor(sale: SurplusSale): DashboardSaleProof {
const ledger = this.proofs?.dashboardProof(sale.id) ?? null;
if (ledger !== null) return ledger;
let saleHash: string | null = null;
try {
saleHash = hashSurplusSale(sale);
} catch {
saleHash = null;
}
return {
status: "pending",
saleHash,
batchRoot: null,
txHash: null,
leafIndex: null,
siblings: [],
};
}
} }
function findOffer( function findOffer(

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
// @vitest-environment happy-dom // @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen, waitFor, within } from "@testing-library/react";
import ResaleDashboard from "../../buyer/src/ui/pages/ResaleDashboard.js"; import { MemoryRouter } from "react-router-dom";
import App from "../../buyer/src/ui/App.js";
import { AuthProvider } from "../../buyer/src/ui/state/AuthContext.js";
const DASHBOARD = { const DASHBOARD = {
generatedAt: "2026-08-28T12:00:00.000Z", generatedAt: "2026-08-28T12:00:00.000Z",
@@ -47,28 +49,62 @@ const DASHBOARD = {
payoutHoldReason: "new_seller", payoutHoldReason: "new_seller",
payoutHoldReleasesAt: "2026-08-29T11:24:42.552Z", payoutHoldReleasesAt: "2026-08-29T11:24:42.552Z",
}, },
recentSales: [{ recentSales: [
id: "sale-1", {
model: "deepseek-v4-flash", id: "sale-1",
createdAt: "2026-08-28T11:00:00.000Z", model: "deepseek-v4-flash",
settlementStatus: "confirmed", createdAt: "2026-08-28T11:00:00.000Z",
inputTokens: 10, settlementStatus: "confirmed",
outputTokens: 2, inputTokens: 10,
cacheReadTokens: 3, outputTokens: 2,
revenueUsd: "0.000083", cacheReadTokens: 3,
transactionHash: "0x" + "c".repeat(64), revenueUsd: "0.000083",
}], vectorProof: {
}, status: "confirmed",
vector: { saleHash: "1".repeat(64),
status: "retired", batchRoot: "2".repeat(64),
model: "deepseek/deepseek-v4-flash", txHash: "d".repeat(64),
supplierWallet: "addr1test", leafIndex: 0,
advertRef: `${"a".repeat(64)}#0`, siblings: ["3".repeat(64)],
retirementTransaction: "b".repeat(64), },
retiredOn: "2026-08-27", },
historicalAp3xEarned: "1.600000", {
historicalSettledJobs: 8, id: "sale-2",
historicalUpstreamSpendUsd: "0.000093", model: "deepseek-v4-flash",
createdAt: "2026-08-28T11:01:00.000Z",
settlementStatus: "confirmed",
inputTokens: 20,
outputTokens: 4,
cacheReadTokens: 6,
revenueUsd: "0.000166",
vectorProof: {
status: "pending",
saleHash: "4".repeat(64),
batchRoot: null,
txHash: null,
leafIndex: null,
siblings: [],
},
},
{
id: "sale-3",
model: "deepseek-v4-flash",
createdAt: "2026-08-28T11:02:00.000Z",
settlementStatus: "confirmed",
inputTokens: 30,
outputTokens: 6,
cacheReadTokens: 9,
revenueUsd: "0.000249",
vectorProof: {
status: "failed",
saleHash: "5".repeat(64),
batchRoot: null,
txHash: null,
leafIndex: null,
siblings: [],
},
},
],
}, },
}; };
@@ -76,29 +112,41 @@ afterEach(() => {
vi.unstubAllGlobals(); vi.unstubAllGlobals();
}); });
describe("ResaleDashboard", () => { describe("Capacity markets dashboard", () => {
it("matches the marketplace card system and renders both settlement markets", async () => { it("renders the authenticated route with a Vector proof state for every sale", async () => {
const fetchMock = vi.fn(async () => Response.json(DASHBOARD)); const fetchMock = vi.fn(async () => Response.json(DASHBOARD));
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
render(<ResaleDashboard />); render(
<MemoryRouter initialEntries={["/dashboard"]}>
<AuthProvider initialStatus="authenticated">
<App />
</AuthProvider>
</MemoryRouter>,
);
expect(await screen.findByRole("heading", { name: "Resale Dashboard" })).toBeInTheDocument(); expect(await screen.findByRole("heading", { name: "Capacity Markets" })).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Capacity" })).toHaveAttribute("href", "/dashboard");
expect(screen.getByRole("heading", { name: "Surplus Intelligence" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Surplus Intelligence" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Vector marketplace" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "Vector proof" })).toBeInTheDocument();
expect(screen.getByText("$0.2399")).toBeInTheDocument(); expect(screen.getByText("$0.2399")).toBeInTheDocument();
expect(screen.getByText("1.600000 AP3X")).toBeInTheDocument();
expect(screen.getByText("confirmed")).toBeInTheDocument();
expect(screen.getByTestId("resale-dashboard")).toHaveClass("max-w-7xl"); expect(screen.getByTestId("resale-dashboard")).toHaveClass("max-w-7xl");
expect(document.querySelectorAll(".bg-white").length).toBeGreaterThan(3); expect(document.querySelectorAll(".bg-white").length).toBeGreaterThan(3);
expect(screen.getByRole("link", { name: /Open confirmed transaction/ })).toHaveAttribute(
const proofSummary = screen.getByRole("group", { name: "Vector proof summary" });
expect(within(proofSummary).getByText("Confirmed 1")).toHaveClass("bg-green-100");
expect(within(proofSummary).getByText("Pending 1")).toHaveClass("bg-amber-100");
expect(within(proofSummary).getByText("Failed 1")).toHaveClass("bg-red-100");
expect(screen.getByText("pending")).toHaveClass("bg-amber-100");
expect(screen.getByText("failed")).toHaveClass("bg-red-100");
const proofLink = screen.getByRole("link", { name: /Confirmed Vector proof transaction/ });
expect(proofLink).toHaveAttribute(
"href", "href",
`https://vector.apexscan.org/en/transaction/${"b".repeat(64)}/summary/`, `https://vector.apexscan.org/en/transaction/${"d".repeat(64)}/summary/`,
);
expect(screen.getByRole("link", { name: /0xcccc/ })).toHaveAttribute(
"href",
`https://basescan.org/tx/0x${"c".repeat(64)}`,
); );
expect(within(proofLink).getByText("dddddddddddd…ddddddddd")).toBeInTheDocument();
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith( await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
"/v1/resale-dashboard", "/v1/resale-dashboard",
{ cache: "no-store" }, { cache: "no-store" },

View File

@@ -36,6 +36,42 @@ function discoveryRow(model: string, providerModelId: string): string {
}); });
} }
function saleRow(
id: string,
overrides: Record<string, unknown> = {},
): Record<string, unknown> {
return {
id,
model: "alpha",
offer_id: "offer-1",
settlement_status: "confirmed",
created_at: 1_787_831_146_968,
seller_cost_usdc: "83",
input_tokens: 10,
output_tokens: 2,
cache_read_tokens: 4,
effective_input_per_1m: 0.004,
effective_output_per_1m: 0.008,
tx_hash: null,
...overrides,
};
}
function earningsEnvelope(
recentSales: Record<string, unknown>[],
): Record<string, unknown> {
return {
total_earned_usdc: "166",
pending_usdc: "166",
paid_usdc: "0",
daily: [],
by_model: [],
share: { requests: recentSales.length, tokens: 0, top_model: "alpha" },
recent_sales: recentSales,
payout_hold: null,
};
}
describe("SurplusClient", () => { describe("SurplusClient", () => {
it("authenticates and parses fragmented NDJSON discovery", async () => { it("authenticates and parses fragmented NDJSON discovery", async () => {
const first = discoveryRow("alpha", "vendor/alpha"); const first = discoveryRow("alpha", "vendor/alpha");
@@ -296,6 +332,90 @@ describe("SurplusClient", () => {
expect(earnings.recentSales[0].createdAt).toBe( expect(earnings.recentSales[0].createdAt).toBe(
new Date(1787831146968).toISOString(), new Date(1787831146968).toISOString(),
); );
expect(earnings.recentSales[0].createdAtMs).toBe(1787831146968);
});
it("returns the deduplicated observed sale window in deterministic sale-id order", async () => {
const duplicate = saleRow("sale-a", {
buyer_address: "must-not-be-retained",
});
const client = new SurplusClient({
apiBaseUrl: "https://api.surplusintelligence.ai",
sellerApiKey: SELLER_KEY,
timeoutMs: 1_000,
fetchFn: async () =>
Response.json(earningsEnvelope([
saleRow("sale-b"),
duplicate,
{ ...duplicate },
])),
});
const sales = await client.listObservedSales();
expect(sales.map((sale) => sale.id)).toEqual(["sale-a", "sale-b"]);
expect(sales[0]).not.toHaveProperty("buyerAddress");
expect(sales[0]).not.toHaveProperty("buyer_address");
});
it("fails closed on conflicting immutable fields in the observed sale window", async () => {
const client = new SurplusClient({
apiBaseUrl: "https://api.surplusintelligence.ai",
sellerApiKey: SELLER_KEY,
timeoutMs: 1_000,
fetchFn: async () =>
Response.json(earningsEnvelope([
saleRow("sale-a"),
saleRow("sale-a", { input_tokens: 11 }),
])),
});
await expect(client.listObservedSales()).rejects.toThrow(
"conflicting immutable fields for sale sale-a",
);
});
it("requires every immutable proof field in observed sales", async () => {
const requiredFields = [
"id",
"model",
"offer_id",
"created_at",
"seller_cost_usdc",
"input_tokens",
"output_tokens",
"cache_read_tokens",
"effective_input_per_1m",
"effective_output_per_1m",
];
for (const field of requiredFields) {
const row = saleRow("sale-a");
delete row[field];
const client = new SurplusClient({
apiBaseUrl: "https://api.surplusintelligence.ai",
sellerApiKey: SELLER_KEY,
timeoutMs: 1_000,
fetchFn: async () => Response.json(earningsEnvelope([row])),
});
await expect(client.listObservedSales()).rejects.toThrow();
}
});
it("fails clearly instead of claiming unavailable historical completeness", async () => {
const fetchFn = vi.fn();
const client = new SurplusClient({
apiBaseUrl: "https://api.surplusintelligence.ai",
sellerApiKey: SELLER_KEY,
timeoutMs: 1_000,
fetchFn,
});
await expect(client.listAllSales()).rejects.toThrow(
"does not provide historical sale pagination",
);
expect(fetchFn).not.toHaveBeenCalled();
}); });
it("sends one absolute cost-multiplier PATCH with the cap", async () => { it("sends one absolute cost-multiplier PATCH with the cap", async () => {

View File

@@ -0,0 +1,340 @@
import { createHash } from "node:crypto";
import {
mkdtemp,
readFile,
rm,
stat,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
buildSurplusMerkleBatch,
canonicalizeSurplusSale,
canonicalSurplusSaleJson,
FileSurplusVectorProofLedger,
hashSurplusSale,
SURPLUS_SALE_PROOF_PROTOCOL,
verifySurplusMerkleProof,
type SurplusMerkleProof,
type SurplusMerkleBatch,
type SurplusSaleProofInput,
} from "../../supplier/src/surplus/vectorProof.js";
const directories: string[] = [];
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), "surplus-vector-proof-"));
directories.push(directory);
return directory;
}
afterEach(async () => {
await Promise.all(directories.splice(0).map((path) =>
rm(path, { recursive: true, force: true })
));
});
function sale(
id: string,
overrides: Partial<SurplusSaleProofInput> = {},
): SurplusSaleProofInput {
return {
id,
offerId: "offer-1",
model: "deepseek-v4-flash",
createdAt: "2026-08-28T11:00:00.123Z",
inputTokens: 10,
outputTokens: 2,
cacheReadTokens: 3,
sellerCostMicroUsd: 83,
effectiveInputUsdPer1m: 0.003972,
effectiveOutputUsdPer1m: 0.007944,
...overrides,
};
}
function proofFor(
batch: SurplusMerkleBatch,
saleId: string,
): SurplusMerkleProof {
const leaf = batch.leaves.find((candidate) => candidate.sale.saleId === saleId);
if (leaf === undefined) throw new Error(`Missing test leaf ${saleId}`);
return {
saleHash: leaf.saleHash,
batchRoot: leaf.batchRoot,
leafIndex: leaf.leafIndex,
leafCount: leaf.leafCount,
siblings: [...leaf.siblings],
};
}
function changedHash(hash: string): string {
return `${hash[0] === "0" ? "1" : "0"}${hash.slice(1)}`;
}
describe("Surplus Vector sale projection", () => {
it("encodes the versioned leaf in the required field order and hashes its UTF-8 JSON", () => {
const input = sale("sale-1");
const canonical = canonicalizeSurplusSale(input);
const json = canonicalSurplusSaleJson(canonical);
expect(canonical).toEqual({
protocol: SURPLUS_SALE_PROOF_PROTOCOL,
saleId: "sale-1",
offerId: "offer-1",
model: "deepseek-v4-flash",
createdAtEpochMs: Date.parse("2026-08-28T11:00:00.123Z"),
inputTokens: 10,
outputTokens: 2,
cacheReadTokens: 3,
sellerCostMicroUsd: 83,
effectiveInputUsdPer1m: "0.003972",
effectiveOutputUsdPer1m: "0.007944",
});
expect(json).toBe(
'{"protocol":"surplus-sale-proof-v1","saleId":"sale-1","offerId":"offer-1","model":"deepseek-v4-flash","createdAtEpochMs":1787914800123,"inputTokens":10,"outputTokens":2,"cacheReadTokens":3,"sellerCostMicroUsd":83,"effectiveInputUsdPer1m":"0.003972","effectiveOutputUsdPer1m":"0.007944"}',
);
expect(hashSurplusSale(input)).toBe(
createHash("sha256").update(json, "utf8").digest("hex"),
);
});
it("rejects sales that cannot produce an immutable canonical leaf", () => {
expect(() => canonicalizeSurplusSale(sale("sale-1", { offerId: null })))
.toThrow("offer id");
expect(() => canonicalizeSurplusSale(sale("sale-1", { createdAt: null })))
.toThrow("no creation time");
expect(() => canonicalizeSurplusSale(sale("sale-1", { inputTokens: -1 })))
.toThrow("non-negative safe integer");
expect(() => canonicalizeSurplusSale(sale("bad\nid")))
.toThrow("printable ASCII");
});
});
describe("Surplus sale Merkle batches", () => {
it("sorts by sale id and duplicates the final node at odd tree levels", () => {
const batch = buildSurplusMerkleBatch([
canonicalizeSurplusSale(sale("sale-c")),
canonicalizeSurplusSale(sale("sale-a")),
canonicalizeSurplusSale(sale("sale-b")),
]);
expect(batch.count).toBe(3);
expect(batch.firstSaleId).toBe("sale-a");
expect(batch.lastSaleId).toBe("sale-c");
expect(batch.leaves.map((leaf) => leaf.sale.saleId)).toEqual([
"sale-a",
"sale-b",
"sale-c",
]);
const last = batch.leaves[2];
expect(last.siblings[0]).toBe(last.saleHash);
for (const leaf of batch.leaves) {
expect(verifySurplusMerkleProof(leaf.sale, leaf)).toBe(true);
}
const reordered = buildSurplusMerkleBatch([
canonicalizeSurplusSale(sale("sale-b")),
canonicalizeSurplusSale(sale("sale-c")),
canonicalizeSurplusSale(sale("sale-a")),
]);
expect(reordered.root).toBe(batch.root);
expect(reordered.leaves).toEqual(batch.leaves);
});
it("rejects every material proof mutation", () => {
const originalSale = sale("sale-a");
const batch = buildSurplusMerkleBatch([
canonicalizeSurplusSale(originalSale),
canonicalizeSurplusSale(sale("sale-b")),
canonicalizeSurplusSale(sale("sale-c")),
]);
const proof = proofFor(batch, "sale-a");
expect(verifySurplusMerkleProof(originalSale, proof)).toBe(true);
const changedFields: SurplusSaleProofInput[] = [
sale("sale-x"),
sale("sale-a", { offerId: "offer-2" }),
sale("sale-a", { model: "another-model" }),
sale("sale-a", { createdAt: "2026-08-28T11:00:00.124Z" }),
sale("sale-a", { inputTokens: 11 }),
sale("sale-a", { outputTokens: 3 }),
sale("sale-a", { cacheReadTokens: 4 }),
sale("sale-a", { sellerCostMicroUsd: 84 }),
sale("sale-a", { effectiveInputUsdPer1m: 0.003973 }),
sale("sale-a", { effectiveOutputUsdPer1m: 0.007945 }),
];
for (const changed of changedFields) {
expect(verifySurplusMerkleProof(changed, proof)).toBe(false);
}
expect(verifySurplusMerkleProof(originalSale, {
...proof,
siblings: [changedHash(proof.siblings[0]), ...proof.siblings.slice(1)],
})).toBe(false);
expect(verifySurplusMerkleProof(originalSale, {
...proof,
leafIndex: 1,
})).toBe(false);
expect(verifySurplusMerkleProof(originalSale, {
...proof,
batchRoot: changedHash(proof.batchRoot),
})).toBe(false);
expect(verifySurplusMerkleProof(originalSale, {
...proof,
saleHash: changedHash(proof.saleHash),
})).toBe(false);
});
});
describe("FileSurplusVectorProofLedger", () => {
it("persists intent before confirmation and never selects confirmed sales again", async () => {
const directory = await temporaryDirectory();
const path = join(directory, "proofs", "ledger.json");
let now = Date.parse("2026-08-28T12:00:00.000Z");
const ledger = await FileSurplusVectorProofLedger.open(path, { now: () => now });
expect(await ledger.ingestSales([
sale("sale-c"),
sale("sale-a"),
sale("sale-b"),
sale("sale-a"),
])).toEqual({ inserted: 3, duplicates: 1 });
expect(ledger.selectUnprovedSales(2).map((item) => item.saleId)).toEqual([
"sale-a",
"sale-b",
]);
const intent = await ledger.persistPendingBatch(["sale-b", "sale-a"]);
expect(intent.leaves.map((leaf) => leaf.saleId)).toEqual(["sale-a", "sale-b"]);
expect((await stat(path)).mode & 0o777).toBe(0o600);
const restarted = await FileSurplusVectorProofLedger.open(path, { now: () => now });
expect(restarted.pendingBatchIntent()).toEqual(intent);
expect(() => restarted.selectUnprovedSales()).toThrow(
"pending Surplus proof batch must be resolved first",
);
await expect(restarted.createPendingBatch()).rejects.toThrow(
"pending Surplus proof batch must be resolved first",
);
now += 1_000;
const txHash = "AB".repeat(32);
const confirmed = await restarted.confirmPendingBatch(intent.batchRoot, txHash);
expect(confirmed.txHash).toBe(txHash.toLowerCase());
for (const id of ["sale-a", "sale-b"]) {
const proof = restarted.dashboardProof(id);
expect(proof).toMatchObject({
status: "confirmed",
batchRoot: intent.batchRoot,
txHash: txHash.toLowerCase(),
});
expect(verifySurplusMerkleProof(sale(id), {
saleHash: proof!.saleHash,
batchRoot: proof!.batchRoot!,
leafIndex: proof!.leafIndex!,
leafCount: intent.count,
siblings: proof!.siblings,
})).toBe(true);
}
const confirmedRestart = await FileSurplusVectorProofLedger.open(path);
expect(confirmedRestart.selectUnprovedSales().map((item) => item.saleId))
.toEqual(["sale-c"]);
expect(confirmedRestart.confirmedBatchHistory()).toEqual([confirmed]);
});
it("deduplicates exact sales and rejects an immutable id conflict", async () => {
const directory = await temporaryDirectory();
const path = join(directory, "ledger.json");
const ledger = await FileSurplusVectorProofLedger.open(path);
expect(await ledger.ingestSales([sale("sale-a")]))
.toEqual({ inserted: 1, duplicates: 0 });
expect(await ledger.ingestSales([sale("sale-a")]))
.toEqual({ inserted: 0, duplicates: 1 });
await expect(ledger.ingestSales([
sale("sale-a", { sellerCostMicroUsd: 84 }),
])).rejects.toThrow("Conflicting Surplus sale id");
expect(ledger.selectUnprovedSales()).toHaveLength(1);
});
it("records a safe failed attempt and makes its sales eligible for a later batch", async () => {
const directory = await temporaryDirectory();
const path = join(directory, "ledger.json");
let now = Date.parse("2026-08-28T12:00:00.000Z");
const ledger = await FileSurplusVectorProofLedger.open(path, { now: () => now });
await ledger.ingestSales([sale("sale-a"), sale("sale-b")]);
const intent = await ledger.createPendingBatch();
if (intent === null) throw new Error("Expected a pending test batch");
now += 1_000;
const failure = await ledger.failPendingBatch(intent.batchRoot, "submit");
expect(failure).toEqual({
batchRoot: intent.batchRoot,
count: 2,
firstSaleId: "sale-a",
lastSaleId: "sale-b",
stage: "submit",
failedAt: "2026-08-28T12:00:01.000Z",
});
expect(ledger.dashboardProof("sale-a")).toMatchObject({
status: "failed",
batchRoot: intent.batchRoot,
txHash: null,
leafIndex: 0,
});
const restarted = await FileSurplusVectorProofLedger.open(path);
expect(restarted.failedAttemptHistory()).toEqual([failure]);
expect(restarted.selectUnprovedSales().map((item) => item.saleId)).toEqual([
"sale-a",
"sale-b",
]);
const retry = await restarted.createPendingBatch();
expect(retry?.batchRoot).toBe(intent.batchRoot);
expect(restarted.dashboardProof("sale-a")?.status).toBe("pending");
});
it("stores only canonical sale fields and proof data", async () => {
const directory = await temporaryDirectory();
const path = join(directory, "ledger.json");
const ledger = await FileSurplusVectorProofLedger.open(path);
const input = {
...sale("sale-a"),
apiKey: "must-not-be-written",
buyerAddress: "must-not-be-written",
};
await ledger.ingestSales([input]);
const text = await readFile(path, "utf8");
expect(text).not.toContain("must-not-be-written");
expect(text).not.toContain("apiKey");
expect(text).not.toContain("buyerAddress");
});
it("rejects ledger symlinks on load and before an atomic write", async () => {
const directory = await temporaryDirectory();
const target = join(directory, "target.json");
const loadPath = join(directory, "load-ledger.json");
await writeFile(
target,
'{"version":1,"sales":[],"pendingBatch":null,"confirmedBatches":[],"failedAttempts":[]}\n',
{ mode: 0o600 },
);
await symlink(target, loadPath);
await expect(FileSurplusVectorProofLedger.open(loadPath)).rejects.toThrow();
const writePath = join(directory, "write-ledger.json");
const ledger = await FileSurplusVectorProofLedger.open(writePath);
await symlink(target, writePath);
await expect(ledger.ingestSales([sale("sale-a")])).rejects.toThrow(
"wrong file type",
);
expect(await readFile(target, "utf8")).not.toContain("sale-a");
});
});

View File

@@ -0,0 +1,227 @@
import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
import type { LiveOgmiosProvider } from "../../packages/shared/src/chain/LiveOgmiosProvider.js";
import type { WalletKey } from "../../packages/shared/src/tx/types.js";
const mocks = vi.hoisted(() => ({
createLucidContext: vi.fn(),
providerConstructor: vi.fn(),
}));
vi.mock(
"../../packages/shared/src/chain/OgmiosLucidProvider.js",
() => ({
OgmiosLucidProvider: class {
constructor(options: unknown) {
mocks.providerConstructor(options);
}
},
}),
);
vi.mock(
"../../packages/shared/src/tx/internal/lucidContext.js",
() => ({ createLucidContext: mocks.createLucidContext }),
);
import {
buildAnchorMetadataTx,
SURPLUS_SALE_PROOF_METADATA_LABEL,
SURPLUS_SALE_PROOF_PROTOCOL,
type SurplusSaleProofAnchorMetadata,
} from "../../packages/shared/src/tx/wallet/anchorMetadata.js";
const walletKey: WalletKey = {
address: "addr1wallet",
pubKeyHash: "1".repeat(56),
pubKeyHex: "2".repeat(64),
privateKeyHex: "3".repeat(64),
};
function validMetadata(): SurplusSaleProofAnchorMetadata {
return {
p: SURPLUS_SALE_PROOF_PROTOCOL,
root: "a".repeat(64),
count: 1_000,
first: "sale-000001",
last: "sale-001000",
};
}
interface LucidHarness {
chain: LiveOgmiosProvider;
submitTx: Mock;
getUtxos: Mock;
newTx: Mock;
attachMetadata: Mock;
addSignerKey: Mock;
setMinFee: Mock;
completeTx: Mock;
withWallet: Mock;
completeSignature: Mock;
payToAddress: Mock;
}
function lucidHarness(): LucidHarness {
const submitTx = vi.fn().mockResolvedValue("submitted-hash");
const chain = {
url: "http://ogmios.test",
fetchImpl: undefined,
submitTx,
} as unknown as LiveOgmiosProvider;
const realWalletUtxos = [
{ txHash: "f".repeat(64), outputIndex: 0, assets: { lovelace: 25_000_000n } },
];
const getUtxos = vi.fn().mockResolvedValue(realWalletUtxos);
const completeSignature = vi.fn().mockResolvedValue({
toCBOR: () => "signed-anchor-cbor",
toHash: () => "b".repeat(64),
});
const withWallet = vi.fn(() => ({ complete: completeSignature }));
const completeTx = vi.fn().mockResolvedValue({ sign: { withWallet } });
const payToAddress = vi.fn();
const txBuilder: Record<string, unknown> = {
pay: { ToAddress: payToAddress },
};
const attachMetadata = vi.fn(() => txBuilder);
const addSignerKey = vi.fn(() => txBuilder);
const setMinFee = vi.fn(() => txBuilder);
Object.assign(txBuilder, {
attachMetadata,
addSignerKey,
setMinFee,
complete: completeTx,
});
const newTx = vi.fn(() => txBuilder);
const lucid = {
wallet: vi.fn(() => ({ getUtxos })),
newTx,
};
mocks.createLucidContext.mockResolvedValue({ lucid });
return {
chain,
submitTx,
getUtxos,
newTx,
attachMetadata,
addSignerKey,
setMinFee,
completeTx,
withWallet,
completeSignature,
payToAddress,
};
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("buildAnchorMetadataTx", () => {
it("signs label-674 metadata with real wallet inputs and submits once", async () => {
const harness = lucidHarness();
const metadata = validMetadata();
const result = await buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata,
});
expect(mocks.providerConstructor).toHaveBeenCalledWith({
ogmiosUrl: "http://ogmios.test",
fetch: undefined,
});
expect(mocks.createLucidContext).toHaveBeenCalledWith(
expect.anything(),
walletKey,
{ networkId: 1, systemStartUnix: 0, slotLengthMs: 1000 },
{ usePresetProtocolParameters: true },
);
expect(harness.getUtxos).toHaveBeenCalledOnce();
expect(harness.newTx).toHaveBeenCalledOnce();
expect(harness.attachMetadata).toHaveBeenCalledWith(
SURPLUS_SALE_PROOF_METADATA_LABEL,
metadata,
);
expect(harness.addSignerKey).toHaveBeenCalledWith(walletKey.pubKeyHash);
expect(harness.setMinFee).toHaveBeenCalledWith(500_000n);
expect(harness.completeTx).toHaveBeenCalledWith({
presetWalletInputs: expect.arrayContaining([
expect.objectContaining({ assets: { lovelace: 25_000_000n } }),
]),
localUPLCEval: false,
});
expect(harness.withWallet).toHaveBeenCalledOnce();
expect(harness.completeSignature).toHaveBeenCalledOnce();
expect(harness.payToAddress).not.toHaveBeenCalled();
expect(harness.submitTx).toHaveBeenCalledOnce();
expect(harness.submitTx).toHaveBeenCalledWith("signed-anchor-cbor");
expect(result).toEqual({
txCborHex: "signed-anchor-cbor",
expectedTxHash: "b".repeat(64),
});
});
it.each([
["wrong protocol", { ...validMetadata(), p: "surplus-sale-proof-v2" }],
["short root", { ...validMetadata(), root: "a".repeat(63) }],
["uppercase root", { ...validMetadata(), root: "A".repeat(64) }],
["non-hex root", { ...validMetadata(), root: "z".repeat(64) }],
["zero count", { ...validMetadata(), count: 0 }],
["fractional count", { ...validMetadata(), count: 1.5 }],
["unsafe count", { ...validMetadata(), count: Number.MAX_SAFE_INTEGER + 1 }],
["empty first ID", { ...validMetadata(), first: "" }],
["control character in last ID", { ...validMetadata(), last: "sale\n2" }],
["non-ASCII first ID", { ...validMetadata(), first: "salé-1" }],
["extra field", { ...validMetadata(), extra: true }],
])("rejects %s before initializing Lucid", async (_case, metadata) => {
const harness = lucidHarness();
await expect(
buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata: metadata as SurplusSaleProofAnchorMetadata,
}),
).rejects.toMatchObject({ reason: "anchor_metadata_invalid" });
expect(mocks.providerConstructor).not.toHaveBeenCalled();
expect(mocks.createLucidContext).not.toHaveBeenCalled();
expect(harness.submitTx).not.toHaveBeenCalled();
});
it("rejects metadata text over 64 UTF-8 bytes before initializing Lucid", async () => {
const harness = lucidHarness();
await expect(
buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata: { ...validMetadata(), last: "x".repeat(65) },
}),
).rejects.toMatchObject({ reason: "anchor_metadata_oversized" });
expect(mocks.providerConstructor).not.toHaveBeenCalled();
expect(mocks.createLucidContext).not.toHaveBeenCalled();
expect(harness.submitTx).not.toHaveBeenCalled();
});
it("does not submit when transaction construction fails", async () => {
const harness = lucidHarness();
harness.completeTx.mockRejectedValueOnce(new Error("coin selection failed"));
await expect(
buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata: validMetadata(),
}),
).rejects.toMatchObject({
reason: "anchor_metadata_build_failed",
message: "coin selection failed",
});
expect(harness.submitTx).not.toHaveBeenCalled();
});
});