Commit Graph

25 Commits

Author SHA1 Message Date
FilipBl4gojevic
069edcb076 feat(ocr): model-scoped OCR capability + gateway /v1/ocr/extract route
Adds the ocr.page.extract.chandra-ocr-2.v1 capability end to end: the
postOcrEscrow builder (locking the min-ada floor for the Submitted-state
datum, required since the min-ada fix), the gateway OCR route that selects
an OCR supplier by capability, posts a per-page escrow, calls the supplier,
verifies and settles, and returns content plus the signed receipt labelled
first-party-brokered. Supplier OCR handler with a hosted document-API
upstream and buyer-SDK OCR support included.

Squashed from the 2026-08-06 working subset; the deploy pack is
intentionally excluded (it lives in a private operator repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-10 16:40:00 +02:00
dcejtlin-apex
d5349057b0 Merge pull request #9 from Apex-Fusion/fix/escrow-minada-floor
fix(escrow): min-ada floor so long-capability Submits settle
2026-08-10 15:08:11 +02:00
FilipBl4gojevic
c391b98af8 fix(escrow): lock min-ada for the largest future datum so long-capability Submits settle
Some checks failed
CI / typecheck + vitest (Node 20) (push) Has been cancelled
The escrow locks payment + both bonds and the validator requires exact
value preservation (value_equal) on Claim and Submit. Submit grows the
inline datum ~43 bytes (submitted_at None->Some, result_receipt_hash
None->Some 32 bytes). When capability_id is long enough, the
Submitted-state output's min-ada exceeds the locked total; lucid bumps
the continuing output, value_equal fails, and the validator errors with
no traces (opaque evaluateTransaction 3010/3012). Measured on mainnet at
utxoCostPerByte 4310: a 20-byte chat capability clears a 2.2M lock by
49,310, a 33-byte OCR capability is over by 11,030 - so short-capability
escrows settle and long ones never can, and because Claim's datum is
smaller it always passes, making a Submit-only failure look chain-side.

escrowLockFloor() computes min-ada for the Submitted state plus a margin
and locks max(economic total, that floor) in the post builders. Buyer-
side only, no contract or supplier changes; lucid never bumps once the
lock is sufficient, and the surplus returns through the >= terminal
distribution. postOcrEscrow's identical change ships with the OCR
feature branch. New unit test covers the floor across capability lengths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 14:22:02 +02:00
FilipBl4gojevic
36c5cfc6be fix(wallet): make wallet-health idempotent - structural health predicate + circuit breakers
Some checks failed
CI / typecheck + vitest (Node 20) (push) Has been cancelled
planConsolidate returned already-healthy only for wallets with EXACTLY
2 UTxOs, but buildConsolidateWalletTx lands 3 (collateral + working +
the fee-reserve remainder as change). The shape consolidation produces
can never satisfy its own health check, so every wallet-health tick
re-consolidates and pays the ~0.5 AP3X min fee again. Measured in a
production deployment: a 45s ticker submitted 232 consolidations and
burned ~116 AP3X in about 2.5 idle hours; at the default 600s interval
the same loop leaks ~3 AP3X per hour whenever the wallet is not exactly
2 UTxOs.

Healthy is now structural: a pure-lovelace UTxO >= collateralLovelace
exists, at least 1 AP3X sits outside it, and fragmentation is bounded
(<= 6 UTxOs, new optional maxUtxos param). The health check also runs
BEFORE the low-balance guard, so a wallet that can already run script
transactions is never reshaped or refused. walletHealth gains two
independent circuit breakers - halt after 3 consecutive consolidations
that never reach healthy, halt past 6 consolidations per 24h - so any
future non-convergence costs at most ~1.5 AP3X instead of the whole
wallet, plus a docstring that matches reality.

Tests: the exact post-consolidation-shape regression, one-step
idempotence against the builder output model, candidate boundaries
(native assets, 1,628-lovelace-short), healthy-below-reshape-threshold,
and breaker behavior including error and reset paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 12:10:45 +02:00
david
9a4f9f0699 fix(gateway,supplier): demo sessions stick to their supplier; free pinned suppliers
The demo endpoint lost its session on nearly every second message: affinity
required clients to re-echo the assistant reply byte-identically, so clients
that strip <think> blocks or reshape whitespace (Open WebUI, litellm) missed
every turn, opened a fresh escrow session on the next free single-slot
supplier, and left the old one pinned. Open WebUI's background title/tag-gen
requests each pinned another supplier — one message could exhaust all three
kimi replicas ("no available chat supplier").

- affinity: lenient assistant-content matching (think-strip + whitespace
  collapse); user/system/tool messages and tool_calls stay byte-strict;
  strict prefixes outrank lenient ones. The mirror is never rewritten with
  client text, so receipt hashes stay valid.
- demoChat: trim trailing assistant/system messages (regenerate semantics)
  instead of 400 "the last message must have role user or tool".
- supplier: roll the transcript back when the LLM call fails so retries
  don't duplicate the delta (ChatSessionStore.truncateTranscript).
- demoChat: LRU-evict a demo session when no supplier is available (supplier
  /v1/chat/end frees the slot, sweeper Accepts the escrow) and retry the
  open with selectCandidates ignoreStatusFor to bypass the ~20s-stale
  indexer status poll. Max 2 evictions per request.
- sweeper: idle janitor closes demo sessions unused for DEMO_SESSION_IDLE_MS
  (default 3 min, below the supplier's 5 min CHAT_IDLE_TIMEOUT_MS) so
  abandoned one-off sessions release their suppliers quickly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 02:29:10 +02:00
david
4ebe7c9683 feat(gateway,supplier,shared): shared demo key with session-backed OpenAI endpoint + tool-calling
MVP demo: one operator-funded API key anyone can plug into an IDE/harness,
running real escrow-per-session marketplace conversations (llm.chat.v1)
behind the standard OpenAI /chat/completions wire shape.

- shared: ChatMessage gains tool role + tool_calls; normalizeChatMessage/
  chatMessagesEquivalent keep supplier transcript and gateway mirror
  hash-identical for receipts (field presence is part of the identity)
- supplier: /v1/chat/message accepts {messages,tools,tool_choice} alongside
  legacy {content}; callOpenAiStream forwards tools, accumulates fragmented
  delta.tool_calls, allows content-less tool turns; done frame carries the
  assistant message verbatim + finish_reason + usage
- gateway: demo flag on api_keys (additive migration), create-demo-key CLI,
  withdraw 403 + hidden deposit address for demo keys, per-IP demo rate
  limit (demo keys skip the shared per-key limit), models filtered to chat
  capability
- gateway executor (demoChat.ts): transcript-prefix affinity maps stateless
  calls onto open sessions, forwards only unseen deltas, transparently
  reopens on supplier idle-close; never falls back to one-shot (its settle
  path assumes <=1 in-flight escrow per wallet)
- mutex scope: key mutex held only for chain work (open/close); off-chain
  turns serialize on per-session locks so replicas serve users concurrently
- sweeper: chat-aware reclaim guard (35min vs 11min) and abandoned-session
  cleanup; terminal state confirmed via on-chain queryUtxo (indexer keeps
  lineage rows at their old state forever)
- deploy: kimi-chat replicas 2+3 compose files; chat idle timeout 2.5min

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:30:17 +02:00
david
f4f35866f5 fix(escrow): reserve pure-ADA collateral UTxO in PostEscrow coin selection
buildLiveTxForEscrow called .complete() with no presetWalletInputs, so lucid was
free to spend the dedicated 5-ADA collateral UTxO as an ordinary input. On
back-to-back requests this left the wallet with no collateral candidate and the
subsequent Accept failed with collateral_required, stranding the escrow.

Reserve the smallest pure-ADA UTxO >= 5 ADA and fund PostEscrow from the rest
(falls back to default selection for single/large-UTxO cases, which keep an
ample pure-ADA change UTxO anyway). Benefits the buyer too (same shared path).
Adds reserveCollateralInputs unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:05:08 +02:00
david
6de62b8cf0 feat(gateway): OpenAI-compatible custodial gateway
New multi-tenant gateway package: external developers point any OpenAI SDK at
the marketplace. Each API key is its own custodial buyer wallet (AES-256-GCM
sealed under GATEWAY_MASTER_KEY); every request posts an on-chain escrow from
that user's wallet.

- POST /openai/v1/chat/completions (one escrow/call, llm.text.generate.v1,
  buffered + pseudo-stream), chat sessions (one escrow/session, llm.chat.v1,
  real SSE), GET /openai/v1/models, /signup, /account, /account/withdraw.
- Settles by resolving the Submitted ref via the indexer + awaited Accept;
  state-aware reclaim sweeper + per-wallet auto-consolidation for collateral.
- Reuses the buyer Marketplace SDK via new buyer exports ./sdk; new shared
  buildWithdrawTx (custodial exit). Runs via tsx; Dockerfile + mainnet compose.

Design in docs/gateway.md. Typecheck passes for all packages; 20 vitest tests
pass. Not yet exercised against a live chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:22:58 +02:00
david
9acad5b2b8 feat: llm.chat.v1 multi-turn supplier type + free Kimi K2.6 chat demo
Some checks failed
CI / typecheck + vitest (Node 20) (push) Has been cancelled
Adds a conversational supplier capability and a free demo, both streaming.

New paid `llm.chat.v1` type: one escrow per conversation (flat fee). "Start
chat" posts the escrow with a session-init placeholder prompt_hash and the
supplier Claims (reserving its single slot); turns then stream off-chain via
SSE with zero chain interaction; "End chat" has the supplier Submit a receipt
over the full transcript and the buyer Accept — the moment the user is charged.
The on-chain validator is content-agnostic (pins but never verifies the
hashes), so this reuses the existing contract unchanged. A supplier-side idle
watchdog auto-ends abandoned sessions before deliver_by.

Free Kimi K2.6 chat demo: a synthetic "demo:kimi-chat" tile that uses the same
chat UI but proxies straight to OpenRouter (no escrow, no payment), mirroring
the existing Piper demo.

Shared:
- postChatEscrow.ts + chatSessionPromptHash() (session-init prompt_hash helper
  shared by the buyer builder and the supplier start gate).

Supplier:
- CAPABILITY_KIND="chat-session" (+ CHAT_IDLE_TIMEOUT_MS); callOpenAiStream()
  streaming client; ChatSessionStore + endChatSession(); /v1/chat/{start,
  message,end} handlers with idle + hard-cap watchdog timers.

Buyer:
- Marketplace.startChat()/endChat() with relaxed (session-init) receipt
  verification + crypto verifyReceipt; /v1/chat/{start,message,end} routes
  (SSE passthrough) and a free /v1/chat-demo/message OpenRouter proxy;
  OPENROUTER_API_KEY/OPENROUTER_BASE_URL config; ChatForm.tsx + Dashboard
  KIMI_DEMO_SUPPLIER tile and llm.chat.v1 dispatch.

Deploy:
- docker-compose.supplier-kimi-chat.yml + supplier/.env.kimi-chat.example.

Typecheck passes across all workspaces; full vitest suite green except 15
pre-existing time-bomb/RED failures (tx-claim/accept/post-advert live-CBOR
fixtures with hardcoded past deadlines + the M0-C golden-datum test), confirmed
identical on a clean tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:18:54 +02:00
david
972c0f9b89 wallet: tx:consolidate-wallet CLI + collateral pre-check across script spends
Adds a one-shot operational tx that re-shapes any operator wallet to
{5 AP3X collateral candidate, working remainder} so subsequent Plutus
script-spends (Claim/Submit/Accept/Reclaim/RetireAdvert) find a UTxO
that satisfies lucid's setCollateral=5_000_000n default coin selector.

Symptom this addresses: open-webui mainnet supplier wallet held 8 UTxOs
max 2.40 AP3X, every Accept tx failed at lucid .complete() with the
"wallet must hold a pure-ADA UTxO ≥ 5 ADA" error path. Same fragmentation
risk on the other 4 mainnet operator wallets (one big UTxO surrounded by
dust — first script-spend consumes the big one and leaves nothing usable
as collateral).

Phase 1 — new tx:consolidate-wallet CLI (supplier + buyer):
  - planConsolidate.ts — pure planner; supports consolidate (N>1 → 2),
    split (1 → 2), and already-healthy skip.
  - consolidateWallet.ts — live builder; presetWalletInputs pins all
    UTxOs, two pay.ToAddress outputs (collateral + working).
  - runConsolidateWallet.ts — orchestration (build + submit + awaitTx).
  - supplier/src/cli/consolidate-wallet.ts + buyer mirror.
  - monitor-wallets.ts — add supplier-tts@vector-marketplace (was
    misclassified as 3rd-party).

Phase 2 — defensive pre-check in the 5 script-spend builders:
  - assertCollateralCandidate(utxos) fires at the top of
    buildSpendOpenOrClaimedTx (Claim/Submit), buildLiveTxForAccept,
    buildLiveTxForReclaim, buildLiveTxForRetireAdvert. Throws a
    TxConstructionError("collateral_required") pointing at
    tx:consolidate-wallet instead of waiting for lucid's deep failure.

Tests: 12 new passing (planConsolidate edge cases + assert helper).
Pre-existing lucid 0.4.30 WASM "unreachable" failures in tx-*-live.test.ts
fixtures are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:59:11 +02:00
david
4f1ec9aa28 shared/tx: bump live-tx setMinFee floor 200K → 500K (all builders)
The comment at every setMinFee call site said "500K is a comfortable
floor" but the actual value was 200K — the floor wasn't high enough to
cover lucid 0.4.30's Conway-era fee underestimation. Smoke-tested
mainnet Accept tx hit Ogmios code 3122: lucid declared 343,132 lovelace
fee but Ogmios required 347,726 (off by ~5K). With the floor at 200K
the floor was inert (343K > 200K), so the underestimated fee stood.

Bumping all six builder paths (PostEscrow, Claim/Submit, Accept,
Reclaim, PostAdvert, UpdateAdvert/RetireAdvert) to 500K — comfortably
above the empirical minimum across operations, ~0.15 AP3X surcharge
per tx, excess becomes change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:56:37 +02:00
david
f344d1475d shared+buyer/cli: lazy-read VECTOR_ZERO_TIME_MS; CLI populates process.env
lucidContext.ts captured VECTOR_ZERO_TIME at module-load time, so CLIs
that read a .env file at runtime (rather than letting Docker env_file
populate process.env before the runtime starts) got the testnet default
and built txs with a wrong slot origin -- Ogmios rejected with code
3118 "outside validity interval" on mainnet.

Two fixes:
- lucidContext.ts: read process.env.VECTOR_ZERO_TIME_MS inside
  applyVectorSlotConfig() so the defensive re-apply in createLucidContext
  picks up env vars set after module-load.
- run-cycle.ts / reclaim-orphans.ts: inject VECTOR_ZERO_TIME_MS,
  OGMIOS_URL, INDEXER_URL, NETWORK_ID into process.env right after
  reading the --env file, before any tx work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:37:58 +02:00
david
5734d9c333 shared: bump publish-refs setMinFee floor 200K → 500K
Lucid's auto-fee calculation under-estimates the publish-refs tx by
~5K lovelace on Vector L2 mainnet (observed: required 372,658, lucid
computed 367,197 → ogmios rejected with code 3122 InsufficientFee).

Bumping the floor to 500K guarantees lucid pays enough. Cost: 0.13 AP3X
overhead, one-shot. Doesn't affect ongoing script-spending txs (those
have their own setMinFee already at 200K, unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:51:41 +02:00
david
aecf4861bc shared+supplier: per-script lovelace overrides for ref-script publish
Splits the single `lovelacePerOutput` param in publishReferenceScripts.ts
into `escrowLovelace` and `advertLovelace`, exposed via env vars
ESCROW_PUBLISH_LOVELACE / ADVERT_PUBLISH_LOVELACE in the CLI. Defaults
unchanged at 30 AP3X each (60 AP3X total).

Motivation: mainnet rollout where the publishing wallet has only ~57 AP3X
(buyer wallet) — needs to publish at calculated min-utxo (~17 AP3X escrow
+ ~7 AP3X advert = 24 AP3X total) instead of the safe 60 AP3X default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:47:10 +02:00
david
1c852f3a68 shared: drop tx setMinFee floor 500K → 200K (unmask CIP-33 fee savings)
The 500K lovelace floor was set higher than the natural fee for both
inlined (~340K) and ref-script (~242K) Plutus V3 spend txs. With it
in place, every script-spending tx pays the same 0.5 AP3X regardless
of whether scripts are inlined or referenced — masking the on-chain
size reduction of ~3.4 KB / -82% per Claim/Submit/Accept measured
end-to-end on Vector L2 testnet.

200K is below both natural fees (so lucid's auto-estimate wins) and
still ~5x above the lucid-evolution 0.4.30 ~5K under-estimate the
floor was originally guarding against.

Expected per-cycle saving on Vector L2 testnet: ~0.32 AP3X
(Claim: 0.34 → 0.24, Submit: 0.34 → 0.25, Accept: 0.35 → 0.23).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:51:22 +02:00
david
3792509892 shared: fix OgmiosLucidProvider scriptRef extraction (Ogmios v6 shape)
ogmiosUtxoToLucidUtxo expected `o.script` to be a hex string, but
Ogmios v6 returns it as `{ language: "plutus:v3", cbor: "<hex>" }`
(or "native"). The typeof === "string" guard never matched, so
scriptRef was always null on returned UTxOs — making
.readFrom([refUtxo]) silently produce a tx with no script witness
and the supplier reject the resulting Claim with 503.

Wired ogmiosScriptToLucidScript() to translate the Ogmios shape into
the lucid-evolution shape across all four script languages.

Verified end-to-end: tmp-repro now reports
"scriptRef: present type=PlutusV3 cborlen=6884" against the published
ref UTxO (250928de…#0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:28:36 +02:00
david
57e77e0e25 shared: route ref-script server helpers via tx/server (keep buyer bundle clean)
Static re-exports from tx/index.ts caused vite to trace
loadEscrowScript/loadAdvertScript/buildLiveTxForPublishReferenceScripts
into the buyer UI bundle, hitting the CML WASM "ESM integration proposal"
limitation. Sibling builders avoid this with /* @vite-ignore */ dynamic
imports; we keep symmetry by parking the helpers on a separate
@marketplace/shared/tx/server entry that the buyer never imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:41:29 +02:00
david
5cae0c4f6c shared: opt-in CIP-33 reference-scripts for script-spending txs
When ESCROW_REF_UTXO / ADVERT_REF_UTXO env vars are set (format
<txhash>#<idx>), Claim/Submit/Accept/Reclaim/RetireAdvert call
.readFrom([refUtxo]) instead of inlining the validator CBOR via
.attach.SpendingValidator(). Lucid-evolution 0.4.30 auto-extracts
utxo.scriptRef into the script-witness map (dist/index.js:1903–1913),
so no separate attach call is needed. Default (env unset) is unchanged.

Adds buildLiveTxForPublishReferenceScripts (a one-shot tx that
publishes both validators as scriptRef-bearing UTxOs at a caller-
supplied address — typically the all-zero-pkh enterprise burn address)
and the supplier tx:publish-reference-scripts CLI that wraps it.

Targeted at testnet trial; mainnet rollout is a separate flip after
verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:25:59 +02:00
david
dcab0ba5b5 deploy: mainnet bring-up patches (env-override genesis + host/network)
shared: VECTOR_ZERO_TIME now reads VECTOR_ZERO_TIME_MS from env, defaults
to testnet genesis (1_752_057_484_000 ms). Mainnet deploys set
VECTOR_ZERO_TIME_MS=1756485600000. Existing testnet deployments unaffected
since env unset → testnet default. Patched in both lucidContext.ts (lucid
slot config override) and liveCbor.ts buildLiveTxForSubmit (submitted_at
slot-alignment).

deploy/mainnet: buyer host changed mp.vector.* → marketplace.vector.* per
operator-owned DNS. Indexer compose drops apex-net network (we point at
the hosted mainnet ogmios over public WSS, not via apex-dashboard-mainnet
which is not deployed on this box).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:32:26 +02:00
david
c09245a507 shared: live-CBOR for deferred builders (Reclaim, RetireAdvert)
Closes 2/4 builders from the M1-F-D-defer list (postAdvert already live in
7ea7328; updateAdvert and release remain).

Reclaim:
- buildLiveTxForReclaim in liveCbor.ts (mirrors buildLiveTxForAccept):
  spends the escrow UTxO with REDEEMER_RECLAIM (Constr3), pays
  payment + buyer_bond + supplier_bond to buyer, delegates UPLC eval
  to Ogmios via localUPLCEval:false. Validity range = tipMs ± 60s.
- reclaim.ts: replace the unconditional mockSlotToWallclockMs tipMs
  computation with `isLive ? Date.now() : mockSlotToWallclockMs(tipSlot)`,
  matching accept.ts:77. Adds the live-CBOR dispatch branch.

Funds-at-risk before this fix: with the broken tipMs path, every live
Reclaim was rejected client-side ("tip < deliver_by" using a Nov-1970
tip). If a supplier never Claimed, buyer's 6-7 AP3X stayed locked
indefinitely. Verified recovery of 4 stuck escrows on Vector L2 testnet
(txs f6660e52, 769f16fd, 79970d86, 9f4db14b — 14 AP3X total recovered).

RetireAdvert:
- buildLiveTxForRetireAdvert in liveCbor.ts: spends advert UTxO with
  REDEEMER_RETIRE_ADVERT (Constr2), advert script address.
- retireAdvert.ts: live-CBOR dispatch.
- supplier CLI: retire-advert.ts + retireAdvertFlow.ts.
- supplier/package.json: tx:retire-advert script.

Indexer:
- follower/blockProcessor + worker: handle RetireAdvert events,
  transition advert row state to Retired.
- db/cache.ts: markAdvertTerminal helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:57:51 +02:00
jelisaveta-talbot
0b93aee1ec test: clarify M1-F-D-defer status + add LIVECBOR_DEBUG env
While checking whether last night's localUPLCEval: false fix unblocks
the 5 tx-submit-live tests skipped under TODO(M1-F-D-defer), found
that those tests still crash — but at a DIFFERENT spot than before.

Original UPLC issue (now fixed in production via 7ea7328 + fd24598):
- lucid's CML WASM UPLC interpreter falsely rejected our Plutus V3
  Submit/Claim/Accept txs. End-to-end chat + TTS lifecycle now works
  on Vector testnet by routing eval to Ogmios.

New / remaining test-side issue:
- Even with a proper evaluateTransaction mock response, lucid's
  applyUPLCEvalProvider → CML TransactionBuilder.set_exunits still
  panics with wasm "unreachable" inside the test fixture. The
  production path doesn't hit this (real Ogmios + real wallet UTxOs).
  Whatever the test-mock + CML interaction trips, it's separate from
  the original UPLC bug.

Refinements landed
- liveCbor.ts: opt-in LIVECBOR_DEBUG=1 env that prints the full inner
  err + WASM stack from rethrowAsTxError, so the next investigation
  doesn't need to re-add this scaffolding.
- tx-submit-live.test.ts:
  - setupSubmitHappyPath() now mocks "evaluateTransaction" with a
    proper EvalRedeemer-shaped response (useful baseline for any
    future un-skip attempt; not enough on its own to make the tests
    pass).
  - The first skipped test's TODO comment updated to record what's
    fixed in production vs. what remains as a test-infra bug.

Test counts unchanged (3 pass, 5 still skipped); production path
still verified end-to-end on testnet. Task M1-F-D-defer stays pending
but the original UPLC-interpreter blocker is gone.
2026-05-11 09:09:49 +02:00
jelisaveta-talbot
fd24598186 shared: also delegate UPLC eval on Accept tx
buildLiveTxForAccept has its own .complete() call separate from
buildSpendOpenOrClaimedTx (which already routed eval to Ogmios in
7ea7328). The same lucid 0.4.30 CML WASM false-rejection bit Accept
on every escrow — confirmed by 502s on three different pending
receipts in the SPA after the chat/TTS Submit fix landed.

Adding `localUPLCEval: false` to Accept's .complete() options closes
the gap; verified end-to-end against Vector testnet (Accept tx
0385442fd7ef72743194f8fa0b468c2c56bacb90b023f86ec2c3df22b18b7ca8
confirmed under the prior workaround; subsequent Accepts on the
patched build also confirm cleanly).
2026-05-07 21:48:47 +02:00
jelisaveta-talbot
7ea7328eed shared: live PostAdvert + delegate UPLC eval to Ogmios
Two production-essential fixes that surfaced during the path-A TTS
deployment to Vector testnet:

1. localUPLCEval: false on every Plutus V3 spend
   ──────────────────────────────────────────────
   lucid-evolution 0.4.30's CML WASM UPLC evaluator has a false-rejection
   bug for our V3 spend pattern: it rejects perfectly valid txs with
   "failed script execution Spend[0] the validator crashed / exited
   prematurely" while the chain itself accepts them without complaint.
   Routing eval to Ogmios's evaluateTransaction (cardano-node's
   evaluator) sidesteps the WASM bug and returns correct ex-units.
   OgmiosLucidProvider.evaluateTx already implements this path.

   This also unblocks the 5 tx-submit-live tests previously skipped
   with the comment "UPLC wasm crashes" (M1-F-D-defer / task #28).

   Without this fix, every Claim/Submit/Accept tx from our supplier
   was failing locally before reaching the chain. The third-party
   summitstak.ing supplier worked because it uses a different
   toolchain that does not run client-side phase-2 evaluation.

2. AdvertDatum BigInt coercion
   ────────────────────────────
   cbor-x emits float64 (`fb` prefix) for JS Number values that can't
   fit CBOR's compact int forms (e.g. POSIX-ms timestamps). Plutus
   Data has no float type, so CML.PlutusData.from_cbor_hex panics
   with "RuntimeError: unreachable" when it sees one. Coerce
   max_output_tokens, max_processing_ms, advertised_at to BigInt
   before encoding so cbor-x always emits major-type-0/1 integers.
   Mirrors the intAsBig pattern already used in EscrowDatum.

3. Live-CBOR PostAdvert builder
   ─────────────────────────────
   M1-F-4 deferred postAdvert/updateAdvert/retireAdvert/reclaim/
   release on the live path. With (1) and (2) in place, posting an
   advert via tx:post-advert against Vector testnet now works.
   Added buildLiveTxForAdvert in liveCbor.ts (mirrors PostEscrow)
   and routed buildPostAdvertTx through it when chain is live.

40 advert/post-advert tests still pass. Verified end-to-end on
Vector testnet: TTS advert posted at
5df793ac12dc426fae7a7a01d56d3e923d3985103d62f0182d9ba63dda871f96#0,
full chat + TTS marketplace lifecycle (PostEscrow → Claim → Submit →
Accept) confirmed completing successfully on chain.
2026-05-07 05:17:19 +02:00
jelisaveta-talbot
34a52d5d16 buyer: TTS marketplace lifecycle (audio.synthesize.piper.v1)
Path-A buyer side: PostEscrow → supplier /v1/audio/synthesize → 202+poll
→ on-chain Submit → receipt verify, returning the audio bytes + signed
receipt to the SPA. Pairs with the supplier-side scaffolding in 3c3e009.

Files

- packages/shared/src/tx/escrow/postTtsEscrow.ts (new):
  buildPostTtsEscrowTx() mirrors postEscrow.ts but commits a TTS-shaped
  prompt_hash = sha256(canonical({text, voice, format, speed})). The
  same hash is exposed as ttsPromptHash() so the buyer SDK and supplier
  validation can pin the canonicalisation; any drift fails the
  supplier's prompt_mismatch gate. Validates body shape (text non-empty,
  voice/format in allow-list, speed ∈ [0.5,1.5]); reuses the chat live-
  cbor path which is already capability-agnostic.

- packages/shared/src/tx/index.ts: export the new TTS surface.

- buyer/src/sdk/Marketplace.ts: + submitTts() — full lifecycle mirror of
  submitPrompt with TTS-shaped POST body, /v1/audio/synthesize endpoint,
  and audio_b64 in the polled response. Receipt verification gates on
  prompt_hash via ttsPromptHash() instead of canonicalize(messages).

- buyer/src/sdk/types.ts: + SubmitTtsOptions / SubmitTtsResult.

- buyer/src/server.ts: + POST /v1/submit-tts (mirrors /v1/submit-prompt
  shape; routes to marketplace.submitTts).

- buyer/src/ui/components/PiperTTSForm.tsx: two-mode rendering. When
  advertRef + payment_lovelace are passed, posts to /v1/submit-tts and
  decodes base64 audio into a blob URL. When neither is passed, falls
  back to /v1/synth-speech (the path-B direct demo proxy).

- buyer/src/ui/pages/Dashboard.tsx: capability dispatch routes a real
  on-chain TTS supplier to the marketplace mode and the synthetic demo
  card to the direct mode (its utxo_ref="demo:piper-tts" fails the ref
  parser → advertRef is null → demo mode). Same component for both.

Tests still passing: 31 buyer + 29 supplier; both typechecks clean.
Chat path behaviourally untouched.
2026-05-06 22:00:31 +02:00
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