Files
agents-marketplace/tests/unit/diag-submit.test.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

107 lines
5.2 KiB
TypeScript

/**
* Diagnostic: test Submit with small timestamps to isolate large-int issue
*/
import { describe, it, expect, vi } from "vitest";
import { buildSupplierWalletKey } from "../../tests/fixtures/supplier-side/wallet-keys.js";
import { buildBuyerWalletKey } from "../../tests/fixtures/buyer-side/wallet-keys.js";
import { LiveOgmiosProvider } from "../../packages/shared/src/chain/LiveOgmiosProvider.js";
import { buildSubmitTx } from "../../packages/shared/src/tx/escrow/submit.js";
import { encodeEscrowDatum } from "../../packages/shared/src/cbor/EscrowDatum.js";
import type { EscrowDatum } from "../../packages/shared/src/cbor/types.js";
import { SUPPLIER_PKH } from "../../tests/fixtures/supplier-side/wallet-keys.js";
import { BUYER_PKH } from "../../tests/fixtures/buyer-side/wallet-keys.js";
import { SLOT_CONFIG_NETWORK } from "../../packages/shared/node_modules/@lucid-evolution/lucid/dist/index.js";
const mockFetch = vi.fn();
function rpcOk<T>(result: T) {
return { ok: true, json: async () => ({ jsonrpc: "2.0", result }) };
}
function makeOgmiosUtxo(txId: string, index: number, address: string, lovelace: number, datumHex?: string) {
return { transaction: { id: txId }, index, address, value: { ada: { lovelace } }, datum: datumHex ?? null, datumHash: null, script: null };
}
function protocolParamsResponse() {
return rpcOk({
minFeeCoefficient: 44, minFeeConstant: { ada: { lovelace: 155381 } },
maxTransactionSize: { bytes: 16384 }, maxValueSize: { bytes: 5000 },
stakeCredentialDeposit: { ada: { lovelace: 2000000 } }, stakePoolDeposit: { ada: { lovelace: 500000000 } },
prices: { memory: "0.0577", steps: "0.0000721" },
maxExecutionUnitsPerTransaction: { memory: 14000000, cpu: 10000000000 },
coinsPerUtxoByte: { ada: { lovelace: 4310 } }, collateralPercentage: 150, maxCollateralInputs: 3,
plutusCostModels: { "plutus:v1": {}, "plutus:v2": {}, "plutus:v3": {} },
monetaryExpansion: "0.003", treasuryExpansion: "0.2",
minStakePoolCost: { ada: { lovelace: 340000000 } },
minFeeReferenceScripts: { base: 15 },
governanceActionDeposit: { ada: { lovelace: 100000000000 } },
delegateRepresentativeDeposit: { ada: { lovelace: 500000000 } },
});
}
describe("Diagnostic - small timestamps", () => {
// Vector zero time = 1_752_057_484_000 ms. We need deliver_by >> VECTOR_ZERO_TIME + current slot
// Use small deliver_by = VECTOR_ZERO_TIME + 100_000_000 (100_000 slots from genesis)
// so that validTo slot = positive small number
const VECTOR_ZERO_TIME = 1_752_057_484_000;
const SMALL_DELIVER_BY = VECTOR_ZERO_TIME + 100_000_000; // 100_000 slots from genesis
const SMALL_TIP_MS = VECTOR_ZERO_TIME + 50_000_000; // 50_000 slots from genesis, before deliver_by
it("Submit with small timestamps (within 100k slots of genesis)", async () => {
const slotCfg = SLOT_CONFIG_NETWORK["Mainnet"];
console.log("Slot config:", JSON.stringify(slotCfg));
const supplier = buildSupplierWalletKey();
const ESCROW_TX_HASH = "f".repeat(64);
// Create a minimal Claimed datum with small timestamps
const datum: EscrowDatum = {
buyer_pkh: BUYER_PKH,
supplier_pkh: SUPPLIER_PKH,
advert_ref: { txHash: "b".repeat(64), index: 0 },
capability_id: "llm.text.generate.v1",
request_spec_hash: "c".repeat(64),
prompt_hash: "d".repeat(64),
payment_lovelace: 2_000_000n,
buyer_bond_lovelace: 1_000_000n,
supplier_bond_lovelace: 1_000_000n,
deliver_by: SMALL_DELIVER_BY,
posted_at: SMALL_TIP_MS,
submitted_at: null,
result_receipt_hash: null,
state: "Claimed",
};
const claimedDatumHex = encodeEscrowDatum(datum);
mockFetch.mockReset();
vi.useFakeTimers();
vi.setSystemTime(SMALL_TIP_MS);
mockFetch.mockImplementation(async (_url: string, init?: RequestInit) => {
const body = JSON.parse((init?.body as string) ?? "{}");
const method: string = body.method ?? "";
if (method === "queryLedgerState/protocolParameters") return protocolParamsResponse();
if (method === "queryLedgerState/utxo") {
if ((body.params ?? {}).outputReferences) {
return rpcOk([makeOgmiosUtxo(ESCROW_TX_HASH, 1, "addr_test1wrqq9qqjzf3uh4w9hm0kqzrpvt60r4ryjp5rjf5epd3nptq7yscm6", 4_000_000, claimedDatumHex)]);
}
return rpcOk([makeOgmiosUtxo("c".repeat(64), 0, supplier.address, 100_000_000)]);
}
if (method === "queryNetwork/tip") {
const tipSlot = Math.floor((SMALL_DELIVER_BY - 30_000) / 1000);
return rpcOk({ slot: tipSlot, id: "a".repeat(64) });
}
if (method === "submitTransaction") return rpcOk({ transaction: { id: "d".repeat(64) } });
return rpcOk({});
});
try {
const chain = new LiveOgmiosProvider({ ogmiosUrl: "http://ogmios:1337", fetch: mockFetch });
const result = await buildSubmitTx({ chain, supplierKey: supplier, escrowRef: { txHash: ESCROW_TX_HASH, index: 1 }, receiptHash: "a".repeat(64) });
console.log("SUCCESS: txCborHex length=", result.txCborHex.length, "first bytes=", result.txCborHex.slice(0,4));
} catch (err) {
console.error("FAILED:", err instanceof Error ? err.message : String(err));
} finally {
vi.useRealTimers();
}
});
});