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)
201 lines
7.5 KiB
TypeScript
201 lines
7.5 KiB
TypeScript
/**
|
|
* lucid-slot-config-patch.test.ts — RED phase tests for M1-F-time-cleanup
|
|
*
|
|
* Asserts that createLucidContext (or a standalone applyVectorSlotConfig()
|
|
* helper Catherine will add) mutates SLOT_CONFIG_NETWORK.Mainnet to match
|
|
* Vector's actual genesis parameters:
|
|
* { zeroTime: 1752057484000, zeroSlot: 0, slotLength: 1000 }
|
|
*
|
|
* The patch must be applied BEFORE lucid uses the network's slot config for
|
|
* validity-range calculations, otherwise live CBOR tx builders produce wrong
|
|
* slot bounds that the chain validator rejects.
|
|
*
|
|
* Imported from @lucid-evolution/plutus (re-exported via @lucid-evolution/lucid):
|
|
* SLOT_CONFIG_NETWORK — mutable Record<Network, SlotConfig>
|
|
* unixTimeToEnclosingSlot — used to verify the patch is semantically correct
|
|
*
|
|
* Production constants:
|
|
* VECTOR_GENESIS_UNIX_MS = 1752057484 * 1000 = 1752057484000
|
|
* VECTOR_SLOT_LENGTH_MS = 1000
|
|
* VECTOR_ZERO_SLOT = 0
|
|
*
|
|
* M1-F-time-cleanup RED — all tests fail until Catherine implements applyVectorSlotConfig().
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import {
|
|
SLOT_CONFIG_NETWORK,
|
|
unixTimeToEnclosingSlot,
|
|
slotToBeginUnixTime,
|
|
} from "@lucid-evolution/lucid";
|
|
import {
|
|
OgmiosLucidProvider,
|
|
} from "../../packages/shared/src/chain/OgmiosLucidProvider.js";
|
|
import { createLucidContext } from "../../packages/shared/src/tx/internal/lucidContext.js";
|
|
import { buildBuyerWalletKey } from "../fixtures/buyer-side/wallet-keys.js";
|
|
|
|
// ─── Vector genesis constants ─────────────────────────────────────────────────
|
|
|
|
const VECTOR_GENESIS_UNIX_MS = 1_752_057_484_000; // 1752057484 seconds * 1000
|
|
const VECTOR_SLOT_LENGTH_MS = 1000;
|
|
const VECTOR_ZERO_SLOT = 0;
|
|
|
|
// ─── Mock fetch for protocol-params bootstrap ─────────────────────────────────
|
|
|
|
const mockFetch = vi.fn();
|
|
|
|
function makeProtocolParamsResponse() {
|
|
return {
|
|
ok: true,
|
|
json: async () => ({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
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 } },
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
mockFetch.mockReset();
|
|
mockFetch.mockResolvedValue(makeProtocolParamsResponse());
|
|
});
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
describe("applyVectorSlotConfig() — SLOT_CONFIG_NETWORK.Mainnet mutation", () => {
|
|
it("createLucidContext patches SLOT_CONFIG_NETWORK.Mainnet.zeroTime to Vector genesis ms", async () => {
|
|
// Arrange: SLOT_CONFIG_NETWORK.Mainnet may start with a different zeroTime.
|
|
// Calling createLucidContext must mutate it to the Vector genesis.
|
|
const provider = new OgmiosLucidProvider({
|
|
ogmiosUrl: "http://localhost:1337",
|
|
fetch: mockFetch,
|
|
});
|
|
const buyer = buildBuyerWalletKey();
|
|
|
|
// Act
|
|
await createLucidContext(provider, buyer, {
|
|
networkId: 1,
|
|
systemStartUnix: 1_752_057_484,
|
|
slotLengthMs: 1000,
|
|
}, { usePresetProtocolParameters: true });
|
|
|
|
// Assert
|
|
expect(SLOT_CONFIG_NETWORK["Mainnet"].zeroTime).toBe(VECTOR_GENESIS_UNIX_MS);
|
|
});
|
|
|
|
it("createLucidContext patches SLOT_CONFIG_NETWORK.Mainnet.zeroSlot to 0", async () => {
|
|
const provider = new OgmiosLucidProvider({
|
|
ogmiosUrl: "http://localhost:1337",
|
|
fetch: mockFetch,
|
|
});
|
|
const buyer = buildBuyerWalletKey();
|
|
|
|
await createLucidContext(provider, buyer, {
|
|
networkId: 1,
|
|
systemStartUnix: 1_752_057_484,
|
|
slotLengthMs: 1000,
|
|
}, { usePresetProtocolParameters: true });
|
|
|
|
expect(SLOT_CONFIG_NETWORK["Mainnet"].zeroSlot).toBe(VECTOR_ZERO_SLOT);
|
|
});
|
|
|
|
it("createLucidContext patches SLOT_CONFIG_NETWORK.Mainnet.slotLength to 1000", async () => {
|
|
const provider = new OgmiosLucidProvider({
|
|
ogmiosUrl: "http://localhost:1337",
|
|
fetch: mockFetch,
|
|
});
|
|
const buyer = buildBuyerWalletKey();
|
|
|
|
await createLucidContext(provider, buyer, {
|
|
networkId: 1,
|
|
systemStartUnix: 1_752_057_484,
|
|
slotLengthMs: 1000,
|
|
}, { usePresetProtocolParameters: true });
|
|
|
|
expect(SLOT_CONFIG_NETWORK["Mainnet"].slotLength).toBe(VECTOR_SLOT_LENGTH_MS);
|
|
});
|
|
|
|
it("patch is idempotent — calling createLucidContext twice does not corrupt SLOT_CONFIG", async () => {
|
|
const provider = new OgmiosLucidProvider({
|
|
ogmiosUrl: "http://localhost:1337",
|
|
fetch: mockFetch,
|
|
});
|
|
const buyer = buildBuyerWalletKey();
|
|
|
|
await createLucidContext(provider, buyer, {
|
|
networkId: 1,
|
|
systemStartUnix: 1_752_057_484,
|
|
slotLengthMs: 1000,
|
|
}, { usePresetProtocolParameters: true });
|
|
|
|
// Reset mock for second call
|
|
mockFetch.mockReset();
|
|
mockFetch.mockResolvedValue(makeProtocolParamsResponse());
|
|
|
|
await createLucidContext(provider, buyer, {
|
|
networkId: 1,
|
|
systemStartUnix: 1_752_057_484,
|
|
slotLengthMs: 1000,
|
|
}, { usePresetProtocolParameters: true });
|
|
|
|
// Values must still be correct after two calls
|
|
expect(SLOT_CONFIG_NETWORK["Mainnet"].zeroTime).toBe(VECTOR_GENESIS_UNIX_MS);
|
|
expect(SLOT_CONFIG_NETWORK["Mainnet"].zeroSlot).toBe(VECTOR_ZERO_SLOT);
|
|
expect(SLOT_CONFIG_NETWORK["Mainnet"].slotLength).toBe(VECTOR_SLOT_LENGTH_MS);
|
|
});
|
|
|
|
it("after patch: unixTimeToEnclosingSlot(VECTOR_GENESIS_UNIX_MS) returns slot 0", async () => {
|
|
const provider = new OgmiosLucidProvider({
|
|
ogmiosUrl: "http://localhost:1337",
|
|
fetch: mockFetch,
|
|
});
|
|
const buyer = buildBuyerWalletKey();
|
|
|
|
await createLucidContext(provider, buyer, {
|
|
networkId: 1,
|
|
systemStartUnix: 1_752_057_484,
|
|
slotLengthMs: 1000,
|
|
}, { usePresetProtocolParameters: true });
|
|
|
|
const slot = unixTimeToEnclosingSlot(VECTOR_GENESIS_UNIX_MS, SLOT_CONFIG_NETWORK["Mainnet"]);
|
|
expect(slot).toBe(0);
|
|
});
|
|
|
|
it("after patch: unixTimeToEnclosingSlot(VECTOR_GENESIS_MS + 25_000_000 * 1000) returns ~25_000_000", async () => {
|
|
// 25,000,000 slots after genesis at 1 slot/s = 25_000_000_000 ms later
|
|
const provider = new OgmiosLucidProvider({
|
|
ogmiosUrl: "http://localhost:1337",
|
|
fetch: mockFetch,
|
|
});
|
|
const buyer = buildBuyerWalletKey();
|
|
|
|
await createLucidContext(provider, buyer, {
|
|
networkId: 1,
|
|
systemStartUnix: 1_752_057_484,
|
|
slotLengthMs: 1000,
|
|
}, { usePresetProtocolParameters: true });
|
|
|
|
const targetUnixMs = VECTOR_GENESIS_UNIX_MS + 25_000_000 * VECTOR_SLOT_LENGTH_MS;
|
|
const slot = unixTimeToEnclosingSlot(targetUnixMs, SLOT_CONFIG_NETWORK["Mainnet"]);
|
|
expect(slot).toBe(25_000_000);
|
|
});
|
|
});
|