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

@@ -1,8 +1,10 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import ResaleDashboard from "../../buyer/src/ui/pages/ResaleDashboard.js";
import { render, screen, waitFor, within } from "@testing-library/react";
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 = {
generatedAt: "2026-08-28T12:00:00.000Z",
@@ -47,28 +49,62 @@ const DASHBOARD = {
payoutHoldReason: "new_seller",
payoutHoldReleasesAt: "2026-08-29T11:24:42.552Z",
},
recentSales: [{
id: "sale-1",
model: "deepseek-v4-flash",
createdAt: "2026-08-28T11:00:00.000Z",
settlementStatus: "confirmed",
inputTokens: 10,
outputTokens: 2,
cacheReadTokens: 3,
revenueUsd: "0.000083",
transactionHash: "0x" + "c".repeat(64),
}],
},
vector: {
status: "retired",
model: "deepseek/deepseek-v4-flash",
supplierWallet: "addr1test",
advertRef: `${"a".repeat(64)}#0`,
retirementTransaction: "b".repeat(64),
retiredOn: "2026-08-27",
historicalAp3xEarned: "1.600000",
historicalSettledJobs: 8,
historicalUpstreamSpendUsd: "0.000093",
recentSales: [
{
id: "sale-1",
model: "deepseek-v4-flash",
createdAt: "2026-08-28T11:00:00.000Z",
settlementStatus: "confirmed",
inputTokens: 10,
outputTokens: 2,
cacheReadTokens: 3,
revenueUsd: "0.000083",
vectorProof: {
status: "confirmed",
saleHash: "1".repeat(64),
batchRoot: "2".repeat(64),
txHash: "d".repeat(64),
leafIndex: 0,
siblings: ["3".repeat(64)],
},
},
{
id: "sale-2",
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();
});
describe("ResaleDashboard", () => {
it("matches the marketplace card system and renders both settlement markets", async () => {
describe("Capacity markets dashboard", () => {
it("renders the authenticated route with a Vector proof state for every sale", async () => {
const fetchMock = vi.fn(async () => Response.json(DASHBOARD));
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: "Vector marketplace" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "Vector proof" })).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(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",
`https://vector.apexscan.org/en/transaction/${"b".repeat(64)}/summary/`,
);
expect(screen.getByRole("link", { name: /0xcccc/ })).toHaveAttribute(
"href",
`https://basescan.org/tx/0x${"c".repeat(64)}`,
`https://vector.apexscan.org/en/transaction/${"d".repeat(64)}/summary/`,
);
expect(within(proofLink).getByText("dddddddddddd…ddddddddd")).toBeInTheDocument();
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
"/v1/resale-dashboard",
{ 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", () => {
it("authenticates and parses fragmented NDJSON discovery", async () => {
const first = discoveryRow("alpha", "vendor/alpha");
@@ -296,6 +332,90 @@ describe("SurplusClient", () => {
expect(earnings.recentSales[0].createdAt).toBe(
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 () => {

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();
});
});