Files
agents-marketplace/contracts/marketplace/validators/escrow.ak
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

736 lines
25 KiB
Plaintext

//// EscrowScript — happy-path bonded escrow.
////
//// ARCHITECTURE.md §4.2 + §4.3. State machine:
////
//// Reclaim (after deliver_by)
//// ┌──────────────────────┐
//// ▼ │
//// ┌───────┐ │
//// │ Open │ ── Claim ──┐ │
//// └───────┘ ▼ │
//// │ ┌─────────┐│
//// │ │ Claimed │┘
//// │ └────┬────┘
//// └────► Reclaim │ Submit
//// (after deliver_by)
//// ▼
//// ┌───────────┐
//// │ Submitted │
//// └─────┬─────┘
//// Accept (buyer) │ Release (after window)
//// ▼ ▼
//// ┌──────────┐ ┌──────────┐
//// │ Accepted │ │ Released │
//// └──────────┘ └──────────┘
////
//// Terminal states: Accepted, Reclaimed, Released. The validator never
//// sees a transition out of a terminal state because there's nothing to
//// re-bind: the funds have left the script address. Each transition rule
//// is implemented as a small pure function below; the spend handler is
//// just a router.
////
//// `advert_ref` is a SPEC-LOCK field — for v1 the validator does not
//// dereference or verify it points at a real Active advert. The TS-side
//// tx builder (M1-B) is responsible for ensuring `advert_ref` is real and
//// pricing matches. We do, however, propagate `advert_ref` through every
//// state transition by requiring the new datum to copy it unchanged. That
//// way the spec-lock is preserved end-to-end on-chain.
use aiken/interval.{Finite, Interval, IntervalBound}
use aiken/primitive/bytearray
use cardano/address.{Address, Script, VerificationKey}
use cardano/assets.{Value}
use cardano/transaction.{
InlineDatum, Input, NoDatum, Output, OutputReference, Transaction, find_input,
}
use marketplace/common.{
accept_window_ms, after_or_at, before_or_at, lovelace_paid_to_pkh, signed_by,
unique_output_to_credential, upper_bound_of, value_equal,
}
use marketplace/types.{
Accept, Claim, Claimed, EscrowDatum, EscrowRedeemer, Open, Reclaim, Release,
Submit, Submitted,
}
validator escrow {
spend(
datum: Option<EscrowDatum>,
redeemer: EscrowRedeemer,
own_ref: OutputReference,
self: Transaction,
) {
expect Some(old_datum) = datum
when redeemer is {
Claim -> handle_claim(old_datum, own_ref, self)
Submit { receipt_hash } -> handle_submit(old_datum, own_ref, receipt_hash, self)
Accept -> handle_accept(old_datum, self)
Reclaim -> handle_reclaim(old_datum, self)
Release -> handle_release(old_datum, self)
}
}
}
// ─── Common: same-script-address rebind ──────────────────────────────
/// Resolve "the same script address as my own input" and locate the unique
/// continuing output. Returns (continuing_output, new_datum_parsed).
fn resolve_continuing(
own_ref: OutputReference,
self: Transaction,
) -> (Output, EscrowDatum) {
expect Some(own_input) = find_input(self.inputs, own_ref)
let own_credential = own_input.output.address.payment_credential
let continuing = unique_output_to_credential(self.outputs, own_credential)
expect new_datum: EscrowDatum = inline_datum_of(continuing)
(continuing, new_datum)
}
fn own_input_value(own_ref: OutputReference, self: Transaction) -> Value {
expect Some(own_input) = find_input(self.inputs, own_ref)
own_input.output.value
}
fn inline_datum_of(output: Output) -> Data {
when output.datum is {
InlineDatum(d) -> d
_ -> fail @"continuing escrow output must carry an inline datum"
}
}
// ─── Claim (Open → Claimed) ──────────────────────────────────────────
fn handle_claim(
old_datum: EscrowDatum,
own_ref: OutputReference,
self: Transaction,
) -> Bool {
let (continuing, new_datum) = resolve_continuing(own_ref, self)
let in_value = own_input_value(own_ref, self)
let signer_ok = signed_by(self.extra_signatories, old_datum.supplier_pkh)
let state_ok = old_datum.state == Open
let new_state_ok = new_datum.state == Claimed
let value_ok = value_equal(continuing.value, in_value)
let datum_unchanged_apart_from_state =
expect_unchanged_modulo_state(old_datum, new_datum)
let deadline_ok = before_or_at(self.validity_range, old_datum.deliver_by)
signer_ok && state_ok && new_state_ok && value_ok && datum_unchanged_apart_from_state && deadline_ok
}
// ─── Submit (Claimed → Submitted) ────────────────────────────────────
fn handle_submit(
old_datum: EscrowDatum,
own_ref: OutputReference,
receipt_hash: ByteArray,
self: Transaction,
) -> Bool {
let (continuing, new_datum) = resolve_continuing(own_ref, self)
let in_value = own_input_value(own_ref, self)
// Validity-range upper bound serves as the canonical submission stamp.
expect Some(stamp) = upper_bound_of(self.validity_range)
let signer_ok = signed_by(self.extra_signatories, old_datum.supplier_pkh)
let state_ok = old_datum.state == Claimed
let new_state_ok = new_datum.state == Submitted
let value_ok = value_equal(continuing.value, in_value)
let receipt_hash_ok =
new_datum.result_receipt_hash == Some(receipt_hash) && bytearray_len(
receipt_hash,
) == 32
let stamp_ok = new_datum.submitted_at == Some(stamp)
let deadline_ok = before_or_at(self.validity_range, old_datum.deliver_by)
let datum_invariants =
expect_unchanged_modulo_submit(old_datum, new_datum)
signer_ok && state_ok && new_state_ok && value_ok && receipt_hash_ok && stamp_ok && deadline_ok && datum_invariants
}
// ─── Accept (Submitted → Accepted, terminal) ─────────────────────────
fn handle_accept(old_datum: EscrowDatum, self: Transaction) -> Bool {
let signer_ok = signed_by(self.extra_signatories, old_datum.buyer_pkh)
let state_ok = old_datum.state == Submitted
// Distribution rule:
// supplier receives payment + supplier_bond
// buyer receives buyer_bond
let supplier_due = old_datum.payment_lovelace + old_datum.supplier_bond_lovelace
let buyer_due = old_datum.buyer_bond_lovelace
let supplier_received =
lovelace_paid_to_pkh(self.outputs, old_datum.supplier_pkh)
let buyer_received = lovelace_paid_to_pkh(self.outputs, old_datum.buyer_pkh)
let distribution_ok =
supplier_received >= supplier_due && buyer_received >= buyer_due
// Time rule: accept must occur within the window from submitted_at.
expect Some(submitted_at) = old_datum.submitted_at
let deadline = submitted_at + accept_window_ms
let window_ok = before_or_at(self.validity_range, deadline)
signer_ok && state_ok && distribution_ok && window_ok
}
// ─── Reclaim (Open|Claimed → Reclaimed, terminal) ────────────────────
fn handle_reclaim(old_datum: EscrowDatum, self: Transaction) -> Bool {
let signer_ok = signed_by(self.extra_signatories, old_datum.buyer_pkh)
let state_ok = old_datum.state == Open || old_datum.state == Claimed
// After deliver_by: validity-range lower bound must be at or after.
let deadline_ok = after_or_at(self.validity_range, old_datum.deliver_by)
// Distribution: buyer receives payment + buyer_bond + supplier_bond.
let buyer_due =
old_datum.payment_lovelace + old_datum.buyer_bond_lovelace + old_datum.supplier_bond_lovelace
let buyer_received = lovelace_paid_to_pkh(self.outputs, old_datum.buyer_pkh)
let distribution_ok = buyer_received >= buyer_due
signer_ok && state_ok && deadline_ok && distribution_ok
}
// ─── Release (Submitted → Released, terminal) ────────────────────────
fn handle_release(old_datum: EscrowDatum, self: Transaction) -> Bool {
let signer_ok = signed_by(self.extra_signatories, old_datum.supplier_pkh)
let state_ok = old_datum.state == Submitted
// After submitted_at + ACCEPT_WINDOW.
expect Some(submitted_at) = old_datum.submitted_at
let threshold = submitted_at + accept_window_ms
let timing_ok = after_or_at(self.validity_range, threshold)
// Distribution: supplier receives payment + supplier_bond + buyer_bond.
let supplier_due =
old_datum.payment_lovelace + old_datum.supplier_bond_lovelace + old_datum.buyer_bond_lovelace
let supplier_received =
lovelace_paid_to_pkh(self.outputs, old_datum.supplier_pkh)
let distribution_ok = supplier_received >= supplier_due
signer_ok && state_ok && timing_ok && distribution_ok
}
// ─── Datum invariants ────────────────────────────────────────────────
/// On Claim, every field except `state` must be unchanged. This forbids the
/// supplier from rewriting hashes, prices, or the advert spec-lock.
fn expect_unchanged_modulo_state(
old: EscrowDatum,
new: EscrowDatum,
) -> Bool {
new.buyer_pkh == old.buyer_pkh && new.supplier_pkh == old.supplier_pkh && new.advert_ref == old.advert_ref && new.capability_id == old.capability_id && new.request_spec_hash == old.request_spec_hash && new.prompt_hash == old.prompt_hash && new.payment_lovelace == old.payment_lovelace && new.buyer_bond_lovelace == old.buyer_bond_lovelace && new.supplier_bond_lovelace == old.supplier_bond_lovelace && new.deliver_by == old.deliver_by && new.posted_at == old.posted_at && new.submitted_at == old.submitted_at && new.result_receipt_hash == old.result_receipt_hash
}
/// On Submit, every field except `state`, `submitted_at`, and
/// `result_receipt_hash` must be unchanged. The first two of those three are
/// checked separately above; this guards the rest.
fn expect_unchanged_modulo_submit(
old: EscrowDatum,
new: EscrowDatum,
) -> Bool {
new.buyer_pkh == old.buyer_pkh && new.supplier_pkh == old.supplier_pkh && new.advert_ref == old.advert_ref && new.capability_id == old.capability_id && new.request_spec_hash == old.request_spec_hash && new.prompt_hash == old.prompt_hash && new.payment_lovelace == old.payment_lovelace && new.buyer_bond_lovelace == old.buyer_bond_lovelace && new.supplier_bond_lovelace == old.supplier_bond_lovelace && new.deliver_by == old.deliver_by && new.posted_at == old.posted_at
}
fn bytearray_len(b: ByteArray) -> Int {
bytearray.length(b)
}
// ─── Tests ───────────────────────────────────────────────────────────
//
// Test fixtures: buyer/supplier with distinct pkhs, a script hash for the
// escrow address, and a deterministic OutputReference for the advert spec
// lock. Time values use POSIX ms; we pick "deliver_by = 5_000_000_000" as
// a baseline and slot the validity range relative to it.
const buyer_pkh: ByteArray =
#"1111111111111111111111111111111111111111111111111111111111"
const supplier_pkh: ByteArray =
#"2222222222222222222222222222222222222222222222222222222222"
const stranger_pkh: ByteArray =
#"3333333333333333333333333333333333333333333333333333333333"
const escrow_script_hash: ByteArray =
#"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefab"
const advert_tx_hash: ByteArray =
#"baadbaadbaadbaadbaadbaadbaadbaadbaadbaadbaadbaadbaadbaadbaadbaad"
const request_hash: ByteArray =
#"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
const prompt_hash: ByteArray =
#"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
const receipt_hash_32: ByteArray =
#"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
const receipt_hash_short: ByteArray =
#"deadbeef"
const deliver_by_ms: Int = 5_000_000_000
const posted_at_ms: Int = 4_999_000_000
const submitted_at_ms: Int = 4_999_500_000
const payment_amt: Int = 5_000_000
const buyer_bond_amt: Int = 1_000_000
const supplier_bond_amt: Int = 1_000_000
fn locked_value() -> Value {
assets.from_lovelace(payment_amt + buyer_bond_amt + supplier_bond_amt)
}
fn escrow_address() -> Address {
Address {
payment_credential: Script(escrow_script_hash),
stake_credential: None,
}
}
fn pkh_address(pkh: ByteArray) -> Address {
Address {
payment_credential: VerificationKey(pkh),
stake_credential: None,
}
}
fn advert_ref() -> OutputReference {
OutputReference { transaction_id: advert_tx_hash, output_index: 0 }
}
fn placeholder_ref(ix: Int) -> OutputReference {
OutputReference {
transaction_id: #"0000000000000000000000000000000000000000000000000000000000000000",
output_index: ix,
}
}
fn open_datum() -> EscrowDatum {
EscrowDatum {
buyer_pkh,
supplier_pkh,
advert_ref: advert_ref(),
capability_id: "llm.text.generate.v1",
request_spec_hash: request_hash,
prompt_hash,
payment_lovelace: payment_amt,
buyer_bond_lovelace: buyer_bond_amt,
supplier_bond_lovelace: supplier_bond_amt,
deliver_by: deliver_by_ms,
posted_at: posted_at_ms,
submitted_at: None,
result_receipt_hash: None,
state: Open,
}
}
fn claimed_datum() -> EscrowDatum {
EscrowDatum { ..open_datum(), state: Claimed }
}
fn submitted_datum() -> EscrowDatum {
EscrowDatum {
..open_datum(),
submitted_at: Some(submitted_at_ms),
result_receipt_hash: Some(receipt_hash_32),
state: Submitted,
}
}
fn escrow_input(ref: OutputReference, datum: EscrowDatum) -> Input {
Input {
output_reference: ref,
output: Output {
address: escrow_address(),
value: locked_value(),
datum: InlineDatum(datum),
reference_script: None,
},
}
}
fn escrow_output(datum: EscrowDatum) -> Output {
Output {
address: escrow_address(),
value: locked_value(),
datum: InlineDatum(datum),
reference_script: None,
}
}
fn pkh_output(pkh: ByteArray, lovelace: Int) -> Output {
Output {
address: pkh_address(pkh),
value: assets.from_lovelace(lovelace),
datum: NoDatum,
reference_script: None,
}
}
/// Closed-interval validity range [lo, hi].
fn validity_between(lo: Int, hi: Int) -> Interval {
Interval {
lower_bound: IntervalBound { bound_type: Finite(lo), is_inclusive: True },
upper_bound: IntervalBound { bound_type: Finite(hi), is_inclusive: True },
}
}
fn base_tx() -> Transaction {
transaction.placeholder
}
// ─── Happy: Claim ─────────────────────────────────────────────────────
test claim_happy_path() {
let own_ref = placeholder_ref(0)
let old = open_datum()
let new = claimed_datum()
let t =
Transaction {
..base_tx(),
inputs: [escrow_input(own_ref, old)],
outputs: [escrow_output(new)],
extra_signatories: [supplier_pkh],
validity_range: validity_between(
deliver_by_ms - 1_000_000,
deliver_by_ms - 500_000,
),
}
handle_claim(old, own_ref, t)
}
// ─── Happy: Submit ────────────────────────────────────────────────────
test submit_happy_path() {
let own_ref = placeholder_ref(0)
let old = claimed_datum()
let stamp = deliver_by_ms - 500_000
let new =
EscrowDatum {
..old,
submitted_at: Some(stamp),
result_receipt_hash: Some(receipt_hash_32),
state: Submitted,
}
let t =
Transaction {
..base_tx(),
inputs: [escrow_input(own_ref, old)],
outputs: [escrow_output(new)],
extra_signatories: [supplier_pkh],
validity_range: validity_between(deliver_by_ms - 1_000_000, stamp),
}
handle_submit(old, own_ref, receipt_hash_32, t)
}
// ─── Happy: Accept ────────────────────────────────────────────────────
test accept_happy_path() {
let old = submitted_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(supplier_pkh, payment_amt + supplier_bond_amt),
pkh_output(buyer_pkh, buyer_bond_amt),
],
extra_signatories: [buyer_pkh],
validity_range: validity_between(
submitted_at_ms + 1,
submitted_at_ms + accept_window_ms,
),
}
handle_accept(old, t)
}
// ─── Happy: Reclaim from Open ─────────────────────────────────────────
test reclaim_from_open_happy_path() {
let old = open_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(buyer_pkh, payment_amt + buyer_bond_amt + supplier_bond_amt),
],
extra_signatories: [buyer_pkh],
validity_range: validity_between(deliver_by_ms, deliver_by_ms + 1_000_000),
}
handle_reclaim(old, t)
}
// ─── Happy: Reclaim from Claimed ──────────────────────────────────────
test reclaim_from_claimed_happy_path() {
let old = claimed_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(buyer_pkh, payment_amt + buyer_bond_amt + supplier_bond_amt),
],
extra_signatories: [buyer_pkh],
validity_range: validity_between(deliver_by_ms, deliver_by_ms + 1_000_000),
}
handle_reclaim(old, t)
}
// ─── Happy: Release after window ──────────────────────────────────────
test release_happy_path() {
let old = submitted_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(
supplier_pkh,
payment_amt + supplier_bond_amt + buyer_bond_amt,
),
],
extra_signatories: [supplier_pkh],
validity_range: validity_between(
submitted_at_ms + accept_window_ms,
submitted_at_ms + accept_window_ms + 1_000_000,
),
}
handle_release(old, t)
}
// ─── Adversarial: Claim ───────────────────────────────────────────────
test claim_rejects_non_supplier() fail {
let own_ref = placeholder_ref(0)
let old = open_datum()
let new = claimed_datum()
let t =
Transaction {
..base_tx(),
inputs: [escrow_input(own_ref, old)],
outputs: [escrow_output(new)],
extra_signatories: [stranger_pkh],
validity_range: validity_between(
deliver_by_ms - 1_000_000,
deliver_by_ms - 500_000,
),
}
expect handle_claim(old, own_ref, t)
True
}
test claim_rejects_after_deliver_by() fail {
let own_ref = placeholder_ref(0)
let old = open_datum()
let new = claimed_datum()
let t =
Transaction {
..base_tx(),
inputs: [escrow_input(own_ref, old)],
outputs: [escrow_output(new)],
extra_signatories: [supplier_pkh],
// Upper bound past deliver_by — must reject.
validity_range: validity_between(
deliver_by_ms - 1_000,
deliver_by_ms + 1_000,
),
}
expect handle_claim(old, own_ref, t)
True
}
// ─── Adversarial: Submit ──────────────────────────────────────────────
test submit_rejects_no_receipt_hash() fail {
let own_ref = placeholder_ref(0)
let old = claimed_datum()
let stamp = deliver_by_ms - 500_000
// New datum reaches state Submitted but result_receipt_hash is None.
let new =
EscrowDatum {
..old,
submitted_at: Some(stamp),
result_receipt_hash: None,
state: Submitted,
}
let t =
Transaction {
..base_tx(),
inputs: [escrow_input(own_ref, old)],
// Note: continuing output's datum has receipt = None so the rule
// `new.result_receipt_hash == Some(receipt_hash)` fails.
outputs: [escrow_output(new)],
extra_signatories: [supplier_pkh],
validity_range: validity_between(deliver_by_ms - 1_000_000, stamp),
}
expect handle_submit(old, own_ref, receipt_hash_32, t)
True
}
test submit_rejects_short_receipt_hash() fail {
let own_ref = placeholder_ref(0)
let old = claimed_datum()
let stamp = deliver_by_ms - 500_000
let new =
EscrowDatum {
..old,
submitted_at: Some(stamp),
result_receipt_hash: Some(receipt_hash_short),
state: Submitted,
}
let t =
Transaction {
..base_tx(),
inputs: [escrow_input(own_ref, old)],
outputs: [escrow_output(new)],
extra_signatories: [supplier_pkh],
validity_range: validity_between(deliver_by_ms - 1_000_000, stamp),
}
expect handle_submit(old, own_ref, receipt_hash_short, t)
True
}
test submit_rejects_double_submit() fail {
let own_ref = placeholder_ref(0)
// Old state is already Submitted — double-submit attempt.
let old = submitted_datum()
let stamp = deliver_by_ms - 500_000
let new =
EscrowDatum {
..old,
submitted_at: Some(stamp),
result_receipt_hash: Some(receipt_hash_32),
state: Submitted,
}
let t =
Transaction {
..base_tx(),
inputs: [escrow_input(own_ref, old)],
outputs: [escrow_output(new)],
extra_signatories: [supplier_pkh],
validity_range: validity_between(deliver_by_ms - 1_000_000, stamp),
}
expect handle_submit(old, own_ref, receipt_hash_32, t)
True
}
// ─── Adversarial: Accept ──────────────────────────────────────────────
test accept_rejects_non_buyer() fail {
let old = submitted_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(supplier_pkh, payment_amt + supplier_bond_amt),
pkh_output(buyer_pkh, buyer_bond_amt),
],
extra_signatories: [supplier_pkh],
validity_range: validity_between(
submitted_at_ms + 1,
submitted_at_ms + accept_window_ms,
),
}
expect handle_accept(old, t)
True
}
test accept_rejects_after_window() fail {
let old = submitted_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(supplier_pkh, payment_amt + supplier_bond_amt),
pkh_output(buyer_pkh, buyer_bond_amt),
],
extra_signatories: [buyer_pkh],
// Validity range strictly after the window — must reject.
validity_range: validity_between(
submitted_at_ms + accept_window_ms + 1,
submitted_at_ms + accept_window_ms + 1_000,
),
}
expect handle_accept(old, t)
True
}
test accept_rejects_supplier_shorted() fail {
let old = submitted_datum()
let t =
Transaction {
..base_tx(),
outputs: [
// Supplier short-paid by 1 lovelace.
pkh_output(supplier_pkh, payment_amt + supplier_bond_amt - 1),
pkh_output(buyer_pkh, buyer_bond_amt),
],
extra_signatories: [buyer_pkh],
validity_range: validity_between(
submitted_at_ms + 1,
submitted_at_ms + accept_window_ms,
),
}
expect handle_accept(old, t)
True
}
test accept_rejects_buyer_shorted() fail {
let old = submitted_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(supplier_pkh, payment_amt + supplier_bond_amt),
// Buyer short-paid by 1 lovelace.
pkh_output(buyer_pkh, buyer_bond_amt - 1),
],
extra_signatories: [buyer_pkh],
validity_range: validity_between(
submitted_at_ms + 1,
submitted_at_ms + accept_window_ms,
),
}
expect handle_accept(old, t)
True
}
// ─── Adversarial: Reclaim ─────────────────────────────────────────────
test reclaim_rejects_before_deliver_by() fail {
let old = open_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(buyer_pkh, payment_amt + buyer_bond_amt + supplier_bond_amt),
],
extra_signatories: [buyer_pkh],
validity_range: validity_between(
deliver_by_ms - 1_000_000,
deliver_by_ms - 1,
),
}
expect handle_reclaim(old, t)
True
}
// ─── Adversarial: Release ─────────────────────────────────────────────
test release_rejects_before_window() fail {
let old = submitted_datum()
let t =
Transaction {
..base_tx(),
outputs: [
pkh_output(
supplier_pkh,
payment_amt + supplier_bond_amt + buyer_bond_amt,
),
],
extra_signatories: [supplier_pkh],
validity_range: validity_between(
submitted_at_ms + 1,
submitted_at_ms + accept_window_ms - 1,
),
}
expect handle_release(old, t)
True
}