Files
agents-marketplace/tests/fixtures/supplier-side/escrow-datum-builders.ts
jelisaveta-talbot cbab81efbd M1-F: end-to-end LLM marketplace lifecycle on Vector testnet
Bonded-escrow marketplace for prompt → response between buyers and
local-LLM suppliers, settling on Vector (UTxO) via Aiken Plutus V3
validators. Lifecycle PostEscrow → Claim → Submit → Accept proven on
Vector testnet 2026-05-06; AcceptEscrow tx 125e3cfe…7dc100d distributes
payment + bonds per the validator's distribution rule.

Workspace layout (pnpm + TypeScript):
  contracts/marketplace/      Aiken validators (advert + escrow) + plutus.json
  packages/shared/            Tx-builders (lucid-evolution), CBOR codecs,
                              chain providers (mock / read-only / live Ogmios)
  supplier/                   OpenAI-compatible HTTP node (Ollama by default;
                              vLLM / llama.cpp / OpenRouter / OpenClaw all
                              swap-in via OLLAMA_URL); 202+poll chat handler
  buyer/                      Express + React SPA; server-side runs the full
                              chain lifecycle via /v1/submit-prompt and
                              /v1/accept; SPA never holds the buyer key
  indexer/                    Chain-follower (extends apex-dashboard pattern),
                              SSE event stream + REST queries
  indexer-ui/                 Operator dashboard (events + suppliers + escrow
                              lookup)
  tests/                      1098 vitest unit + integration tests (10 skipped
                              under M1-F-D-defer for UPLC test plumbing)
  deploy/testnet/             Per-service docker-compose for the Hetzner stack
  docs/EXECUTIVE_SUMMARY.*    Marp deck (PDF + PPTX) — 11 slides, mermaid
                              diagrams pre-rendered for portable export
  docs/CLOUDFLARE_WAF_BYPASS.md  CF Skip-rule runbook for /v1/* paths
  docs/ARCHITECTURE.md        canonical spec

Live demo (testnet, 2026-05-06):
  https://mp-buyers.vector.testnet.apexfusion.org      (buyer SPA)
  https://mp-suppliers.vector.testnet.apexfusion.org   (supplier API)
  https://mp-indexer.vector.testnet.apexfusion.org     (indexer + UI)
2026-05-06 13:27:48 +02:00

131 lines
4.3 KiB
TypeScript

/**
* Supplier-side EscrowDatum fixture builder — M0-B RED phase
*
* Constructs EscrowDatum INDEPENDENTLY from the spec (ARCHITECTURE.md §4.2).
* MUST NOT import from buyer-side builders or any shared fixture helper.
*
* Golden file consumer: Reads tests/fixtures/golden/escrow-datum-open.hex
* (produced by buyer-side), independently encodes the same datum, and asserts
* hex equality. Divergence indicates a spec interpretation error.
*/
import { Tag } from "cbor-x";
import { readFileSync } from "fs";
import type { EscrowDatum } from "../../../packages/shared/src/cbor/types.js";
// NOTE: Shared low-level CBOR encoder. Does NOT violate fixture isolation —
// see comment in advert-datum-builders.ts (same rationale).
import { encodePlutus } from "../../../packages/shared/src/cbor/plutus-encoder.js";
// ─── Helpers (no import from buyer-side) ────────────────────────────────────
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
function plutusTag(tagNumber: number, value: unknown): Tag {
return new (Tag as unknown as new (a: unknown, b: unknown) => Tag)(
tagNumber,
value,
);
}
function utf8Bytes(s: string): Uint8Array {
return new TextEncoder().encode(s);
}
function optionNone(): Tag {
return plutusTag(122, []);
}
const STATE_TAGS: Record<string, number> = {
Open: 121,
Claimed: 122,
Submitted: 123,
Accepted: 124,
Reclaimed: 125,
Released: 126,
};
// ─── Sample datum values — from ARCHITECTURE.md §4.2 ─────────────────────────
const BUYER_PKH = "1234567890abcdef1234567890abcdef1234567890abcdef12345678";
const SUPPLIER_PKH = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef01";
const ADVERT_TX_HASH = "b".repeat(64);
const ADVERT_INDEX = 0;
const CAPABILITY_ID = "llm.text.generate.v1";
const REQUEST_SPEC_HASH = "c".repeat(64);
const PROMPT_HASH = "d".repeat(64);
const PAYMENT_LOVELACE = 2_000_000n;
const BUYER_BOND = 1_000_000n;
const SUPPLIER_BOND = 1_000_000n;
const DELIVER_BY = 1745500060000;
const POSTED_AT = 1745500000000;
/**
* buildSampleEscrowDatum — returns the canonical Open escrow datum.
* Derived independently from ARCHITECTURE.md §4.2.
*/
export function buildSampleEscrowDatum(): EscrowDatum {
return {
buyer_pkh: BUYER_PKH,
supplier_pkh: SUPPLIER_PKH,
advert_ref: { txHash: ADVERT_TX_HASH, index: ADVERT_INDEX },
capability_id: CAPABILITY_ID,
request_spec_hash: REQUEST_SPEC_HASH,
prompt_hash: PROMPT_HASH,
payment_lovelace: PAYMENT_LOVELACE,
buyer_bond_lovelace: BUYER_BOND,
supplier_bond_lovelace: SUPPLIER_BOND,
deliver_by: DELIVER_BY,
posted_at: POSTED_AT,
submitted_at: null,
result_receipt_hash: null,
state: "Open",
};
}
/**
* encodeSampleEscrowDatumHex — CBOR-encodes the Open sample datum directly.
* Written independently from buyer-side. Both must produce identical hex.
*/
export function encodeSampleEscrowDatumHex(): string {
const datum = buildSampleEscrowDatum();
// Plutus integers must be CBOR major type 0/1; `number` values exceeding
// u32 (POSIX-ms timestamps) emit as float64 by cbor-x and are rejected by
// CML.PlutusData.from_cbor_hex. Coerce all int fields to BigInt. Matches
// the same fix in packages/shared/src/cbor/EscrowDatum.ts.
const cborDatum = plutusTag(121, [
hexToBytes(datum.buyer_pkh),
hexToBytes(datum.supplier_pkh),
plutusTag(121, [hexToBytes(datum.advert_ref.txHash), BigInt(datum.advert_ref.index)]),
utf8Bytes(datum.capability_id),
hexToBytes(datum.request_spec_hash),
hexToBytes(datum.prompt_hash),
datum.payment_lovelace,
datum.buyer_bond_lovelace,
datum.supplier_bond_lovelace,
BigInt(datum.deliver_by),
BigInt(datum.posted_at),
optionNone(),
optionNone(),
plutusTag(STATE_TAGS[datum.state], []),
]);
return bytesToHex(encodePlutus(cborDatum));
}
export function readGoldenEscrowDatum(goldenPath: string): string {
return readFileSync(goldenPath, "utf8").trim();
}