From 67f412e2acf56c784b2e6b9e5c789350db8a9ecf Mon Sep 17 00:00:00 2001 From: Mercurial Date: Mon, 23 Mar 2026 06:52:29 +0800 Subject: [PATCH 1/2] Add 5-minute prediction market on Cardano using Pyth Lazer Team Cliley submission for Pythathon 2026. Constant-product AMM prediction market with Pyth Lazer oracle integration for real-time price feeds on Cardano (Plutus V3). Full end-to-end flow verified on Preprod: create, bet, resolve, claim. --- lazer/cardano/prediction-market/README.md | 130 +++ .../prediction-market/contracts/aiken.lock | 27 + .../prediction-market/contracts/aiken.toml | 23 + .../prediction_market/market_validation.ak | 350 +++++++++ .../contracts/lib/prediction_market/mocks.ak | 110 +++ .../contracts/lib/prediction_market/types.ak | 46 ++ .../prediction-market/contracts/plutus.json | 363 +++++++++ .../contracts/validators/market.ak | 42 + .../contracts/validators/pyth_test.ak | 37 + .../contracts/validators/tests/bet_tests.ak | 227 ++++++ .../contracts/validators/tests/claim_tests.ak | 208 +++++ .../contracts/validators/tests/mint_tests.ak | 150 ++++ .../prediction-market/offchain/.env.example | 6 + .../prediction-market/offchain/.gitignore | 2 + .../prediction-market/offchain/index.ts | 737 ++++++++++++++++++ .../prediction-market/offchain/package.json | 18 + .../prediction-market/offchain/tsconfig.json | 29 + 17 files changed, 2505 insertions(+) create mode 100644 lazer/cardano/prediction-market/README.md create mode 100644 lazer/cardano/prediction-market/contracts/aiken.lock create mode 100644 lazer/cardano/prediction-market/contracts/aiken.toml create mode 100644 lazer/cardano/prediction-market/contracts/lib/prediction_market/market_validation.ak create mode 100644 lazer/cardano/prediction-market/contracts/lib/prediction_market/mocks.ak create mode 100644 lazer/cardano/prediction-market/contracts/lib/prediction_market/types.ak create mode 100644 lazer/cardano/prediction-market/contracts/plutus.json create mode 100644 lazer/cardano/prediction-market/contracts/validators/market.ak create mode 100644 lazer/cardano/prediction-market/contracts/validators/pyth_test.ak create mode 100644 lazer/cardano/prediction-market/contracts/validators/tests/bet_tests.ak create mode 100644 lazer/cardano/prediction-market/contracts/validators/tests/claim_tests.ak create mode 100644 lazer/cardano/prediction-market/contracts/validators/tests/mint_tests.ak create mode 100644 lazer/cardano/prediction-market/offchain/.env.example create mode 100644 lazer/cardano/prediction-market/offchain/.gitignore create mode 100644 lazer/cardano/prediction-market/offchain/index.ts create mode 100644 lazer/cardano/prediction-market/offchain/package.json create mode 100644 lazer/cardano/prediction-market/offchain/tsconfig.json diff --git a/lazer/cardano/prediction-market/README.md b/lazer/cardano/prediction-market/README.md new file mode 100644 index 00000000..94c8cd8e --- /dev/null +++ b/lazer/cardano/prediction-market/README.md @@ -0,0 +1,130 @@ +# 5-Minute Prediction Market on Cardano + +A decentralized prediction market on Cardano that uses **Pyth Lazer** oracle for real-time price feeds. Users create markets on any Pyth-supported asset (BTC, ETH, ADA, etc.), place bets through a constant-product AMM, and settle based on verified on-chain prices. + +## Team Cliley + +- **Clark Alesna** (Captain) — clark@saib.dev +- **Riley Kilgore** — riley.kilgore@iohk.io + +## How Pyth Lazer Is Used + +The contract integrates Pyth Lazer via the **withdraw-0 pattern** — the standard approach for consuming Pyth price data on Cardano: + +1. **Market Creation** — The creator sets a target price and a Pyth feed ID (e.g., BTC/USD = 1). The Pyth deployment policy ID is stored in the on-chain datum. + +2. **Market Resolution** — After the 5-minute window, anyone can resolve the market: + - The off-chain CLI fetches a signed price update from the Pyth Lazer REST API + - The signed message is included as a withdrawal redeemer in the transaction + - The Pyth State UTxO (holding the Pyth NFT + withdraw script) is added as a reference input + - On-chain, the validator calls `pyth.get_updates()` which: + - Finds the Pyth State UTxO via the stored policy ID + - Extracts the withdraw script hash from the Pyth state datum + - Reads the signed price update from the withdrawal redeemer + - Parses the Pyth Lazer message format to extract the verified price + - The contract compares `oracle_price > target_price` to determine the winning side + +3. **Key Pyth Integration Points:** + - `pyth-network/pyth-lazer-cardano` Aiken library (contract side) + - Pyth Lazer REST API at `https://pyth-lazer.dourolabs.app/v1/latest_price` (off-chain side) + - Pyth State UTxO on Preprod: policy `d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6` + +## Architecture + +``` +contracts/ Aiken smart contracts (Plutus V3) +├── lib/ Validation logic + types +│ └── prediction_market/ +│ ├── types.ak MarketDatum, redeemers, params +│ └── market_validation.ak Bet, Resolve, Claim, Mint/Burn +├── validators/ +│ ├── market.ak Main validator (dual spend + mint) +│ └── pyth_test.ak Pyth integration test validator +│ └── tests/ Unit tests +offchain/ TypeScript CLI (Bun + blaze-cardano) +├── index.ts Full CLI: create, bet, resolve, claim, price +``` + +## Contract Design + +**One validator, dual purpose** — parameterized with a one-shot UTxO for unique policy IDs per market: + +| Action | Description | +|--------|-------------| +| **Create** | Consume one-shot, mint state thread + YES/NO tokens, lock seed ADA | +| **Bet** | Constant-product AMM: `tokens = reserve - k / (other_reserve + amount)` | +| **Resolve** | Pyth Lazer price check, set `resolved=True` + `winning_side` | +| **Claim** | Burn winning tokens, receive proportional ADA payout | + +The state thread token (empty asset name) ensures continuity across transactions. The AMM uses `k = yes_reserve * no_reserve` invariant. + +## Verified on Preprod + +Full end-to-end flow tested on Cardano Preprod: + +| Step | Tx Hash | +|------|---------| +| Create (10 ADA seed, BTC/USD) | `9d840a1215456feea68708cab9088c84779b172f8106272d991e5fbdb78d05bd` | +| Bet YES (2 ADA) | `3c5d2bf0627693b47922124761e311f5f1e07e401385cc35e4a615b7204f4153` | +| Resolve (NO won via Pyth) | `88f97270e83503eba87f0bc6f677141100b06c5df216f73676937527e6bb131d` | +| Claim (12 ADA payout) | `7d691f3c2f52f0a48bd5378c7090560597c0e09fdf235883d455e2c9e9e04de5` | + +## Quick Start + +### Prerequisites + +- [Aiken](https://aiken-lang.org/) v1.1+ +- [Bun](https://bun.sh/) v1.0+ +- Blockfrost API key (Preprod) +- Pyth Lazer API key + +### Build Contracts + +```bash +cd contracts +aiken build +aiken check # run tests +``` + +### Run Off-chain CLI + +```bash +cd offchain +cp .env.example .env # fill in your keys +bun install +bun run index.ts help +``` + +### Full Flow + +```bash +# Check current BTC price +bun run index.ts price BTC/USD + +# Create a 5-min market (10 ADA seed, BTC/USD) +bun run index.ts create BTC/USD 10 +# → prints policy + oneshot params for subsequent commands + +# Place a bet (2 ADA on YES) +bun run index.ts bet yes 2 + +# Wait 5 minutes, then resolve +bun run index.ts resolve + +# Claim winnings +bun run index.ts claim +``` + +## Supported Feeds + +Any Pyth Lazer feed ID works. Common ones: + +| Feed | ID | +|------|----| +| BTC/USD | 1 | +| ETH/USD | 2 | +| ADA/USD | 16 | + +## License + +MIT diff --git a/lazer/cardano/prediction-market/contracts/aiken.lock b/lazer/cardano/prediction-market/contracts/aiken.lock new file mode 100644 index 00000000..a995f249 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/aiken.lock @@ -0,0 +1,27 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[[requirements]] +name = "pyth-network/pyth-lazer-cardano" +version = "main" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +requirements = [] +source = "github" + +[[packages]] +name = "pyth-network/pyth-lazer-cardano" +version = "main" +requirements = [] +source = "github" + +[etags] +"pyth-network/pyth-lazer-cardano@main" = [{ secs_since_epoch = 1774200439, nanos_since_epoch = 656666028 }, "a46dacd97a22eb07feeaf966d48c3116c8249ddc836705656e3135cea285bcfc"] diff --git a/lazer/cardano/prediction-market/contracts/aiken.toml b/lazer/cardano/prediction-market/contracts/aiken.toml new file mode 100644 index 00000000..de3e4c6c --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/aiken.toml @@ -0,0 +1,23 @@ +name = "saib/prediction-market" +version = "0.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "Aiken contracts for project 'saib/prediction-market'" + +[repository] +user = "saib" +project = "prediction-market" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[[dependencies]] +name = "pyth-network/pyth-lazer-cardano" +version = "main" +source = "github" + +[config] diff --git a/lazer/cardano/prediction-market/contracts/lib/prediction_market/market_validation.ak b/lazer/cardano/prediction-market/contracts/lib/prediction_market/market_validation.ak new file mode 100644 index 00000000..a31752b9 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/lib/prediction_market/market_validation.ak @@ -0,0 +1,350 @@ +use aiken/collection/dict +use aiken/collection/list +use aiken/interval.{Finite} +use cardano/address.{Script} +use cardano/assets +use cardano/transaction.{InlineDatum, OutputReference, Transaction, find_input} +use prediction_market/types.{MarketDatum, No, Yes} +use pyth +use types/u32 + +// Token names +pub const yes_token: ByteArray = "YES" + +pub const no_token: ByteArray = "NO" + +pub const state_thread_token: ByteArray = "" + +fn has_state_thread(value: assets.Value, policy_id: assets.PolicyId) -> Bool { + assets.quantity_of(value, policy_id, state_thread_token) == 1 +} + +fn has_no_market_policy_mint( + policy_id: assets.PolicyId, + tx: Transaction, +) -> Bool { + let mint_dict = tx.mint |> assets.tokens(policy_id) + dict.foldl(mint_dict, True, fn(_asset_name, _qty, _acc) { False }) +} + +fn only_mints_market_token( + policy_id: assets.PolicyId, + token_name: ByteArray, + amount: Int, + tx: Transaction, +) -> Bool { + let mint_dict = tx.mint |> assets.tokens(policy_id) + + assets.quantity_of(tx.mint, policy_id, token_name) == amount && dict.foldl( + mint_dict, + True, + fn(asset_name, qty, acc) { + acc && asset_name == token_name && qty == amount + }, + ) +} + +fn only_burns_market_token( + policy_id: assets.PolicyId, + token_name: ByteArray, + amount: Int, + burn_state_thread: Bool, + tx: Transaction, +) -> Bool { + let mint_dict = tx.mint |> assets.tokens(policy_id) + + assets.quantity_of(tx.mint, policy_id, token_name) == amount && if burn_state_thread { + assets.quantity_of(tx.mint, policy_id, state_thread_token) == -1 + } else { + assets.quantity_of(tx.mint, policy_id, state_thread_token) == 0 + } && dict.foldl( + mint_dict, + True, + fn(asset_name, qty, acc) { + acc && if asset_name == token_name { + qty == amount + } else { + burn_state_thread && asset_name == state_thread_token && qty == -1 + } + }, + ) +} + +// ─── Bet validation ────────────────────────────────────────────────────────── + +pub fn validate_bet( + datum: MarketDatum, + direction: types.BetDirection, + amount: Int, + spend_out_ref: OutputReference, + tx: Transaction, +) -> Bool { + // Market must not be resolved + expect !datum.resolved + + // Amount must be positive + expect amount > 0 + + // Must be before resolution time + expect Finite(upper) = tx.validity_range.upper_bound.bound_type + expect upper <= datum.resolution_time + + // Calculate tokens out via constant product formula + let (tokens_out, new_datum) = + when direction is { + Yes -> { + let tokens = datum.yes_reserve - datum.k / ( datum.no_reserve + amount ) + expect tokens > 0 + let new = + MarketDatum { + ..datum, + yes_reserve: datum.yes_reserve - tokens, + no_reserve: datum.no_reserve + amount, + total_yes_minted: datum.total_yes_minted + tokens, + total_ada: datum.total_ada + amount, + } + (tokens, new) + } + No -> { + let tokens = datum.no_reserve - datum.k / ( datum.yes_reserve + amount ) + expect tokens > 0 + let new = + MarketDatum { + ..datum, + yes_reserve: datum.yes_reserve + amount, + no_reserve: datum.no_reserve - tokens, + total_no_minted: datum.total_no_minted + tokens, + total_ada: datum.total_ada + amount, + } + (tokens, new) + } + } + + // Find continuing output at script address with updated datum + expect Some(spend_input) = find_input(tx.inputs, spend_out_ref) + expect Script(market_policy_id) = + spend_input.output.address.payment_credential + let script_address = spend_input.output.address + expect has_state_thread(spend_input.output.value, market_policy_id) + + expect Some(continuing_output) = + tx.outputs + |> list.find(fn(output) { output.address == script_address }) + + // Verify continuing output has correct datum + expect InlineDatum(out_datum) = continuing_output.datum + expect output_market_datum: MarketDatum = out_datum + expect output_market_datum == new_datum + expect has_state_thread(continuing_output.value, market_policy_id) + + // Verify correct tokens are minted + let expected_token_name = + when direction is { + Yes -> yes_token + No -> no_token + } + + expect + only_mints_market_token( + market_policy_id, + expected_token_name, + tokens_out, + tx, + ) + + // Verify ADA increased by bet amount + let input_lovelace = assets.lovelace_of(spend_input.output.value) + let output_lovelace = assets.lovelace_of(continuing_output.value) + expect output_lovelace >= input_lovelace + amount + + True +} + +// ─── Resolve validation ────────────────────────────────────────────────────── + +pub fn validate_resolve( + datum: MarketDatum, + spend_out_ref: OutputReference, + tx: Transaction, +) -> Bool { + // Market must not already be resolved + expect !datum.resolved + + // Must be after resolution time + expect Finite(lower) = tx.validity_range.lower_bound.bound_type + expect lower >= datum.resolution_time + + // Get verified price from Pyth Lazer oracle + expect [update] = pyth.get_updates(datum.pyth_id, tx) + expect Some(feed) = + list.find(update.feeds, fn(f) { u32.as_int(f.feed_id) == datum.feed_id }) + expect Some(Some(oracle_price)) = feed.price + + // Determine winning side + let winning_side = + if oracle_price > datum.target_price { + Yes + } else { + No + } + + // Creator must sign (hackathon simplification) + expect tx.extra_signatories |> list.has(datum.creator) + + // Verify continuing output + expect Some(spend_input) = find_input(tx.inputs, spend_out_ref) + expect Script(market_policy_id) = + spend_input.output.address.payment_credential + let script_address = spend_input.output.address + expect has_state_thread(spend_input.output.value, market_policy_id) + + let expected_datum = + MarketDatum { ..datum, resolved: True, winning_side: Some(winning_side) } + + expect Some(continuing_output) = + tx.outputs + |> list.find(fn(output) { output.address == script_address }) + + expect InlineDatum(out_datum) = continuing_output.datum + expect output_market_datum: MarketDatum = out_datum + expect output_market_datum == expected_datum + expect has_state_thread(continuing_output.value, market_policy_id) + expect has_no_market_policy_mint(market_policy_id, tx) + + // Value must be preserved (no ADA removed during resolve) + let input_lovelace = assets.lovelace_of(spend_input.output.value) + let output_lovelace = assets.lovelace_of(continuing_output.value) + expect output_lovelace >= input_lovelace + + True +} + +// ─── Claim validation ──────────────────────────────────────────────────────── + +pub fn validate_claim( + datum: MarketDatum, + burn_amount: Int, + spend_out_ref: OutputReference, + tx: Transaction, +) -> Bool { + // Market must be resolved + expect datum.resolved + expect Some(winning_side) = datum.winning_side + expect burn_amount > 0 + + // Determine winning token name and total minted + let (winning_token, total_winning_minted) = + when winning_side is { + Yes -> (yes_token, datum.total_yes_minted) + No -> (no_token, datum.total_no_minted) + } + + // Calculate payout + let payout = burn_amount * datum.total_ada / total_winning_minted + + // Verify continuing output with reduced values + expect Some(spend_input) = find_input(tx.inputs, spend_out_ref) + expect Script(market_policy_id) = + spend_input.output.address.payment_credential + let script_address = spend_input.output.address + expect has_state_thread(spend_input.output.value, market_policy_id) + + let expected_datum = + when winning_side is { + Yes -> + MarketDatum { + ..datum, + total_ada: datum.total_ada - payout, + total_yes_minted: datum.total_yes_minted - burn_amount, + } + No -> + MarketDatum { + ..datum, + total_ada: datum.total_ada - payout, + total_no_minted: datum.total_no_minted - burn_amount, + } + } + + // If all winning tokens are claimed, no continuing output needed + if total_winning_minted == burn_amount { + // Last claimer — gets whatever remains, no continuing output required + expect + only_burns_market_token( + market_policy_id, + winning_token, + -burn_amount, + True, + tx, + ) + True + } else { + expect Some(continuing_output) = + tx.outputs + |> list.find(fn(output) { output.address == script_address }) + + expect InlineDatum(out_datum) = continuing_output.datum + expect output_market_datum: MarketDatum = out_datum + expect output_market_datum == expected_datum + expect has_state_thread(continuing_output.value, market_policy_id) + expect + only_burns_market_token( + market_policy_id, + winning_token, + -burn_amount, + False, + tx, + ) + + // Verify ADA decreased by exactly payout + let input_lovelace = assets.lovelace_of(spend_input.output.value) + let output_lovelace = assets.lovelace_of(continuing_output.value) + expect output_lovelace >= input_lovelace - payout + + True + } +} + +// ─── Mint validation ───────────────────────────────────────────────────────── + +pub fn validate_mint_tokens( + one_shot: OutputReference, + policy_id: assets.PolicyId, + tx: Transaction, +) -> Bool { + let one_shot_consumed = + tx.inputs + |> list.any(fn(input) { input.output_reference == one_shot }) + + let has_market_spend = + tx.inputs + |> list.any( + fn(input) { + input.output.address.payment_credential == Script(policy_id) && has_state_thread( + input.output.value, + policy_id, + ) + }, + ) + + let minted_state_thread_qty = + assets.quantity_of(tx.mint, policy_id, state_thread_token) + + let mint_dict = tx.mint |> assets.tokens(policy_id) + let all_market_mints_are_positive = + dict.foldl(mint_dict, True, fn(_asset_name, qty, acc) { acc && qty > 0 }) + + expect all_market_mints_are_positive + + if one_shot_consumed { + expect minted_state_thread_qty == 1 + True + } else { + expect minted_state_thread_qty == 0 + has_market_spend + } +} + +pub fn validate_burn_tokens(policy_id: assets.PolicyId, tx: Transaction) -> Bool { + let mint_dict = tx.mint |> assets.tokens(policy_id) + dict.foldl(mint_dict, True, fn(_key, qty, acc) { acc && qty < 0 }) +} diff --git a/lazer/cardano/prediction-market/contracts/lib/prediction_market/mocks.ak b/lazer/cardano/prediction-market/contracts/lib/prediction_market/mocks.ak new file mode 100644 index 00000000..8841f702 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/lib/prediction_market/mocks.ak @@ -0,0 +1,110 @@ +use aiken/collection/dict +use aiken/interval +use cardano/address.{Address, Script, VerificationKey} +use cardano/assets +use cardano/transaction.{ + InlineDatum, Input, Output, OutputReference, Transaction, +} +use prediction_market/types.{MarketDatum} + +// ── Mock constants ─────────────────────────────────────────────────────────── + +pub const mock_creator_vkh = + #"01010101010101010101010101010101010101010101010101010101" + +pub const mock_market_hash = + #"aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd" + +pub const mock_pyth_id = + #"55667788556677885566778855667788556677885566778855667788" + +pub const mock_tx_id = + #"0000000000000000000000000000000000000000000000000000000000000000" + +// BTC/USD Pyth feed ID +pub const mock_feed_id = 1 + +// ── Mock builders ──────────────────────────────────────────────────────────── + +pub fn mock_creator_address() -> Address { + Address { + payment_credential: VerificationKey(mock_creator_vkh), + stake_credential: None, + } +} + +pub fn mock_script_address() -> Address { + Address { + payment_credential: Script(mock_market_hash), + stake_credential: None, + } +} + +pub fn mock_market_datum() -> MarketDatum { + MarketDatum { + creator: mock_creator_vkh, + pyth_id: mock_pyth_id, + feed_id: mock_feed_id, + target_price: 5_000_000_000_000, + resolution_time: 300_000, + token_policy: mock_market_hash, + yes_reserve: 100_000_000, + no_reserve: 100_000_000, + k: 10_000_000_000_000_000, + total_yes_minted: 100_000_000, + total_no_minted: 100_000_000, + total_ada: 100_000_000, + resolved: False, + winning_side: None, + } +} + +pub fn mock_market_value(lovelace: Int) -> assets.Value { + assets.from_lovelace(lovelace) + |> assets.add(mock_market_hash, "", 1) +} + +pub fn mock_market_input(lovelace: Int) -> Input { + Input { + output_reference: OutputReference { + transaction_id: mock_tx_id, + output_index: 0, + }, + output: Output { + address: mock_script_address(), + value: mock_market_value(lovelace), + datum: InlineDatum(mock_market_datum()), + reference_script: None, + }, + } +} + +pub fn mock_market_output(datum: MarketDatum, lovelace: Int) -> Output { + Output { + address: mock_script_address(), + value: mock_market_value(lovelace), + datum: InlineDatum(datum), + reference_script: None, + } +} + +pub fn mock_transaction() -> Transaction { + Transaction { + inputs: [], + reference_inputs: [], + outputs: [], + fee: 0, + mint: assets.zero, + certificates: [], + withdrawals: [], + validity_range: interval.between(0, 100_000), + extra_signatories: [], + redeemers: [], + datums: dict.empty, + id: mock_tx_id, + votes: [], + proposal_procedures: [], + current_treasury_amount: None, + treasury_donation: None, + } +} diff --git a/lazer/cardano/prediction-market/contracts/lib/prediction_market/types.ak b/lazer/cardano/prediction-market/contracts/lib/prediction_market/types.ak new file mode 100644 index 00000000..3ce4d516 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/lib/prediction_market/types.ak @@ -0,0 +1,46 @@ +use cardano/assets.{PolicyId} +use cardano/transaction.{OutputReference} + +// ── Market types ───────────────────────────────────────────────────────────── + +pub type BetDirection { + Yes + No +} + +pub type MarketDatum { + creator: ByteArray, + pyth_id: PolicyId, + feed_id: Int, + target_price: Int, + resolution_time: Int, + token_policy: PolicyId, + yes_reserve: Int, + no_reserve: Int, + k: Int, + total_yes_minted: Int, + total_no_minted: Int, + total_ada: Int, + resolved: Bool, + winning_side: Option, +} + +// ── Redeemers ──────────────────────────────────────────────────────────────── + +pub type MarketAction { + Bet { direction: BetDirection, amount: Int } + Resolve + Claim { burn_amount: Int } +} + +pub type MintAction { + MintTokens + BurnTokens +} + +// ── One-shot parameter ─────────────────────────────────────────────────────── + +/// Parameter for the market validator (makes each market's policy ID unique) +pub type MarketParams { + one_shot: OutputReference, +} diff --git a/lazer/cardano/prediction-market/contracts/plutus.json b/lazer/cardano/prediction-market/contracts/plutus.json new file mode 100644 index 00000000..62fd78f9 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/plutus.json @@ -0,0 +1,363 @@ +{ + "preamble": { + "title": "saib/prediction-market", + "description": "Aiken contracts for project 'saib/prediction-market'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "market.market.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/prediction_market~1types~1MarketDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/prediction_market~1types~1MarketAction" + } + }, + "parameters": [ + { + "title": "params", + "schema": { + "$ref": "#/definitions/prediction_market~1types~1MarketParams" + } + } + ], + "compiledCode": "59248e010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a48888888966003300130033754013223232330010010042259800800c00e2646644b30013372200e00515980099b8f0070028800c01900944cc014014c03c0110091bae3008001375660120026016002804852f5bded8c1370e900148c01cc0200064600e6010601060106010003222329800800c01200680088896600200510018994c004012601c00798009bae30090019bad300a001801200a4010601800480526e2120009b87480026e9520024888888888cc88ca6002601c6ea8006602401922300c9800801400691100401130120034888966002601c0091980098099baa00591192cc004c0300062b3001301637540070028b202e8acc004c0440062b3001301637540070028b202e8b2028405060286ea800a6e1d200498091baa00148888cc88c9660026020009132323259800981080144cc024c08000c0122c80f0dd6980f800980f800980d1baa0088acc004c0540122b3001301a37540110018b20368acc004c01401226464b30013020002801c5901d1bad301e001301a375401116406080c10180acc004c03cc060dd5000c66002603860326ea80064603a603c603c603c603c603c603c603c603c603c603c603c603c0032301d301e301e0019ba5480026e25200494c00400691100a4410040352301d301e301e301e301e301e301e301e301e301e301e0019180e980f180f180f180f180f180f180f180f180f180f180f000c8c074c078c078c078c078c078c078c078c078c07800660306ea801e4603a603c603c603c603c603c603c603c0032301d301e301e301e301e301e301e301e301e0014888888888888c8cc8966002603c00d13298009816000cdd698161816800c8c0b4c0b8c0b8c0b8c0b8c0b8c0b800522259800cc004c09cc0acdd5180918161baa013a50a5140a9159800981180144c966002605060586ea80062b3001337126eb4c0c0c0b4dd50009bad3026302d375402913298009bad303100198189819000ccc020dd6181898171baa01e017488966002604e60606ea800626464b3001302e303237540031332259800998121bab302f30353754605e606a6ea801000a264b3001302c3035375400313232598009811981b9baa001899198090008acc004cdd78008054566002660506eacc0ccc0e4dd5001803456600264b30013370f30013756606660746ea80aa00f00140b4019198009981b1bab3033303a375405400f4a322259800800c56600266e3c00c012266e1c00803e294103b4528207640c914a081c166002605e60706ea8042291103594553008a45024e4f0040dd15980099b893370060366eacc0ccc0e4dd51819981c9baa00800f301b3756606660726ea800e29462c81ba2c81ba2c81ba2c81b8c0ecc0e0dd5000c59036180e181b9baa0013039303637540031640d0660206eb0c068c0d4dd5012919baf3039303637540020051640cc6eb8c0d8c0ccdd5000981b18199baa302d303337540051640c4606a60646ea8c0d4c0c8dd5181618191baa0013034303137540031640bc59800981198161baa0048992cc004c0940062660606ea0004cc0c0c8c8c8c8c8c8c8c8c068cc0e0c0e4020cc0e0c0e401ccc0e0c0e4018cc0e0c0e4014cc0e0c0e4010cc0e0c0e400ccc0e0dd419b81375a6016606c6ea8074024cc0e0dd419b80375a6026606c6ea8074030cc0e0c0e4008cc0e0dd419b80375a602a606c6ea8074024cc0e0c0e4004cc0e0dd419b80375a602c606c6ea8074030c0e8c0e8004c0e4c0e4004c0e0c0e0c0e0004c0dc004c0d8004c0d4004c0d0004c0cc004c0b8dd500aa5eb822c8160cdc09bad3002302d375402866e0cdd6980498169baa014337006eb4c028c0b4dd500a001c4c966002604a0031330303750002660606464646464646464603466070607201066070607200e66070607200c66070607200a660706072008660706072006660706ea0cdc01bad300b3036375403a018660706ea0cdc09bad30133036375403a012660706072004660706072002660706ea0cdc01bad30173036375403a012660706ea0cdc01bad30163036375403a0186074607460740026072002607060706070002606e002606c002606a00260680026066002605c6ea80552f5c11640b066e04dd6980518169baa014337066eb4c024c0b4dd500a19b80375a6004605a6ea805000d02b45902b45902b181798161baa3026302c3754601260586ea80722c81522c815060506ea805a264b300130240078cc0048966002604260546ea800a2646464b30013032002899192cc004c0980062b3001303037540050068b20628acc004c0ac00626464b300130360028044590331bad303400130303754005159800980d800c56600260606ea800a00d1640c51640b8817102e18171baa00130310038b205e32598009817000c5660026022605a00316898149816800a0588b205e37546060002606000260566ea800a2c814a4464b30013004375c605e60600031302f0018b205430020019119198008008019119801800980100148c0b4c0b8c0b8c0b800522223259800cc004c0a4c0b4dd5180a18171baa015a50a5140b113259800981518171baa0018acc004cdc49bad3028302f375402c6eb4c0c8c0bcdd5000c4c966002606800313259800981398181baa0018992cc004c0a0c0c4dd5000c4c966002605260646ea80062b3001323322330020020012259800800c528456600266e3cdd7181c800801c528c4cc008008c0e8005034206e3758602060686ea8090dd7181b18199baa01a8992cc004c0a8c0ccdd5000c4c8c9660026062606a6ea800626644b3001330273756606460706ea8c0c8c0e0dd500200144cc8966002606060726ea800626464b30013027303b37540031323301600115980099baf0010058acc004cc0b0dd5981b981e9baa0030078acc0066002660726eacc0d8c0f4dd5016803d28c88928206a8acc004cdc4980f9bab3037303d3754606e607a6ea8024c07cdd5981b981e9baa0038a518b20768b20768b20768b2076303f303c37540031640e8604060766ea8004c0f4c0e8dd5000c5903819191919191919191919191813998229823005998229823005198229823004998229823004198229823003998229823003198229823002998229823002198229823001998229823001198229823000998229823182380099822cc00528d30103d87a8000a60103d879800041046608a604e6608ab3001337106eb4c05cc10cdd50151bad30463043375402314c0103d87980008a6103d87a8000410497ae04bd701823000982280098220009821800982100098208009820000981f800981f000981e800981c1baa01f330133758603a60706ea80a08cdd7981e181c9baa0010028b206c375c6072606c6ea8004c0e4c0d8dd51818181b1baa0028b20683038303537546070606a6ea8c0bcc0d4dd5000981b981a1baa0018b20643300d3758606c60666ea808c0722c818a2c8188c0d4c0c8dd5000c59030181598189baa3034303137540031640bc660166eb0c054c0c0dd51819800919b87375a606860626ea8004dd6980b18189baa0188b2062323259800981398181baa0018992cc004c0a0c0c4dd5000c4c8cc0200048966002005132330010010042259800800c52f5c113303998009b89480026e3120049b804800e6e2520089b8c480026e3120089b804801d222222233223301623235980099b8f375c608a0029110475d3c793008cc004888c8cc00cdd698248009bae3049304a00130030019b89480426e3120109b804803e600e90082444446600a464660966ea0cde5250375c6098002660966098609a00297ae05980098029b8d0018998251ba930020013304a37526600860066e340040052f5c1164118466446601046609a6ea0cdc7000a40006609a6ea4cc00cc008dc6800800a5eb808cc0248cc138dd419b8e00148000cc138dd49980218019b8d0010014bd70111919198289819998289ba800833051375000a660a260a400297ae033051305230530014bd7018008012cc004c05800a2653001001a5eb82006800888966002608c003123233054374e002660a86ea40092f5c065300100180252f5c080088896600200510018cc00400e60b0005330563057002001400c82aa246530010059982a182a8008024c02400e6eb8c154c1580050054c004c8cc150dd419bca4a06eb8c154004cc150c154c1580052f5c0b30013017371a00313305337526024002660a66ea4cc054c050dc6800800a5eb822c827a44660aa004660aa6ea40052f5c1370e90034dc3a4010911119191919803194c0040066eb0c17000a607a660b660b8010660b698103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80004bd702002222598008014400633001003982f8014c8c96600260a60031323042330603061001330603042330603061305e375400697ae030623062001305d3754007159800982c000c4c8c8c10ccc184c188008cc184c188004cc184c10ccc184c188c17cdd500225eb80c18cc18c004c188004c174dd5001c5660026090003132323230443306230630033306230630023306230630013306230443306230633060375400a97ae03064306400130630013062001305d37540071598009805000c4c8c8c8c8c114cc18cc190010cc18cc19000ccc18cc190008cc18cc190004cc18cc114cc18cc190c184dd500325eb80c194c194004c190004c18c004c188004c174dd5001c56600260120031323232323230463306430650053306430650043306430650033306430650023306430650013306430463306430653062375400e97ae0306630660013065001306400130630013062001305d375400715980099b87480280062646464646464608e660ca60cc00c660ca60cc00a660ca60cc008660ca60cc006660ca60cc004660ca60cc002660ca608e660ca60cc60c66ea80212f5c060ce60ce00260cc00260ca00260c800260c600260c400260ba6ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c120cc198c19c01ccc198c19c018cc198c19c014cc198c19c010cc198c19c00ccc198c19c008cc198c19c004cc198c120cc198c19cc190dd5004a5eb80c1a0c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001c56600266e1d200e00189919191919191919182499833983400419833983400399833983400319833983400299833983400219833983400199833983400119833983400099833982499833983418329baa00a4bd70183498348009834000983380098330009832800983200098318009831000982e9baa0038acc004cdc3a40200031323232323232323232304a33068306900933068306900833068306900733068306900633068306900533068306900433068306900333068306900233068306900133068304a3306830693066375401697ae0306a306a00130690013068001306700130660013065001306400130630013062001305d375400715980099b87480480062646464646464646464646096660d260d4014660d260d4012660d260d4010660d260d400e660d260d400c660d260d400a660d260d4008660d260d4006660d260d4004660d260d4002660d26096660d260d460ce6ea80312f5c060d660d600260d400260d200260d000260ce00260cc00260ca00260c800260c600260c400260ba6ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c130cc1a8c1ac02ccc1a8c1ac028cc1a8c1ac024cc1a8c1ac020cc1a8c1ac01ccc1a8c1ac018cc1a8c1ac014cc1a8c1ac010cc1a8c1ac00ccc1a8c1ac008cc1a8c1ac004cc1a8c130cc1a8c1acc1a0dd5006a5eb80c1b0c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001c56600266e1d20160018991919191919191919191919182699835983600619835983600599835983600519835983600499835983600419835983600399835983600319835983600299835983600219835983600199835983600119835983600099835982699835983618349baa00e4bd701836983680098360009835800983500098348009834000983380098330009832800983200098318009831000982e9baa00389919191919191919191919191826998359836006198359836005998359836005198359836004998359836004198359836003998359836003198359836002998359836002198359836001998359836001198359836000998359836183680099835982699835983618349baa00e4bd7025eb80c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001a0b6416c82d905b20b6416c82d905b20b6416c82d905b182d9baa001305e002400c82e0dd7182d982e000991acc004c08400a2653001001a5eb8200680088896600260a200312323305f374e002660be6ea40092f5c065300100180252f5c080088896600200510018cc00400e60c6005330613062002001400c8302246530010059982f98300008024c05000e6eb8c180c1840050051802800a0b28b20ac375c60b660b8006530012232598009800a40211598009800a4001148002266e3922010801020408102040800000241651598009800a408113370490400219801801980b00144cdc124101010100406600600666e00009203f416482c8dc4000c888c8cc178c00cdd6982f8009982f182f9830000a5eb80c00c006660b46ea0cdc7000a4000660b46ea4cc040c03cdc6800800a5eb8122232329800919802919198311ba833794940dd718318009983118319832000a5eb816600260386e340062660c26ea4c064004cc184dd49980d980d1b8d0010014bd7045905d1191801acc004c158006298103d87a800089822198311ba80014bd7020bc30030019bad3060003911191acc006600260ae6eb4c19000694294505f44cc02000c8c014c118cc190dd4000a5eb822601e600898103d87a8000417c6eb8c190c194004cc188dd419b8e00148000cc188dd49980c180b9b8d0010014bd7024446b30013056002898019182299831800a5eb822b3001305b002898019182b19831800a5eb822b3001304b00289801919ba548010cc18c0052f5c1159800980680144cc01c8c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b30013044371a00313306337526046002660c66ea4cc0a4c0a0dc6800800a5eb822c82f88cdd2a400c660c66ea00052f5c1159800980600144cc01c8c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b30013044371a00313306337526046002660c66ea4cc0a4c0a0dc6800800a5eb822c82f88cdd2a4010660c66ea16600266e20005208080048800c4cdc080098032404082f92f5c115980099b874802800a26006466e95200a330630014bd70456600266e1d200c002899800919ba548030cc18c0052f5c0464660c86ea0c018dd698328009983218329833000a5eb80c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2b30013370e900700144cc0048cdd2a401c660c600297ae023233064375066f29281bae306500133064306530660014bd702cc004c078dc6800c4cc18cdd4980d800998319ba93301d301c371a00200297ae08b20be8acc004cdc3a402000513300123374a900819831800a5eb808c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2b30013370e9009001448c8c8cc194cdd2a4024660ca60cc00297ae033065306630670014bd70180080119804119198329ba833794940dd718330009983298331833800a5eb8166002608a6e340062660c86ea4c090004cc190dd49981518149b8d0010014bd7045906012cc004c1600062980103d87980008acc004c174006298103d87a80008acc004c134006298103d87b80008acc004c03c006298103d87c80008acc004c038006298103d87d80008b20c04180830106020c08acc004cdc3a40280051300323374a900a19831800a5eb822b30013370e900b00144c00c8cdd2a402c660c600297ae08acc004cdc3a403000513300123374a900c19831800a5eb808c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2c82f105e20bc417882f105e20bc417882f105e20bc417882f0dd718319832003096600266e2000520808080808080808080028800c4cdc08009801241000282d8c00c00c6eb4c164004c8cc160dd419b8e00148000cc160dd49980718069b8d0010014bd701bae30583059004209c8b2096371890011b80480051641006eb8c114c11800566002600e6e340062660866ea4c008004cc10cdd49980298021b8d0010014bd7045903f1bae3025304037546602c4646b30013371e6eb8c11400522104b9011a8200891919191919191991198269817998269827003998269ba90013304d304e0024bd70198269827182780125eb80d660026024003125980099b89002371a00313304c37526601e004002660986ea66002005337026e3400400a002b8c25eb822c82422c8238dd7182618268011bae304c004375a609600264660946ea0cde5250375c6096002660946096609800297ae05980098151b8d0018998249ba930090013304937526601e601c6e340040052f5c11641146eb8c124c128004d6600294624b30013371290201b8d0018998241ba93300b48100004cc120dd4cc005204099b80371a002901fc0057184bd704590444590431bae30483049001300100259800a51892cc004cdc4a4100026e3400626608c6ea4cc0252080010013304637533001482000666e00dc6800a40ff0015c612f5c11641091641051641006eb8c114c11800566002600e6e340062660866ea4c008004cc10cdd49980298021b8d0010014bd7045903f1bae304300a300348010c00d200819801001181d800a0708991801181d0019bae303800240d86eb0c0d4c0c8dd5000c5903019199119801001000912cc004006298103d87a80008992cc004cdd7802181a000c4c064cc0dcc0d40052f5c1133003003303900240cc606e00281a8dd5980898191baa0223374a900219819981319819980298189baa30343031375400297ae04bd7045902f19198008009bac302b3031375404244b30010018a6103d87a80008992cc006600266ebc00530103d87a8000a50a5140c51001899801801981b8012062325980099912cc00400a2942265300132598009816981b1baa00189bad3037303a37566074606e6ea800629000206a30390019baf3039303a0019bab003488966002003159800980100344cc01400d20008a5040dd1325980099baf30390014c010140008acc004cc018010dd6981d181e9bab303a001898019ba6303e0028a5040e11598009980300224001130030078a5040e081c0c0f000503a0ca600200337566060606c6ea8c0c0c0d8dd5002488cc0e8008cc0e8dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0e00066eacc0e4006607a0069112cc004cdc8a441000038acc004c05000e26600a60406607c6e980092f5c00031330054c103d87a800000640e919800803c006446600e0046608066ec0dd48029ba6004001401c81d0607600481ca29422942294103a1ba633035337606ea4010dd31981a99bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c566002605c60646ea8c0d8c0ccdd5181b18199baa302d3033375400313259800980f98199baa0018992cc004c0acc0d0dd5000c4c8c8c8c8cc8966002607e007132598009819181d9baa0018991919194c004dd69821800cdd718218024dd69821801cdd7182180124444b3001304800589980c9bab304700e225980080144cc06c03089660020051302c3304a0144bd7044c8c8cc074c1240084c00cc138010dd7182480098258012092899191980d98240010980198260021bae30460013049002411d164114304300130420013041001303c37540031640e8607c00d1640f06eb8c0f0004dd5981e001181e000981d800981d000981a9baa0018b20663037303437540031640c8603060666ea8c0b4c0ccdd5000c590314530103d87a800040c4606a0028198dd7181498179baa0168b205a8b205a3031302e37546062605c6ea8c02cc0b8dd500f45902c112cc004c094c0b8dd500144c8c8c966002606c00513300930350031330090010048b206630340013034001302f37540051640b4899912cc004c098c0a8dd5180898159baa0128992cc004c088c0acdd5000c4c966002604800913232980099b833370400c6eb4c03cc0bcdd500b000ccc024dd6181918179baa01f0189bae3032002488966002605060626ea800a26464b3001302f303337540031332259800998129bab3030303637546060606c6ea801000a264b30013370e01201d159800cc00400e00d337029000007528c09d00d4528c5903544c966002605c606e6ea800626464b30013025303937540031323301400115980099baf0010058acc004cc0a8dd5981a981d9baa0030078acc006600200f00a99b814800004a942056808a2b30013371266e04c074dd5981a981d9baa3035303b3754012018603a6eacc0d4c0ecdd5001c528c59039459039459039459039181e981d1baa0018b2070301e30393754002607660706ea80062c81b0cc048dd6180e181b9baa02723375e607660706ea800400d0352cc004c0b0c0d4dd500544c8c8c8c8c8c8c8c8c8c8c090cc108c10c028cc108c10c024cc108c10c020cc108c10c01ccc108c10c018cc108c10c014cc108c10c010cc108c10c00ccc108c10c008cc108dd419b81375a603e60806ea809c05ccc108c10c004cc108dd419b81375a604060806ea809c044c110c110004c10cc10c004c108004c104004c100004c0fc004c0f8004c0f4004c0f0004c0ec004c0d8dd500ec4c8c8c8c8c8c8c8c8c8c8c090cc108c10c028cc108c10c024cc108c10c020cc108c10c01ccc108c10c018cc108c10c014cc108c10c010cc108c10c00ccc108c10c008cc108c10c004cc108dd419b81375a604260806ea809c05ccc108dd419b81375a604060806ea809c044c110c110c110004c10c004c108004c104004c100004c0fc004c0f8004c0f4004c0f0004c0ec004c0d8dd500ea0688b2068375c606e60686ea8004c0dcc0d0dd51817181a1baa0028b2064303630333754606c60666ea8c0b4c0ccdd5000981a98191baa0028b20601bad3031303200159800981198161baa001899817a610443594553003302f300c302d375402897ae0899817a6103424e4f003302f300e302d375402897ae040ad1640ac605e60586ea80062c8150c0b8c0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0acdd50094590291bad302c3029375402e44444b30013370f30013756604e605c6ea800600b0044084007159800acc00400a266e1e60026eacc09cc0b8dd5000c0169110040849000c4c09260026eacc09cc0b8dd5000c01691010040848162330013302a3756604e605c6ea8004016946444b30010018acc004cdc7801803c4cdc3801003456600200b1598009804801c4cdc38012400314a0817a294102f205e8a5040bc8132294102c45282058409c6e3d22010040984466006004466ebcc0b0c0a4dd5000801112cc004c078c09cdd500144c8c8c8c8c8c8c8c8c8c8c8c8ca600264b300130360018acc004c064c0d40062d13031303500140d11640dc6ea8c0e00066072003375c607001b375c6070019375a6070017375a6070015375a6070013375c6070011375a607000f375a607000d375a607000b375a6070009375a6070007375a6070004911111111111112cc004c11c03626464b3001303b0018992cc004c128006266064609200203f16411c608a6ea800a2b300130400018acc004c114dd5001407a2c82322c821904318219baa001304600d8b2088181c000981b800981b000981a800981a000981980098190009818800981800098178009817000981680098141baa0028b204c2232330010010032259800800c530103d87a80008992cc004c0100062601c6605800297ae089980180198170012050302c00140a88b202e301a004301a301b004456600260120091323322598009806000c566002602c6ea801200516405d1598009808800c566002602c6ea801200516405d16405080a0566002601460266ea800a330012232330010010032259800800c52845660026006603600314a3133002002301c001405880ca602e60286ea806f30013756601a60286ea801200348900401c9112cc0066002660266eacc040c05cdd5003802528c88966002003130110028a504060807a2b30013300337586034602e6ea801c8cdd7980d980c1baa0010038acc004c04800629462c80aa2b3001300d0018998019bac301a3017375400e4b30013375e603660306ea8c06cc060dd51809180c1baa001300d3301a375200a97ae08998039bab301230183754602460306ea80040162941016459015202a8b202a466002660206eacc034c050dd5002000d28c8896600200313371000490004528202a40308090c04cdd50011bae30163013375400b16404480886022602400460200124446466446600400400244b3001001801c4c8cc896600266e4401c00a2b30013371e00e00510018032026899802802980c8022026375c60240026eb4c04c004c054005013198060020018a40008a4d13656400401", + "hash": "a299f37e4b0c30c7e131b76bfe3669d9a4d73b10d8fbc175691789b5" + }, + { + "title": "market.market.mint", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/prediction_market~1types~1MintAction" + } + }, + "parameters": [ + { + "title": "params", + "schema": { + "$ref": "#/definitions/prediction_market~1types~1MarketParams" + } + } + ], + "compiledCode": "59248e010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a48888888966003300130033754013223232330010010042259800800c00e2646644b30013372200e00515980099b8f0070028800c01900944cc014014c03c0110091bae3008001375660120026016002804852f5bded8c1370e900148c01cc0200064600e6010601060106010003222329800800c01200680088896600200510018994c004012601c00798009bae30090019bad300a001801200a4010601800480526e2120009b87480026e9520024888888888cc88ca6002601c6ea8006602401922300c9800801400691100401130120034888966002601c0091980098099baa00591192cc004c0300062b3001301637540070028b202e8acc004c0440062b3001301637540070028b202e8b2028405060286ea800a6e1d200498091baa00148888cc88c9660026020009132323259800981080144cc024c08000c0122c80f0dd6980f800980f800980d1baa0088acc004c0540122b3001301a37540110018b20368acc004c01401226464b30013020002801c5901d1bad301e001301a375401116406080c10180acc004c03cc060dd5000c66002603860326ea80064603a603c603c603c603c603c603c603c603c603c603c603c603c0032301d301e301e0019ba5480026e25200494c00400691100a4410040352301d301e301e301e301e301e301e301e301e301e301e0019180e980f180f180f180f180f180f180f180f180f180f180f000c8c074c078c078c078c078c078c078c078c078c07800660306ea801e4603a603c603c603c603c603c603c603c0032301d301e301e301e301e301e301e301e301e0014888888888888c8cc8966002603c00d13298009816000cdd698161816800c8c0b4c0b8c0b8c0b8c0b8c0b8c0b800522259800cc004c09cc0acdd5180918161baa013a50a5140a9159800981180144c966002605060586ea80062b3001337126eb4c0c0c0b4dd50009bad3026302d375402913298009bad303100198189819000ccc020dd6181898171baa01e017488966002604e60606ea800626464b3001302e303237540031332259800998121bab302f30353754605e606a6ea801000a264b3001302c3035375400313232598009811981b9baa001899198090008acc004cdd78008054566002660506eacc0ccc0e4dd5001803456600264b30013370f30013756606660746ea80aa00f00140b4019198009981b1bab3033303a375405400f4a322259800800c56600266e3c00c012266e1c00803e294103b4528207640c914a081c166002605e60706ea8042291103594553008a45024e4f0040dd15980099b893370060366eacc0ccc0e4dd51819981c9baa00800f301b3756606660726ea800e29462c81ba2c81ba2c81ba2c81b8c0ecc0e0dd5000c59036180e181b9baa0013039303637540031640d0660206eb0c068c0d4dd5012919baf3039303637540020051640cc6eb8c0d8c0ccdd5000981b18199baa302d303337540051640c4606a60646ea8c0d4c0c8dd5181618191baa0013034303137540031640bc59800981198161baa0048992cc004c0940062660606ea0004cc0c0c8c8c8c8c8c8c8c8c068cc0e0c0e4020cc0e0c0e401ccc0e0c0e4018cc0e0c0e4014cc0e0c0e4010cc0e0c0e400ccc0e0dd419b81375a6016606c6ea8074024cc0e0dd419b80375a6026606c6ea8074030cc0e0c0e4008cc0e0dd419b80375a602a606c6ea8074024cc0e0c0e4004cc0e0dd419b80375a602c606c6ea8074030c0e8c0e8004c0e4c0e4004c0e0c0e0c0e0004c0dc004c0d8004c0d4004c0d0004c0cc004c0b8dd500aa5eb822c8160cdc09bad3002302d375402866e0cdd6980498169baa014337006eb4c028c0b4dd500a001c4c966002604a0031330303750002660606464646464646464603466070607201066070607200e66070607200c66070607200a660706072008660706072006660706ea0cdc01bad300b3036375403a018660706ea0cdc09bad30133036375403a012660706072004660706072002660706ea0cdc01bad30173036375403a012660706ea0cdc01bad30163036375403a0186074607460740026072002607060706070002606e002606c002606a00260680026066002605c6ea80552f5c11640b066e04dd6980518169baa014337066eb4c024c0b4dd500a19b80375a6004605a6ea805000d02b45902b45902b181798161baa3026302c3754601260586ea80722c81522c815060506ea805a264b300130240078cc0048966002604260546ea800a2646464b30013032002899192cc004c0980062b3001303037540050068b20628acc004c0ac00626464b300130360028044590331bad303400130303754005159800980d800c56600260606ea800a00d1640c51640b8817102e18171baa00130310038b205e32598009817000c5660026022605a00316898149816800a0588b205e37546060002606000260566ea800a2c814a4464b30013004375c605e60600031302f0018b205430020019119198008008019119801800980100148c0b4c0b8c0b8c0b800522223259800cc004c0a4c0b4dd5180a18171baa015a50a5140b113259800981518171baa0018acc004cdc49bad3028302f375402c6eb4c0c8c0bcdd5000c4c966002606800313259800981398181baa0018992cc004c0a0c0c4dd5000c4c966002605260646ea80062b3001323322330020020012259800800c528456600266e3cdd7181c800801c528c4cc008008c0e8005034206e3758602060686ea8090dd7181b18199baa01a8992cc004c0a8c0ccdd5000c4c8c9660026062606a6ea800626644b3001330273756606460706ea8c0c8c0e0dd500200144cc8966002606060726ea800626464b30013027303b37540031323301600115980099baf0010058acc004cc0b0dd5981b981e9baa0030078acc0066002660726eacc0d8c0f4dd5016803d28c88928206a8acc004cdc4980f9bab3037303d3754606e607a6ea8024c07cdd5981b981e9baa0038a518b20768b20768b20768b2076303f303c37540031640e8604060766ea8004c0f4c0e8dd5000c5903819191919191919191919191813998229823005998229823005198229823004998229823004198229823003998229823003198229823002998229823002198229823001998229823001198229823000998229823182380099822cc00528d30103d87a8000a60103d879800041046608a604e6608ab3001337106eb4c05cc10cdd50151bad30463043375402314c0103d87980008a6103d87a8000410497ae04bd701823000982280098220009821800982100098208009820000981f800981f000981e800981c1baa01f330133758603a60706ea80a08cdd7981e181c9baa0010028b206c375c6072606c6ea8004c0e4c0d8dd51818181b1baa0028b20683038303537546070606a6ea8c0bcc0d4dd5000981b981a1baa0018b20643300d3758606c60666ea808c0722c818a2c8188c0d4c0c8dd5000c59030181598189baa3034303137540031640bc660166eb0c054c0c0dd51819800919b87375a606860626ea8004dd6980b18189baa0188b2062323259800981398181baa0018992cc004c0a0c0c4dd5000c4c8cc0200048966002005132330010010042259800800c52f5c113303998009b89480026e3120049b804800e6e2520089b8c480026e3120089b804801d222222233223301623235980099b8f375c608a0029110475d3c793008cc004888c8cc00cdd698248009bae3049304a00130030019b89480426e3120109b804803e600e90082444446600a464660966ea0cde5250375c6098002660966098609a00297ae05980098029b8d0018998251ba930020013304a37526600860066e340040052f5c1164118466446601046609a6ea0cdc7000a40006609a6ea4cc00cc008dc6800800a5eb808cc0248cc138dd419b8e00148000cc138dd49980218019b8d0010014bd70111919198289819998289ba800833051375000a660a260a400297ae033051305230530014bd7018008012cc004c05800a2653001001a5eb82006800888966002608c003123233054374e002660a86ea40092f5c065300100180252f5c080088896600200510018cc00400e60b0005330563057002001400c82aa246530010059982a182a8008024c02400e6eb8c154c1580050054c004c8cc150dd419bca4a06eb8c154004cc150c154c1580052f5c0b30013017371a00313305337526024002660a66ea4cc054c050dc6800800a5eb822c827a44660aa004660aa6ea40052f5c1370e90034dc3a4010911119191919803194c0040066eb0c17000a607a660b660b8010660b698103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80004bd702002222598008014400633001003982f8014c8c96600260a60031323042330603061001330603042330603061305e375400697ae030623062001305d3754007159800982c000c4c8c8c10ccc184c188008cc184c188004cc184c10ccc184c188c17cdd500225eb80c18cc18c004c188004c174dd5001c5660026090003132323230443306230630033306230630023306230630013306230443306230633060375400a97ae03064306400130630013062001305d37540071598009805000c4c8c8c8c8c114cc18cc190010cc18cc19000ccc18cc190008cc18cc190004cc18cc114cc18cc190c184dd500325eb80c194c194004c190004c18c004c188004c174dd5001c56600260120031323232323230463306430650053306430650043306430650033306430650023306430650013306430463306430653062375400e97ae0306630660013065001306400130630013062001305d375400715980099b87480280062646464646464608e660ca60cc00c660ca60cc00a660ca60cc008660ca60cc006660ca60cc004660ca60cc002660ca608e660ca60cc60c66ea80212f5c060ce60ce00260cc00260ca00260c800260c600260c400260ba6ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c120cc198c19c01ccc198c19c018cc198c19c014cc198c19c010cc198c19c00ccc198c19c008cc198c19c004cc198c120cc198c19cc190dd5004a5eb80c1a0c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001c56600266e1d200e00189919191919191919182499833983400419833983400399833983400319833983400299833983400219833983400199833983400119833983400099833982499833983418329baa00a4bd70183498348009834000983380098330009832800983200098318009831000982e9baa0038acc004cdc3a40200031323232323232323232304a33068306900933068306900833068306900733068306900633068306900533068306900433068306900333068306900233068306900133068304a3306830693066375401697ae0306a306a00130690013068001306700130660013065001306400130630013062001305d375400715980099b87480480062646464646464646464646096660d260d4014660d260d4012660d260d4010660d260d400e660d260d400c660d260d400a660d260d4008660d260d4006660d260d4004660d260d4002660d26096660d260d460ce6ea80312f5c060d660d600260d400260d200260d000260ce00260cc00260ca00260c800260c600260c400260ba6ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c130cc1a8c1ac02ccc1a8c1ac028cc1a8c1ac024cc1a8c1ac020cc1a8c1ac01ccc1a8c1ac018cc1a8c1ac014cc1a8c1ac010cc1a8c1ac00ccc1a8c1ac008cc1a8c1ac004cc1a8c130cc1a8c1acc1a0dd5006a5eb80c1b0c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001c56600266e1d20160018991919191919191919191919182699835983600619835983600599835983600519835983600499835983600419835983600399835983600319835983600299835983600219835983600199835983600119835983600099835982699835983618349baa00e4bd701836983680098360009835800983500098348009834000983380098330009832800983200098318009831000982e9baa00389919191919191919191919191826998359836006198359836005998359836005198359836004998359836004198359836003998359836003198359836002998359836002198359836001998359836001198359836000998359836183680099835982699835983618349baa00e4bd7025eb80c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001a0b6416c82d905b20b6416c82d905b20b6416c82d905b182d9baa001305e002400c82e0dd7182d982e000991acc004c08400a2653001001a5eb8200680088896600260a200312323305f374e002660be6ea40092f5c065300100180252f5c080088896600200510018cc00400e60c6005330613062002001400c8302246530010059982f98300008024c05000e6eb8c180c1840050051802800a0b28b20ac375c60b660b8006530012232598009800a40211598009800a4001148002266e3922010801020408102040800000241651598009800a408113370490400219801801980b00144cdc124101010100406600600666e00009203f416482c8dc4000c888c8cc178c00cdd6982f8009982f182f9830000a5eb80c00c006660b46ea0cdc7000a4000660b46ea4cc040c03cdc6800800a5eb8122232329800919802919198311ba833794940dd718318009983118319832000a5eb816600260386e340062660c26ea4c064004cc184dd49980d980d1b8d0010014bd7045905d1191801acc004c158006298103d87a800089822198311ba80014bd7020bc30030019bad3060003911191acc006600260ae6eb4c19000694294505f44cc02000c8c014c118cc190dd4000a5eb822601e600898103d87a8000417c6eb8c190c194004cc188dd419b8e00148000cc188dd49980c180b9b8d0010014bd7024446b30013056002898019182299831800a5eb822b3001305b002898019182b19831800a5eb822b3001304b00289801919ba548010cc18c0052f5c1159800980680144cc01c8c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b30013044371a00313306337526046002660c66ea4cc0a4c0a0dc6800800a5eb822c82f88cdd2a400c660c66ea00052f5c1159800980600144cc01c8c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b30013044371a00313306337526046002660c66ea4cc0a4c0a0dc6800800a5eb822c82f88cdd2a4010660c66ea16600266e20005208080048800c4cdc080098032404082f92f5c115980099b874802800a26006466e95200a330630014bd70456600266e1d200c002899800919ba548030cc18c0052f5c0464660c86ea0c018dd698328009983218329833000a5eb80c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2b30013370e900700144cc0048cdd2a401c660c600297ae023233064375066f29281bae306500133064306530660014bd702cc004c078dc6800c4cc18cdd4980d800998319ba93301d301c371a00200297ae08b20be8acc004cdc3a402000513300123374a900819831800a5eb808c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2b30013370e9009001448c8c8cc194cdd2a4024660ca60cc00297ae033065306630670014bd70180080119804119198329ba833794940dd718330009983298331833800a5eb8166002608a6e340062660c86ea4c090004cc190dd49981518149b8d0010014bd7045906012cc004c1600062980103d87980008acc004c174006298103d87a80008acc004c134006298103d87b80008acc004c03c006298103d87c80008acc004c038006298103d87d80008b20c04180830106020c08acc004cdc3a40280051300323374a900a19831800a5eb822b30013370e900b00144c00c8cdd2a402c660c600297ae08acc004cdc3a403000513300123374a900c19831800a5eb808c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2c82f105e20bc417882f105e20bc417882f105e20bc417882f0dd718319832003096600266e2000520808080808080808080028800c4cdc08009801241000282d8c00c00c6eb4c164004c8cc160dd419b8e00148000cc160dd49980718069b8d0010014bd701bae30583059004209c8b2096371890011b80480051641006eb8c114c11800566002600e6e340062660866ea4c008004cc10cdd49980298021b8d0010014bd7045903f1bae3025304037546602c4646b30013371e6eb8c11400522104b9011a8200891919191919191991198269817998269827003998269ba90013304d304e0024bd70198269827182780125eb80d660026024003125980099b89002371a00313304c37526601e004002660986ea66002005337026e3400400a002b8c25eb822c82422c8238dd7182618268011bae304c004375a609600264660946ea0cde5250375c6096002660946096609800297ae05980098151b8d0018998249ba930090013304937526601e601c6e340040052f5c11641146eb8c124c128004d6600294624b30013371290201b8d0018998241ba93300b48100004cc120dd4cc005204099b80371a002901fc0057184bd704590444590431bae30483049001300100259800a51892cc004cdc4a4100026e3400626608c6ea4cc0252080010013304637533001482000666e00dc6800a40ff0015c612f5c11641091641051641006eb8c114c11800566002600e6e340062660866ea4c008004cc10cdd49980298021b8d0010014bd7045903f1bae304300a300348010c00d200819801001181d800a0708991801181d0019bae303800240d86eb0c0d4c0c8dd5000c5903019199119801001000912cc004006298103d87a80008992cc004cdd7802181a000c4c064cc0dcc0d40052f5c1133003003303900240cc606e00281a8dd5980898191baa0223374a900219819981319819980298189baa30343031375400297ae04bd7045902f19198008009bac302b3031375404244b30010018a6103d87a80008992cc006600266ebc00530103d87a8000a50a5140c51001899801801981b8012062325980099912cc00400a2942265300132598009816981b1baa00189bad3037303a37566074606e6ea800629000206a30390019baf3039303a0019bab003488966002003159800980100344cc01400d20008a5040dd1325980099baf30390014c010140008acc004cc018010dd6981d181e9bab303a001898019ba6303e0028a5040e11598009980300224001130030078a5040e081c0c0f000503a0ca600200337566060606c6ea8c0c0c0d8dd5002488cc0e8008cc0e8dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0e00066eacc0e4006607a0069112cc004cdc8a441000038acc004c05000e26600a60406607c6e980092f5c00031330054c103d87a800000640e919800803c006446600e0046608066ec0dd48029ba6004001401c81d0607600481ca29422942294103a1ba633035337606ea4010dd31981a99bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c566002605c60646ea8c0d8c0ccdd5181b18199baa302d3033375400313259800980f98199baa0018992cc004c0acc0d0dd5000c4c8c8c8c8cc8966002607e007132598009819181d9baa0018991919194c004dd69821800cdd718218024dd69821801cdd7182180124444b3001304800589980c9bab304700e225980080144cc06c03089660020051302c3304a0144bd7044c8c8cc074c1240084c00cc138010dd7182480098258012092899191980d98240010980198260021bae30460013049002411d164114304300130420013041001303c37540031640e8607c00d1640f06eb8c0f0004dd5981e001181e000981d800981d000981a9baa0018b20663037303437540031640c8603060666ea8c0b4c0ccdd5000c590314530103d87a800040c4606a0028198dd7181498179baa0168b205a8b205a3031302e37546062605c6ea8c02cc0b8dd500f45902c112cc004c094c0b8dd500144c8c8c966002606c00513300930350031330090010048b206630340013034001302f37540051640b4899912cc004c098c0a8dd5180898159baa0128992cc004c088c0acdd5000c4c966002604800913232980099b833370400c6eb4c03cc0bcdd500b000ccc024dd6181918179baa01f0189bae3032002488966002605060626ea800a26464b3001302f303337540031332259800998129bab3030303637546060606c6ea801000a264b30013370e01201d159800cc00400e00d337029000007528c09d00d4528c5903544c966002605c606e6ea800626464b30013025303937540031323301400115980099baf0010058acc004cc0a8dd5981a981d9baa0030078acc006600200f00a99b814800004a942056808a2b30013371266e04c074dd5981a981d9baa3035303b3754012018603a6eacc0d4c0ecdd5001c528c59039459039459039459039181e981d1baa0018b2070301e30393754002607660706ea80062c81b0cc048dd6180e181b9baa02723375e607660706ea800400d0352cc004c0b0c0d4dd500544c8c8c8c8c8c8c8c8c8c8c090cc108c10c028cc108c10c024cc108c10c020cc108c10c01ccc108c10c018cc108c10c014cc108c10c010cc108c10c00ccc108c10c008cc108dd419b81375a603e60806ea809c05ccc108c10c004cc108dd419b81375a604060806ea809c044c110c110004c10cc10c004c108004c104004c100004c0fc004c0f8004c0f4004c0f0004c0ec004c0d8dd500ec4c8c8c8c8c8c8c8c8c8c8c090cc108c10c028cc108c10c024cc108c10c020cc108c10c01ccc108c10c018cc108c10c014cc108c10c010cc108c10c00ccc108c10c008cc108c10c004cc108dd419b81375a604260806ea809c05ccc108dd419b81375a604060806ea809c044c110c110c110004c10c004c108004c104004c100004c0fc004c0f8004c0f4004c0f0004c0ec004c0d8dd500ea0688b2068375c606e60686ea8004c0dcc0d0dd51817181a1baa0028b2064303630333754606c60666ea8c0b4c0ccdd5000981a98191baa0028b20601bad3031303200159800981198161baa001899817a610443594553003302f300c302d375402897ae0899817a6103424e4f003302f300e302d375402897ae040ad1640ac605e60586ea80062c8150c0b8c0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0acdd50094590291bad302c3029375402e44444b30013370f30013756604e605c6ea800600b0044084007159800acc00400a266e1e60026eacc09cc0b8dd5000c0169110040849000c4c09260026eacc09cc0b8dd5000c01691010040848162330013302a3756604e605c6ea8004016946444b30010018acc004cdc7801803c4cdc3801003456600200b1598009804801c4cdc38012400314a0817a294102f205e8a5040bc8132294102c45282058409c6e3d22010040984466006004466ebcc0b0c0a4dd5000801112cc004c078c09cdd500144c8c8c8c8c8c8c8c8c8c8c8c8ca600264b300130360018acc004c064c0d40062d13031303500140d11640dc6ea8c0e00066072003375c607001b375c6070019375a6070017375a6070015375a6070013375c6070011375a607000f375a607000d375a607000b375a6070009375a6070007375a6070004911111111111112cc004c11c03626464b3001303b0018992cc004c128006266064609200203f16411c608a6ea800a2b300130400018acc004c114dd5001407a2c82322c821904318219baa001304600d8b2088181c000981b800981b000981a800981a000981980098190009818800981800098178009817000981680098141baa0028b204c2232330010010032259800800c530103d87a80008992cc004c0100062601c6605800297ae089980180198170012050302c00140a88b202e301a004301a301b004456600260120091323322598009806000c566002602c6ea801200516405d1598009808800c566002602c6ea801200516405d16405080a0566002601460266ea800a330012232330010010032259800800c52845660026006603600314a3133002002301c001405880ca602e60286ea806f30013756601a60286ea801200348900401c9112cc0066002660266eacc040c05cdd5003802528c88966002003130110028a504060807a2b30013300337586034602e6ea801c8cdd7980d980c1baa0010038acc004c04800629462c80aa2b3001300d0018998019bac301a3017375400e4b30013375e603660306ea8c06cc060dd51809180c1baa001300d3301a375200a97ae08998039bab301230183754602460306ea80040162941016459015202a8b202a466002660206eacc034c050dd5002000d28c8896600200313371000490004528202a40308090c04cdd50011bae30163013375400b16404480886022602400460200124446466446600400400244b3001001801c4c8cc896600266e4401c00a2b30013371e00e00510018032026899802802980c8022026375c60240026eb4c04c004c054005013198060020018a40008a4d13656400401", + "hash": "a299f37e4b0c30c7e131b76bfe3669d9a4d73b10d8fbc175691789b5" + }, + { + "title": "market.market.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "params", + "schema": { + "$ref": "#/definitions/prediction_market~1types~1MarketParams" + } + } + ], + "compiledCode": "59248e010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a48888888966003300130033754013223232330010010042259800800c00e2646644b30013372200e00515980099b8f0070028800c01900944cc014014c03c0110091bae3008001375660120026016002804852f5bded8c1370e900148c01cc0200064600e6010601060106010003222329800800c01200680088896600200510018994c004012601c00798009bae30090019bad300a001801200a4010601800480526e2120009b87480026e9520024888888888cc88ca6002601c6ea8006602401922300c9800801400691100401130120034888966002601c0091980098099baa00591192cc004c0300062b3001301637540070028b202e8acc004c0440062b3001301637540070028b202e8b2028405060286ea800a6e1d200498091baa00148888cc88c9660026020009132323259800981080144cc024c08000c0122c80f0dd6980f800980f800980d1baa0088acc004c0540122b3001301a37540110018b20368acc004c01401226464b30013020002801c5901d1bad301e001301a375401116406080c10180acc004c03cc060dd5000c66002603860326ea80064603a603c603c603c603c603c603c603c603c603c603c603c603c0032301d301e301e0019ba5480026e25200494c00400691100a4410040352301d301e301e301e301e301e301e301e301e301e301e0019180e980f180f180f180f180f180f180f180f180f180f180f000c8c074c078c078c078c078c078c078c078c078c07800660306ea801e4603a603c603c603c603c603c603c603c0032301d301e301e301e301e301e301e301e301e0014888888888888c8cc8966002603c00d13298009816000cdd698161816800c8c0b4c0b8c0b8c0b8c0b8c0b8c0b800522259800cc004c09cc0acdd5180918161baa013a50a5140a9159800981180144c966002605060586ea80062b3001337126eb4c0c0c0b4dd50009bad3026302d375402913298009bad303100198189819000ccc020dd6181898171baa01e017488966002604e60606ea800626464b3001302e303237540031332259800998121bab302f30353754605e606a6ea801000a264b3001302c3035375400313232598009811981b9baa001899198090008acc004cdd78008054566002660506eacc0ccc0e4dd5001803456600264b30013370f30013756606660746ea80aa00f00140b4019198009981b1bab3033303a375405400f4a322259800800c56600266e3c00c012266e1c00803e294103b4528207640c914a081c166002605e60706ea8042291103594553008a45024e4f0040dd15980099b893370060366eacc0ccc0e4dd51819981c9baa00800f301b3756606660726ea800e29462c81ba2c81ba2c81ba2c81b8c0ecc0e0dd5000c59036180e181b9baa0013039303637540031640d0660206eb0c068c0d4dd5012919baf3039303637540020051640cc6eb8c0d8c0ccdd5000981b18199baa302d303337540051640c4606a60646ea8c0d4c0c8dd5181618191baa0013034303137540031640bc59800981198161baa0048992cc004c0940062660606ea0004cc0c0c8c8c8c8c8c8c8c8c068cc0e0c0e4020cc0e0c0e401ccc0e0c0e4018cc0e0c0e4014cc0e0c0e4010cc0e0c0e400ccc0e0dd419b81375a6016606c6ea8074024cc0e0dd419b80375a6026606c6ea8074030cc0e0c0e4008cc0e0dd419b80375a602a606c6ea8074024cc0e0c0e4004cc0e0dd419b80375a602c606c6ea8074030c0e8c0e8004c0e4c0e4004c0e0c0e0c0e0004c0dc004c0d8004c0d4004c0d0004c0cc004c0b8dd500aa5eb822c8160cdc09bad3002302d375402866e0cdd6980498169baa014337006eb4c028c0b4dd500a001c4c966002604a0031330303750002660606464646464646464603466070607201066070607200e66070607200c66070607200a660706072008660706072006660706ea0cdc01bad300b3036375403a018660706ea0cdc09bad30133036375403a012660706072004660706072002660706ea0cdc01bad30173036375403a012660706ea0cdc01bad30163036375403a0186074607460740026072002607060706070002606e002606c002606a00260680026066002605c6ea80552f5c11640b066e04dd6980518169baa014337066eb4c024c0b4dd500a19b80375a6004605a6ea805000d02b45902b45902b181798161baa3026302c3754601260586ea80722c81522c815060506ea805a264b300130240078cc0048966002604260546ea800a2646464b30013032002899192cc004c0980062b3001303037540050068b20628acc004c0ac00626464b300130360028044590331bad303400130303754005159800980d800c56600260606ea800a00d1640c51640b8817102e18171baa00130310038b205e32598009817000c5660026022605a00316898149816800a0588b205e37546060002606000260566ea800a2c814a4464b30013004375c605e60600031302f0018b205430020019119198008008019119801800980100148c0b4c0b8c0b8c0b800522223259800cc004c0a4c0b4dd5180a18171baa015a50a5140b113259800981518171baa0018acc004cdc49bad3028302f375402c6eb4c0c8c0bcdd5000c4c966002606800313259800981398181baa0018992cc004c0a0c0c4dd5000c4c966002605260646ea80062b3001323322330020020012259800800c528456600266e3cdd7181c800801c528c4cc008008c0e8005034206e3758602060686ea8090dd7181b18199baa01a8992cc004c0a8c0ccdd5000c4c8c9660026062606a6ea800626644b3001330273756606460706ea8c0c8c0e0dd500200144cc8966002606060726ea800626464b30013027303b37540031323301600115980099baf0010058acc004cc0b0dd5981b981e9baa0030078acc0066002660726eacc0d8c0f4dd5016803d28c88928206a8acc004cdc4980f9bab3037303d3754606e607a6ea8024c07cdd5981b981e9baa0038a518b20768b20768b20768b2076303f303c37540031640e8604060766ea8004c0f4c0e8dd5000c5903819191919191919191919191813998229823005998229823005198229823004998229823004198229823003998229823003198229823002998229823002198229823001998229823001198229823000998229823182380099822cc00528d30103d87a8000a60103d879800041046608a604e6608ab3001337106eb4c05cc10cdd50151bad30463043375402314c0103d87980008a6103d87a8000410497ae04bd701823000982280098220009821800982100098208009820000981f800981f000981e800981c1baa01f330133758603a60706ea80a08cdd7981e181c9baa0010028b206c375c6072606c6ea8004c0e4c0d8dd51818181b1baa0028b20683038303537546070606a6ea8c0bcc0d4dd5000981b981a1baa0018b20643300d3758606c60666ea808c0722c818a2c8188c0d4c0c8dd5000c59030181598189baa3034303137540031640bc660166eb0c054c0c0dd51819800919b87375a606860626ea8004dd6980b18189baa0188b2062323259800981398181baa0018992cc004c0a0c0c4dd5000c4c8cc0200048966002005132330010010042259800800c52f5c113303998009b89480026e3120049b804800e6e2520089b8c480026e3120089b804801d222222233223301623235980099b8f375c608a0029110475d3c793008cc004888c8cc00cdd698248009bae3049304a00130030019b89480426e3120109b804803e600e90082444446600a464660966ea0cde5250375c6098002660966098609a00297ae05980098029b8d0018998251ba930020013304a37526600860066e340040052f5c1164118466446601046609a6ea0cdc7000a40006609a6ea4cc00cc008dc6800800a5eb808cc0248cc138dd419b8e00148000cc138dd49980218019b8d0010014bd70111919198289819998289ba800833051375000a660a260a400297ae033051305230530014bd7018008012cc004c05800a2653001001a5eb82006800888966002608c003123233054374e002660a86ea40092f5c065300100180252f5c080088896600200510018cc00400e60b0005330563057002001400c82aa246530010059982a182a8008024c02400e6eb8c154c1580050054c004c8cc150dd419bca4a06eb8c154004cc150c154c1580052f5c0b30013017371a00313305337526024002660a66ea4cc054c050dc6800800a5eb822c827a44660aa004660aa6ea40052f5c1370e90034dc3a4010911119191919803194c0040066eb0c17000a607a660b660b8010660b698103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80003305b4c103d87a80004bd702002222598008014400633001003982f8014c8c96600260a60031323042330603061001330603042330603061305e375400697ae030623062001305d3754007159800982c000c4c8c8c10ccc184c188008cc184c188004cc184c10ccc184c188c17cdd500225eb80c18cc18c004c188004c174dd5001c5660026090003132323230443306230630033306230630023306230630013306230443306230633060375400a97ae03064306400130630013062001305d37540071598009805000c4c8c8c8c8c114cc18cc190010cc18cc19000ccc18cc190008cc18cc190004cc18cc114cc18cc190c184dd500325eb80c194c194004c190004c18c004c188004c174dd5001c56600260120031323232323230463306430650053306430650043306430650033306430650023306430650013306430463306430653062375400e97ae0306630660013065001306400130630013062001305d375400715980099b87480280062646464646464608e660ca60cc00c660ca60cc00a660ca60cc008660ca60cc006660ca60cc004660ca60cc002660ca608e660ca60cc60c66ea80212f5c060ce60ce00260cc00260ca00260c800260c600260c400260ba6ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c120cc198c19c01ccc198c19c018cc198c19c014cc198c19c010cc198c19c00ccc198c19c008cc198c19c004cc198c120cc198c19cc190dd5004a5eb80c1a0c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001c56600266e1d200e00189919191919191919182499833983400419833983400399833983400319833983400299833983400219833983400199833983400119833983400099833982499833983418329baa00a4bd70183498348009834000983380098330009832800983200098318009831000982e9baa0038acc004cdc3a40200031323232323232323232304a33068306900933068306900833068306900733068306900633068306900533068306900433068306900333068306900233068306900133068304a3306830693066375401697ae0306a306a00130690013068001306700130660013065001306400130630013062001305d375400715980099b87480480062646464646464646464646096660d260d4014660d260d4012660d260d4010660d260d400e660d260d400c660d260d400a660d260d4008660d260d4006660d260d4004660d260d4002660d26096660d260d460ce6ea80312f5c060d660d600260d400260d200260d000260ce00260cc00260ca00260c800260c600260c400260ba6ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c130cc1a8c1ac02ccc1a8c1ac028cc1a8c1ac024cc1a8c1ac020cc1a8c1ac01ccc1a8c1ac018cc1a8c1ac014cc1a8c1ac010cc1a8c1ac00ccc1a8c1ac008cc1a8c1ac004cc1a8c130cc1a8c1acc1a0dd5006a5eb80c1b0c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001c56600266e1d20160018991919191919191919191919182699835983600619835983600599835983600519835983600499835983600419835983600399835983600319835983600299835983600219835983600199835983600119835983600099835982699835983618349baa00e4bd701836983680098360009835800983500098348009834000983380098330009832800983200098318009831000982e9baa00389919191919191919191919191826998359836006198359836005998359836005198359836004998359836004198359836003998359836003198359836002998359836002198359836001998359836001198359836000998359836183680099835982699835983618349baa00e4bd7025eb80c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c190004c18c004c188004c174dd5001a0b6416c82d905b20b6416c82d905b20b6416c82d905b182d9baa001305e002400c82e0dd7182d982e000991acc004c08400a2653001001a5eb8200680088896600260a200312323305f374e002660be6ea40092f5c065300100180252f5c080088896600200510018cc00400e60c6005330613062002001400c8302246530010059982f98300008024c05000e6eb8c180c1840050051802800a0b28b20ac375c60b660b8006530012232598009800a40211598009800a4001148002266e3922010801020408102040800000241651598009800a408113370490400219801801980b00144cdc124101010100406600600666e00009203f416482c8dc4000c888c8cc178c00cdd6982f8009982f182f9830000a5eb80c00c006660b46ea0cdc7000a4000660b46ea4cc040c03cdc6800800a5eb8122232329800919802919198311ba833794940dd718318009983118319832000a5eb816600260386e340062660c26ea4c064004cc184dd49980d980d1b8d0010014bd7045905d1191801acc004c158006298103d87a800089822198311ba80014bd7020bc30030019bad3060003911191acc006600260ae6eb4c19000694294505f44cc02000c8c014c118cc190dd4000a5eb822601e600898103d87a8000417c6eb8c190c194004cc188dd419b8e00148000cc188dd49980c180b9b8d0010014bd7024446b30013056002898019182299831800a5eb822b3001305b002898019182b19831800a5eb822b3001304b00289801919ba548010cc18c0052f5c1159800980680144cc01c8c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b30013044371a00313306337526046002660c66ea4cc0a4c0a0dc6800800a5eb822c82f88cdd2a400c660c66ea00052f5c1159800980600144cc01c8c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b30013044371a00313306337526046002660c66ea4cc0a4c0a0dc6800800a5eb822c82f88cdd2a4010660c66ea16600266e20005208080048800c4cdc080098032404082f92f5c115980099b874802800a26006466e95200a330630014bd70456600266e1d200c002899800919ba548030cc18c0052f5c0464660c86ea0c018dd698328009983218329833000a5eb80c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2b30013370e900700144cc0048cdd2a401c660c600297ae023233064375066f29281bae306500133064306530660014bd702cc004c078dc6800c4cc18cdd4980d800998319ba93301d301c371a00200297ae08b20be8acc004cdc3a402000513300123374a900819831800a5eb808c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2b30013370e9009001448c8c8cc194cdd2a4024660ca60cc00297ae033065306630670014bd70180080119804119198329ba833794940dd718330009983298331833800a5eb8166002608a6e340062660c86ea4c090004cc190dd49981518149b8d0010014bd7045906012cc004c1600062980103d87980008acc004c174006298103d87a80008acc004c134006298103d87b80008acc004c03c006298103d87c80008acc004c038006298103d87d80008b20c04180830106020c08acc004cdc3a40280051300323374a900a19831800a5eb822b30013370e900b00144c00c8cdd2a402c660c600297ae08acc004cdc3a403000513300123374a900c19831800a5eb808c8cc190dd419bca4a06eb8c194004cc190c194c1980052f5c0b3001301e371a00313306337526036002660c66ea4cc074c070dc6800800a5eb822c82fa2c82f105e20bc417882f105e20bc417882f105e20bc417882f0dd718319832003096600266e2000520808080808080808080028800c4cdc08009801241000282d8c00c00c6eb4c164004c8cc160dd419b8e00148000cc160dd49980718069b8d0010014bd701bae30583059004209c8b2096371890011b80480051641006eb8c114c11800566002600e6e340062660866ea4c008004cc10cdd49980298021b8d0010014bd7045903f1bae3025304037546602c4646b30013371e6eb8c11400522104b9011a8200891919191919191991198269817998269827003998269ba90013304d304e0024bd70198269827182780125eb80d660026024003125980099b89002371a00313304c37526601e004002660986ea66002005337026e3400400a002b8c25eb822c82422c8238dd7182618268011bae304c004375a609600264660946ea0cde5250375c6096002660946096609800297ae05980098151b8d0018998249ba930090013304937526601e601c6e340040052f5c11641146eb8c124c128004d6600294624b30013371290201b8d0018998241ba93300b48100004cc120dd4cc005204099b80371a002901fc0057184bd704590444590431bae30483049001300100259800a51892cc004cdc4a4100026e3400626608c6ea4cc0252080010013304637533001482000666e00dc6800a40ff0015c612f5c11641091641051641006eb8c114c11800566002600e6e340062660866ea4c008004cc10cdd49980298021b8d0010014bd7045903f1bae304300a300348010c00d200819801001181d800a0708991801181d0019bae303800240d86eb0c0d4c0c8dd5000c5903019199119801001000912cc004006298103d87a80008992cc004cdd7802181a000c4c064cc0dcc0d40052f5c1133003003303900240cc606e00281a8dd5980898191baa0223374a900219819981319819980298189baa30343031375400297ae04bd7045902f19198008009bac302b3031375404244b30010018a6103d87a80008992cc006600266ebc00530103d87a8000a50a5140c51001899801801981b8012062325980099912cc00400a2942265300132598009816981b1baa00189bad3037303a37566074606e6ea800629000206a30390019baf3039303a0019bab003488966002003159800980100344cc01400d20008a5040dd1325980099baf30390014c010140008acc004cc018010dd6981d181e9bab303a001898019ba6303e0028a5040e11598009980300224001130030078a5040e081c0c0f000503a0ca600200337566060606c6ea8c0c0c0d8dd5002488cc0e8008cc0e8dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0e00066eacc0e4006607a0069112cc004cdc8a441000038acc004c05000e26600a60406607c6e980092f5c00031330054c103d87a800000640e919800803c006446600e0046608066ec0dd48029ba6004001401c81d0607600481ca29422942294103a1ba633035337606ea4010dd31981a99bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c566002605c60646ea8c0d8c0ccdd5181b18199baa302d3033375400313259800980f98199baa0018992cc004c0acc0d0dd5000c4c8c8c8c8cc8966002607e007132598009819181d9baa0018991919194c004dd69821800cdd718218024dd69821801cdd7182180124444b3001304800589980c9bab304700e225980080144cc06c03089660020051302c3304a0144bd7044c8c8cc074c1240084c00cc138010dd7182480098258012092899191980d98240010980198260021bae30460013049002411d164114304300130420013041001303c37540031640e8607c00d1640f06eb8c0f0004dd5981e001181e000981d800981d000981a9baa0018b20663037303437540031640c8603060666ea8c0b4c0ccdd5000c590314530103d87a800040c4606a0028198dd7181498179baa0168b205a8b205a3031302e37546062605c6ea8c02cc0b8dd500f45902c112cc004c094c0b8dd500144c8c8c966002606c00513300930350031330090010048b206630340013034001302f37540051640b4899912cc004c098c0a8dd5180898159baa0128992cc004c088c0acdd5000c4c966002604800913232980099b833370400c6eb4c03cc0bcdd500b000ccc024dd6181918179baa01f0189bae3032002488966002605060626ea800a26464b3001302f303337540031332259800998129bab3030303637546060606c6ea801000a264b30013370e01201d159800cc00400e00d337029000007528c09d00d4528c5903544c966002605c606e6ea800626464b30013025303937540031323301400115980099baf0010058acc004cc0a8dd5981a981d9baa0030078acc006600200f00a99b814800004a942056808a2b30013371266e04c074dd5981a981d9baa3035303b3754012018603a6eacc0d4c0ecdd5001c528c59039459039459039459039181e981d1baa0018b2070301e30393754002607660706ea80062c81b0cc048dd6180e181b9baa02723375e607660706ea800400d0352cc004c0b0c0d4dd500544c8c8c8c8c8c8c8c8c8c8c090cc108c10c028cc108c10c024cc108c10c020cc108c10c01ccc108c10c018cc108c10c014cc108c10c010cc108c10c00ccc108c10c008cc108dd419b81375a603e60806ea809c05ccc108c10c004cc108dd419b81375a604060806ea809c044c110c110004c10cc10c004c108004c104004c100004c0fc004c0f8004c0f4004c0f0004c0ec004c0d8dd500ec4c8c8c8c8c8c8c8c8c8c8c090cc108c10c028cc108c10c024cc108c10c020cc108c10c01ccc108c10c018cc108c10c014cc108c10c010cc108c10c00ccc108c10c008cc108c10c004cc108dd419b81375a604260806ea809c05ccc108dd419b81375a604060806ea809c044c110c110c110004c10c004c108004c104004c100004c0fc004c0f8004c0f4004c0f0004c0ec004c0d8dd500ea0688b2068375c606e60686ea8004c0dcc0d0dd51817181a1baa0028b2064303630333754606c60666ea8c0b4c0ccdd5000981a98191baa0028b20601bad3031303200159800981198161baa001899817a610443594553003302f300c302d375402897ae0899817a6103424e4f003302f300e302d375402897ae040ad1640ac605e60586ea80062c8150c0b8c0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0bcc0acdd50094590291bad302c3029375402e44444b30013370f30013756604e605c6ea800600b0044084007159800acc00400a266e1e60026eacc09cc0b8dd5000c0169110040849000c4c09260026eacc09cc0b8dd5000c01691010040848162330013302a3756604e605c6ea8004016946444b30010018acc004cdc7801803c4cdc3801003456600200b1598009804801c4cdc38012400314a0817a294102f205e8a5040bc8132294102c45282058409c6e3d22010040984466006004466ebcc0b0c0a4dd5000801112cc004c078c09cdd500144c8c8c8c8c8c8c8c8c8c8c8c8ca600264b300130360018acc004c064c0d40062d13031303500140d11640dc6ea8c0e00066072003375c607001b375c6070019375a6070017375a6070015375a6070013375c6070011375a607000f375a607000d375a607000b375a6070009375a6070007375a6070004911111111111112cc004c11c03626464b3001303b0018992cc004c128006266064609200203f16411c608a6ea800a2b300130400018acc004c114dd5001407a2c82322c821904318219baa001304600d8b2088181c000981b800981b000981a800981a000981980098190009818800981800098178009817000981680098141baa0028b204c2232330010010032259800800c530103d87a80008992cc004c0100062601c6605800297ae089980180198170012050302c00140a88b202e301a004301a301b004456600260120091323322598009806000c566002602c6ea801200516405d1598009808800c566002602c6ea801200516405d16405080a0566002601460266ea800a330012232330010010032259800800c52845660026006603600314a3133002002301c001405880ca602e60286ea806f30013756601a60286ea801200348900401c9112cc0066002660266eacc040c05cdd5003802528c88966002003130110028a504060807a2b30013300337586034602e6ea801c8cdd7980d980c1baa0010038acc004c04800629462c80aa2b3001300d0018998019bac301a3017375400e4b30013375e603660306ea8c06cc060dd51809180c1baa001300d3301a375200a97ae08998039bab301230183754602460306ea80040162941016459015202a8b202a466002660206eacc034c050dd5002000d28c8896600200313371000490004528202a40308090c04cdd50011bae30163013375400b16404480886022602400460200124446466446600400400244b3001001801c4c8cc896600266e4401c00a2b30013371e00e00510018032026899802802980c8022026375c60240026eb4c04c004c054005013198060020018a40008a4d13656400401", + "hash": "a299f37e4b0c30c7e131b76bfe3669d9a4d73b10d8fbc175691789b5" + }, + { + "title": "pyth_test.pyth_test.withdraw", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "pyth_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "59157f010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae002488888888966003300130043754015370e90034dc3a4001370e9002244446465300130093754003300d00698068012444b300130060038cc004c040c034dd500248c044c0480064602260246024003374a90004dc3a400491111192cc004c05c006264b3001300e30133754003132598009807980a1baa0018acc004c03cc050dd5180c180a9baa0018a518b20268b2026300630143754602e60286ea80062c8090c8cc004004dd61802980a1baa30170022259800800c530103d87a80008992cc004c014dd6980c980b1baa001898031980c000a5eb82266006006603400480a0c0600050164590141919199119912cc004c048c05cdd500144c966002602660306ea80062646600a00244b300100289919800800802112cc004006297ae08998104c004dc4a4001371890024dc024007371290044dc624001371890044dc02400e91111111991198081191acc004cdc79bae302c0014890475d3c793008cc004888c8cc00cdd698180009bae3030303100130030019b89480426e3120109b804803e600e90082444446600a464660646ea0cde5250375c6066002660646066606800297ae05980098029b8d0018998189ba930020013303137526600860066e340040052f5c11640b446644660104660686ea0cdc7000a4000660686ea4cc00cc008dc6800800a5eb808cc0248cc0d4dd419b8e00148000cc0d4dd49980218019b8d0010014bd701119191981c18131981c1ba800833038375000a66070607200297ae0330383039303a0014bd7018008012cc004c05800a2653001001a5eb82006800888966002606200312323303b374e002660766ea40092f5c065300100180252f5c080088896600200510018cc00400e607e0053303d303e002001400c81e2246530010059981d981e0008024c02400e6eb8c0f0c0f400500519194c004dd6981e800cdc3a4011223303e0023303e375200297ae04888c8cc008ca600200337586084005302f330413042007330414c0103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a80004bd70200222259800801440063300100398228014c8c966002607a00313230343304630470013304630343304630473044375400697ae030483048001304337540071598009819000c4c8c8c0d4cc11cc120008cc11cc120004cc11cc0d4cc11cc120c114dd500225eb80c124c124004c120004c10cdd5001c5660026078003132323230363304830490033304830490023304830490013304830363304830493046375400a97ae0304a304a0013049001304800130433754007159800981f000c4c8c8c8c8c0dccc124c128010cc124c12800ccc124c128008cc124c128004cc124c0dccc124c128c11cdd500325eb80c12cc12c004c128004c124004c120004c10cdd5001c56600260100031323232323230383304a304b0053304a304b0043304a304b0033304a304b0023304a304b0013304a30383304a304b3048375400e97ae0304c304c001304b001304a001304900130480013043375400715980099b87480280062646464646464607266096609800c66096609800a66096609800866096609800666096609800466096609800266096607266096609860926ea80212f5c0609a609a0026098002609600260940026092002609000260866ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c0e8cc130c13401ccc130c134018cc130c134014cc130c134010cc130c13400ccc130c134008cc130c134004cc130c0e8cc130c134c128dd5004a5eb80c138c138004c134004c130004c12c004c128004c124004c120004c10cdd5001c56600266e1d200e00189919191919191919181d99826982700419826982700399826982700319826982700299826982700219826982700199826982700119826982700099826981d99826982718259baa00a4bd7018279827800982700098268009826000982580098250009824800982400098219baa0038acc004cdc3a40200031323232323232323232303c3304e304f0093304e304f0083304e304f0073304e304f0063304e304f0053304e304f0043304e304f0033304e304f0023304e304f0013304e303c3304e304f304c375401697ae030503050001304f001304e001304d001304c001304b001304a001304900130480013043375400715980099b8748048006264646464646464646464607a6609e60a00146609e60a00126609e60a00106609e60a000e6609e60a000c6609e60a000a6609e60a00086609e60a00066609e60a00046609e60a00026609e607a6609e60a0609a6ea80312f5c060a260a200260a0002609e002609c002609a0026098002609600260940026092002609000260866ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c0f8cc140c14402ccc140c144028cc140c144024cc140c144020cc140c14401ccc140c144018cc140c144014cc140c144010cc140c14400ccc140c144008cc140c144004cc140c0f8cc140c144c138dd5006a5eb80c148c148004c144004c140004c13c004c138004c134004c130004c12c004c128004c124004c120004c10cdd5001c56600266e1d20160018991919191919191919191919181f99828982900619828982900599828982900519828982900499828982900419828982900399828982900319828982900299828982900219828982900199828982900119828982900099828981f99828982918279baa00e4bd70182998298009829000982880098280009827800982700098268009826000982580098250009824800982400098219baa0038991919191919191919191919181f998289829006198289829005998289829005198289829004998289829004198289829003998289829003198289829002998289829002198289829001998289829001198289829000998289829182980099828981f99828982918279baa00e4bd7025eb80c148004c144004c140004c13c004c138004c134004c130004c12c004c128004c124004c120004c10cdd5001a0824104820904120824104820904120824104820904118209baa0013044002400c8210dd718209821000991acc004c0800122653001001a5eb8200a8008889660026076003123233045374e0026608a6ea40092f5c065300100180252f5c080088896600200510018cc00400e6092005330473048002001400c8232246530010059982298230008024c04c00e6eb8c118c11c0050051802800a07e8b2078375c6082608400a530012232598009800a40211598009800a4001148002266e3922010801020408102040800000240fd1598009800a408113370490400219801801980a80144cdc124101010100406600600666e00009203f40fc81f8dc4000c888c8cc110c00cdd698228009982218229823000a5eb80c00c006660806ea0cdc7000a4000660806ea4cc03cc038dc6800800a5eb8122232329800919802919198241ba833794940dd718248009982418249825000a5eb816600260366e3400626608e6ea4c060004cc11cdd49980d180c9b8d0010014bd704590431191801acc004c100006298103d87a80008981b198241ba80014bd70208830030019bad3046003911191acc006600260826eb4c12800694294504544cc02000c8c014c0e0cc128dd4000a5eb8226016600898103d87a800041146eb8c128c12c004cc120dd419b8e00148000cc120dd49980b980b1b8d0010014bd7024446b30013040002898019181b99824800a5eb822b3001303500289801919ba548008cc1240052f5c1159800981f80144c00c8cdd2a40086609200297ae08acc004c10400a26600e464660946ea0cde5250375c6096002660946096609800297ae059800981a9b8d0018998249ba9302200133049375266050604e6e340040052f5c1164114466e95200633049375000297ae08acc004c02c00a26600e464660946ea0cde5250375c6096002660946096609800297ae059800981a9b8d0018998249ba9302200133049375266050604e6e340040052f5c1164114466e952008330493750b3001337100029040400244006266e04004c0192020411497ae08acc004cdc3a40140051300323374a900519824800a5eb822b30013370e900600144cc0048cdd2a40186609200297ae02323304a3750600c6eb4c12c004cc128c12cc1300052f5c064660946ea0cde5250375c6096002660946096609800297ae059800980e9b8d0018998249ba9301a0013304937526603860366e340040052f5c116411515980099b874803800a266002466e95200e330490014bd70119198251ba833794940dd718258009982518259826000a5eb8166002603a6e340062660926ea4c068004cc124dd49980e180d9b8d0010014bd70459045456600266e1d2010002899800919ba548040cc1240052f5c0464660946ea0cde5250375c6096002660946096609800297ae059800980e9b8d0018998249ba9301a0013304937526603860366e340040052f5c116411515980099b874804800a2464646609666e9520123304b304c0014bd701982598261826800a5eb80c004008cc0208c8cc12cdd419bca4a06eb8c130004cc12cc130c1340052f5c0b30013036371a00313304a37526046002660946ea4cc0a4c0a0dc6800800a5eb822c8230966002608400314c103d87980008acc004c0dc006298103d87a80008acc004c104006298103d87b80008acc004c10c006298103d87c80008acc004c034006298103d87d80008b208c41188231046208c8acc004cdc3a40280051300323374a900a19824800a5eb822b30013370e900b00144c00c8cdd2a402c6609200297ae08acc004cdc3a403000513300123374a900c19824800a5eb808c8cc128dd419bca4a06eb8c12c004cc128c12cc1300052f5c0b3001301d371a00313304937526034002660926ea4cc070c06cdc6800800a5eb822c822a2c8221044208841108221044208841108221044208841108220dd718249825003096600266e2000520808080808080808080028800c4cdc0800980124100028208c00c00c323303c375066e3800520003303c37526601660146e340040052f5c06eb8c0f0c0f4004c8cc0ecdd419bca4a06eb8c0f0004cc0ecc0f0c0f40052f5c0b30013017371a00313303a37526024002660746ea4cc054c050dc6800800a5eb822c81b10354590321b8c48008dc0240028b204e375c6058605a002b30013007371a00313302a37526004002660546ea4cc014c010dc6800800a5eb822c8130dd7180c18139baa3301023235980099b8f375c6058002910104b9011a82008919191919191919911981a18111981a181a8039981a1ba90013303430350024bd701981a181a981b00125eb80d660026024003125980099b89002371a00313303337526601e004002660666ea66002005337026e3400400a002b8c25eb822c817a2c8170dd71819981a0011bae3033004375a606400264660626ea0cde5250375c6064002660626064606600297ae059800980e1b8d0018998181ba930090013303037526601e601c6e340040052f5c11640b06eb8c0c0c0c4004d6600294624b30013371290201b8d0018998179ba93300b48100004cc0bcdd4cc005204099b80371a002901fc0057184bd7045902b45902a1bae302f3030001300100259800a51892cc004cdc4a4100026e3400626605a6ea4cc0252080010013302d37533001482000666e00dc6800a40ff0015c612f5c11640a51640a116409c6eb8c0b0c0b400566002600e6e340062660546ea4c008004cc0a8dd49980298021b8d0010014bd704590261bae302a00a300348010c00d2008198010011811000a03e899180118108019bae301f00240746eb0c070c064dd5000c5901719199119801001000912cc004006298103d87a80008992cc004cdd7802180d800c4c030cc078c0700052f5c113300300330200024068603c00280e0dd5980e180e980e980e980e980e980e980e980e980e980c9baa00e3374a90021980d19ba548008cc068c06cc070c070c070c060dd5180d980c1baa0024bd7025eb822c80b0c8cc004004dd61804980b9baa00c2259800800c530103d87a80008992cc006600266ebc00530103d87a8000a50a51405d1001899801801980e801202e325980099912cc00400a294226530013259800980b980e1baa00189bad301d302037566040603a6ea8006290002036301f0019baf301f30200019bab003488966002003159800980100344cc01400d20008a5040751325980099baf301f0014c010140008acc004cc018010dd6981018119bab3020001898019ba630240028a5040791598009980300224001130030078a50407880f0c0880050200ca60020033756601c60386ea8c038c070dd5002488cc080008cc080dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0780066eacc07c00660460069112cc004cdc8a441000038acc004cdc7a441000038998029809198121ba60024bd70000c4cc015300103d87a8000006408119800803c006446600e0046604c66ec0dd48029ba6004001401c8100604200480fa2942294229410201ba63301b337606ea4058dd31980d99bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c566002601060306ea8c070c064dd5180e180c9baa300b30193754003132598009809980c9baa0018992cc004c054c068dd5000c4c8c8c8c8cc8966002604a00713259800980e18109baa0018991919194c004dd69814800cdd718148024dd69814801cdd7181480124444b3001302e00589980a9bab302d00e225980080144cc05c03089660020051301e330300144bd7044c8c8cc070c0bc0084c00cc0d0010dd718178009818801205e899191980d18170010980198190021bae302c001302f00240b51640ac30290013028001302700130223754003164080604800d1640886eb8c088004dd59811001181100098108009810000980d9baa0018b2032301d301a3754003164060601460326ea8c02cc064dd5000c590174530103d87a8000405c603600280c888c96600266e3d22100375c6038603a0031301c0018b202e300200122598009808180a9baa00289919192cc004c07400a26600c603800626600c00200916406860360026036002602c6ea800a2c80a088c8cc00400400c88cc00c004c0080088966002601e60286ea800a2646464b3001301c002899192cc004c0500062b3001301a37540050068b20368acc004c02400626464b3001302000280445901d1bad301e001301a37540051598009809800c56600260346ea800a00d16406d16406080c1018180c1baa001301b0038b20323259800980c000c566002600c602e0031689803980b800a02c8b2032375460340026034002602a6ea800a2c8098dc4a40088acc004c02000e26464b30013009300e37546024602600514a31640346eb4c044004c034dd500245900b2016180618068009806002229344d95900201", + "hash": "0651d8ff35a0146c10f565d2e798ac698d5f4ac0464002b33c8b130a" + }, + { + "title": "pyth_test.pyth_test.publish", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "pyth_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "59157f010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae002488888888966003300130043754015370e90034dc3a4001370e9002244446465300130093754003300d00698068012444b300130060038cc004c040c034dd500248c044c0480064602260246024003374a90004dc3a400491111192cc004c05c006264b3001300e30133754003132598009807980a1baa0018acc004c03cc050dd5180c180a9baa0018a518b20268b2026300630143754602e60286ea80062c8090c8cc004004dd61802980a1baa30170022259800800c530103d87a80008992cc004c014dd6980c980b1baa001898031980c000a5eb82266006006603400480a0c0600050164590141919199119912cc004c048c05cdd500144c966002602660306ea80062646600a00244b300100289919800800802112cc004006297ae08998104c004dc4a4001371890024dc024007371290044dc624001371890044dc02400e91111111991198081191acc004cdc79bae302c0014890475d3c793008cc004888c8cc00cdd698180009bae3030303100130030019b89480426e3120109b804803e600e90082444446600a464660646ea0cde5250375c6066002660646066606800297ae05980098029b8d0018998189ba930020013303137526600860066e340040052f5c11640b446644660104660686ea0cdc7000a4000660686ea4cc00cc008dc6800800a5eb808cc0248cc0d4dd419b8e00148000cc0d4dd49980218019b8d0010014bd701119191981c18131981c1ba800833038375000a66070607200297ae0330383039303a0014bd7018008012cc004c05800a2653001001a5eb82006800888966002606200312323303b374e002660766ea40092f5c065300100180252f5c080088896600200510018cc00400e607e0053303d303e002001400c81e2246530010059981d981e0008024c02400e6eb8c0f0c0f400500519194c004dd6981e800cdc3a4011223303e0023303e375200297ae04888c8cc008ca600200337586084005302f330413042007330414c0103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a80004bd70200222259800801440063300100398228014c8c966002607a00313230343304630470013304630343304630473044375400697ae030483048001304337540071598009819000c4c8c8c0d4cc11cc120008cc11cc120004cc11cc0d4cc11cc120c114dd500225eb80c124c124004c120004c10cdd5001c5660026078003132323230363304830490033304830490023304830490013304830363304830493046375400a97ae0304a304a0013049001304800130433754007159800981f000c4c8c8c8c8c0dccc124c128010cc124c12800ccc124c128008cc124c128004cc124c0dccc124c128c11cdd500325eb80c12cc12c004c128004c124004c120004c10cdd5001c56600260100031323232323230383304a304b0053304a304b0043304a304b0033304a304b0023304a304b0013304a30383304a304b3048375400e97ae0304c304c001304b001304a001304900130480013043375400715980099b87480280062646464646464607266096609800c66096609800a66096609800866096609800666096609800466096609800266096607266096609860926ea80212f5c0609a609a0026098002609600260940026092002609000260866ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c0e8cc130c13401ccc130c134018cc130c134014cc130c134010cc130c13400ccc130c134008cc130c134004cc130c0e8cc130c134c128dd5004a5eb80c138c138004c134004c130004c12c004c128004c124004c120004c10cdd5001c56600266e1d200e00189919191919191919181d99826982700419826982700399826982700319826982700299826982700219826982700199826982700119826982700099826981d99826982718259baa00a4bd7018279827800982700098268009826000982580098250009824800982400098219baa0038acc004cdc3a40200031323232323232323232303c3304e304f0093304e304f0083304e304f0073304e304f0063304e304f0053304e304f0043304e304f0033304e304f0023304e304f0013304e303c3304e304f304c375401697ae030503050001304f001304e001304d001304c001304b001304a001304900130480013043375400715980099b8748048006264646464646464646464607a6609e60a00146609e60a00126609e60a00106609e60a000e6609e60a000c6609e60a000a6609e60a00086609e60a00066609e60a00046609e60a00026609e607a6609e60a0609a6ea80312f5c060a260a200260a0002609e002609c002609a0026098002609600260940026092002609000260866ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c0f8cc140c14402ccc140c144028cc140c144024cc140c144020cc140c14401ccc140c144018cc140c144014cc140c144010cc140c14400ccc140c144008cc140c144004cc140c0f8cc140c144c138dd5006a5eb80c148c148004c144004c140004c13c004c138004c134004c130004c12c004c128004c124004c120004c10cdd5001c56600266e1d20160018991919191919191919191919181f99828982900619828982900599828982900519828982900499828982900419828982900399828982900319828982900299828982900219828982900199828982900119828982900099828981f99828982918279baa00e4bd70182998298009829000982880098280009827800982700098268009826000982580098250009824800982400098219baa0038991919191919191919191919181f998289829006198289829005998289829005198289829004998289829004198289829003998289829003198289829002998289829002198289829001998289829001198289829000998289829182980099828981f99828982918279baa00e4bd7025eb80c148004c144004c140004c13c004c138004c134004c130004c12c004c128004c124004c120004c10cdd5001a0824104820904120824104820904120824104820904118209baa0013044002400c8210dd718209821000991acc004c0800122653001001a5eb8200a8008889660026076003123233045374e0026608a6ea40092f5c065300100180252f5c080088896600200510018cc00400e6092005330473048002001400c8232246530010059982298230008024c04c00e6eb8c118c11c0050051802800a07e8b2078375c6082608400a530012232598009800a40211598009800a4001148002266e3922010801020408102040800000240fd1598009800a408113370490400219801801980a80144cdc124101010100406600600666e00009203f40fc81f8dc4000c888c8cc110c00cdd698228009982218229823000a5eb80c00c006660806ea0cdc7000a4000660806ea4cc03cc038dc6800800a5eb8122232329800919802919198241ba833794940dd718248009982418249825000a5eb816600260366e3400626608e6ea4c060004cc11cdd49980d180c9b8d0010014bd704590431191801acc004c100006298103d87a80008981b198241ba80014bd70208830030019bad3046003911191acc006600260826eb4c12800694294504544cc02000c8c014c0e0cc128dd4000a5eb8226016600898103d87a800041146eb8c128c12c004cc120dd419b8e00148000cc120dd49980b980b1b8d0010014bd7024446b30013040002898019181b99824800a5eb822b3001303500289801919ba548008cc1240052f5c1159800981f80144c00c8cdd2a40086609200297ae08acc004c10400a26600e464660946ea0cde5250375c6096002660946096609800297ae059800981a9b8d0018998249ba9302200133049375266050604e6e340040052f5c1164114466e95200633049375000297ae08acc004c02c00a26600e464660946ea0cde5250375c6096002660946096609800297ae059800981a9b8d0018998249ba9302200133049375266050604e6e340040052f5c1164114466e952008330493750b3001337100029040400244006266e04004c0192020411497ae08acc004cdc3a40140051300323374a900519824800a5eb822b30013370e900600144cc0048cdd2a40186609200297ae02323304a3750600c6eb4c12c004cc128c12cc1300052f5c064660946ea0cde5250375c6096002660946096609800297ae059800980e9b8d0018998249ba9301a0013304937526603860366e340040052f5c116411515980099b874803800a266002466e95200e330490014bd70119198251ba833794940dd718258009982518259826000a5eb8166002603a6e340062660926ea4c068004cc124dd49980e180d9b8d0010014bd70459045456600266e1d2010002899800919ba548040cc1240052f5c0464660946ea0cde5250375c6096002660946096609800297ae059800980e9b8d0018998249ba9301a0013304937526603860366e340040052f5c116411515980099b874804800a2464646609666e9520123304b304c0014bd701982598261826800a5eb80c004008cc0208c8cc12cdd419bca4a06eb8c130004cc12cc130c1340052f5c0b30013036371a00313304a37526046002660946ea4cc0a4c0a0dc6800800a5eb822c8230966002608400314c103d87980008acc004c0dc006298103d87a80008acc004c104006298103d87b80008acc004c10c006298103d87c80008acc004c034006298103d87d80008b208c41188231046208c8acc004cdc3a40280051300323374a900a19824800a5eb822b30013370e900b00144c00c8cdd2a402c6609200297ae08acc004cdc3a403000513300123374a900c19824800a5eb808c8cc128dd419bca4a06eb8c12c004cc128c12cc1300052f5c0b3001301d371a00313304937526034002660926ea4cc070c06cdc6800800a5eb822c822a2c8221044208841108221044208841108221044208841108220dd718249825003096600266e2000520808080808080808080028800c4cdc0800980124100028208c00c00c323303c375066e3800520003303c37526601660146e340040052f5c06eb8c0f0c0f4004c8cc0ecdd419bca4a06eb8c0f0004cc0ecc0f0c0f40052f5c0b30013017371a00313303a37526024002660746ea4cc054c050dc6800800a5eb822c81b10354590321b8c48008dc0240028b204e375c6058605a002b30013007371a00313302a37526004002660546ea4cc014c010dc6800800a5eb822c8130dd7180c18139baa3301023235980099b8f375c6058002910104b9011a82008919191919191919911981a18111981a181a8039981a1ba90013303430350024bd701981a181a981b00125eb80d660026024003125980099b89002371a00313303337526601e004002660666ea66002005337026e3400400a002b8c25eb822c817a2c8170dd71819981a0011bae3033004375a606400264660626ea0cde5250375c6064002660626064606600297ae059800980e1b8d0018998181ba930090013303037526601e601c6e340040052f5c11640b06eb8c0c0c0c4004d6600294624b30013371290201b8d0018998179ba93300b48100004cc0bcdd4cc005204099b80371a002901fc0057184bd7045902b45902a1bae302f3030001300100259800a51892cc004cdc4a4100026e3400626605a6ea4cc0252080010013302d37533001482000666e00dc6800a40ff0015c612f5c11640a51640a116409c6eb8c0b0c0b400566002600e6e340062660546ea4c008004cc0a8dd49980298021b8d0010014bd704590261bae302a00a300348010c00d2008198010011811000a03e899180118108019bae301f00240746eb0c070c064dd5000c5901719199119801001000912cc004006298103d87a80008992cc004cdd7802180d800c4c030cc078c0700052f5c113300300330200024068603c00280e0dd5980e180e980e980e980e980e980e980e980e980e980c9baa00e3374a90021980d19ba548008cc068c06cc070c070c070c060dd5180d980c1baa0024bd7025eb822c80b0c8cc004004dd61804980b9baa00c2259800800c530103d87a80008992cc006600266ebc00530103d87a8000a50a51405d1001899801801980e801202e325980099912cc00400a294226530013259800980b980e1baa00189bad301d302037566040603a6ea8006290002036301f0019baf301f30200019bab003488966002003159800980100344cc01400d20008a5040751325980099baf301f0014c010140008acc004cc018010dd6981018119bab3020001898019ba630240028a5040791598009980300224001130030078a50407880f0c0880050200ca60020033756601c60386ea8c038c070dd5002488cc080008cc080dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0780066eacc07c00660460069112cc004cdc8a441000038acc004cdc7a441000038998029809198121ba60024bd70000c4cc015300103d87a8000006408119800803c006446600e0046604c66ec0dd48029ba6004001401c8100604200480fa2942294229410201ba63301b337606ea4058dd31980d99bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c566002601060306ea8c070c064dd5180e180c9baa300b30193754003132598009809980c9baa0018992cc004c054c068dd5000c4c8c8c8c8cc8966002604a00713259800980e18109baa0018991919194c004dd69814800cdd718148024dd69814801cdd7181480124444b3001302e00589980a9bab302d00e225980080144cc05c03089660020051301e330300144bd7044c8c8cc070c0bc0084c00cc0d0010dd718178009818801205e899191980d18170010980198190021bae302c001302f00240b51640ac30290013028001302700130223754003164080604800d1640886eb8c088004dd59811001181100098108009810000980d9baa0018b2032301d301a3754003164060601460326ea8c02cc064dd5000c590174530103d87a8000405c603600280c888c96600266e3d22100375c6038603a0031301c0018b202e300200122598009808180a9baa00289919192cc004c07400a26600c603800626600c00200916406860360026036002602c6ea800a2c80a088c8cc00400400c88cc00c004c0080088966002601e60286ea800a2646464b3001301c002899192cc004c0500062b3001301a37540050068b20368acc004c02400626464b3001302000280445901d1bad301e001301a37540051598009809800c56600260346ea800a00d16406d16406080c1018180c1baa001301b0038b20323259800980c000c566002600c602e0031689803980b800a02c8b2032375460340026034002602a6ea800a2c8098dc4a40088acc004c02000e26464b30013009300e37546024602600514a31640346eb4c044004c034dd500245900b2016180618068009806002229344d95900201", + "hash": "0651d8ff35a0146c10f565d2e798ac698d5f4ac0464002b33c8b130a" + }, + { + "title": "pyth_test.pyth_test.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "pyth_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "59157f010100229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae002488888888966003300130043754015370e90034dc3a4001370e9002244446465300130093754003300d00698068012444b300130060038cc004c040c034dd500248c044c0480064602260246024003374a90004dc3a400491111192cc004c05c006264b3001300e30133754003132598009807980a1baa0018acc004c03cc050dd5180c180a9baa0018a518b20268b2026300630143754602e60286ea80062c8090c8cc004004dd61802980a1baa30170022259800800c530103d87a80008992cc004c014dd6980c980b1baa001898031980c000a5eb82266006006603400480a0c0600050164590141919199119912cc004c048c05cdd500144c966002602660306ea80062646600a00244b300100289919800800802112cc004006297ae08998104c004dc4a4001371890024dc024007371290044dc624001371890044dc02400e91111111991198081191acc004cdc79bae302c0014890475d3c793008cc004888c8cc00cdd698180009bae3030303100130030019b89480426e3120109b804803e600e90082444446600a464660646ea0cde5250375c6066002660646066606800297ae05980098029b8d0018998189ba930020013303137526600860066e340040052f5c11640b446644660104660686ea0cdc7000a4000660686ea4cc00cc008dc6800800a5eb808cc0248cc0d4dd419b8e00148000cc0d4dd49980218019b8d0010014bd701119191981c18131981c1ba800833038375000a66070607200297ae0330383039303a0014bd7018008012cc004c05800a2653001001a5eb82006800888966002606200312323303b374e002660766ea40092f5c065300100180252f5c080088896600200510018cc00400e607e0053303d303e002001400c81e2246530010059981d981e0008024c02400e6eb8c0f0c0f400500519194c004dd6981e800cdc3a4011223303e0023303e375200297ae04888c8cc008ca600200337586084005302f330413042007330414c0103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a8000330414c103d87a80004bd70200222259800801440063300100398228014c8c966002607a00313230343304630470013304630343304630473044375400697ae030483048001304337540071598009819000c4c8c8c0d4cc11cc120008cc11cc120004cc11cc0d4cc11cc120c114dd500225eb80c124c124004c120004c10cdd5001c5660026078003132323230363304830490033304830490023304830490013304830363304830493046375400a97ae0304a304a0013049001304800130433754007159800981f000c4c8c8c8c8c0dccc124c128010cc124c12800ccc124c128008cc124c128004cc124c0dccc124c128c11cdd500325eb80c12cc12c004c128004c124004c120004c10cdd5001c56600260100031323232323230383304a304b0053304a304b0043304a304b0033304a304b0023304a304b0013304a30383304a304b3048375400e97ae0304c304c001304b001304a001304900130480013043375400715980099b87480280062646464646464607266096609800c66096609800a66096609800866096609800666096609800466096609800266096607266096609860926ea80212f5c0609a609a0026098002609600260940026092002609000260866ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c0e8cc130c13401ccc130c134018cc130c134014cc130c134010cc130c13400ccc130c134008cc130c134004cc130c0e8cc130c134c128dd5004a5eb80c138c138004c134004c130004c12c004c128004c124004c120004c10cdd5001c56600266e1d200e00189919191919191919181d99826982700419826982700399826982700319826982700299826982700219826982700199826982700119826982700099826981d99826982718259baa00a4bd7018279827800982700098268009826000982580098250009824800982400098219baa0038acc004cdc3a40200031323232323232323232303c3304e304f0093304e304f0083304e304f0073304e304f0063304e304f0053304e304f0043304e304f0033304e304f0023304e304f0013304e303c3304e304f304c375401697ae030503050001304f001304e001304d001304c001304b001304a001304900130480013043375400715980099b8748048006264646464646464646464607a6609e60a00146609e60a00126609e60a00106609e60a000e6609e60a000c6609e60a000a6609e60a00086609e60a00066609e60a00046609e60a00026609e607a6609e60a0609a6ea80312f5c060a260a200260a0002609e002609c002609a0026098002609600260940026092002609000260866ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c0f8cc140c14402ccc140c144028cc140c144024cc140c144020cc140c14401ccc140c144018cc140c144014cc140c144010cc140c14400ccc140c144008cc140c144004cc140c0f8cc140c144c138dd5006a5eb80c148c148004c144004c140004c13c004c138004c134004c130004c12c004c128004c124004c120004c10cdd5001c56600266e1d20160018991919191919191919191919181f99828982900619828982900599828982900519828982900499828982900419828982900399828982900319828982900299828982900219828982900199828982900119828982900099828981f99828982918279baa00e4bd70182998298009829000982880098280009827800982700098268009826000982580098250009824800982400098219baa0038991919191919191919191919181f998289829006198289829005998289829005198289829004998289829004198289829003998289829003198289829002998289829002198289829001998289829001198289829000998289829182980099828981f99828982918279baa00e4bd7025eb80c148004c144004c140004c13c004c138004c134004c130004c12c004c128004c124004c120004c10cdd5001a0824104820904120824104820904120824104820904118209baa0013044002400c8210dd718209821000991acc004c0800122653001001a5eb8200a8008889660026076003123233045374e0026608a6ea40092f5c065300100180252f5c080088896600200510018cc00400e6092005330473048002001400c8232246530010059982298230008024c04c00e6eb8c118c11c0050051802800a07e8b2078375c6082608400a530012232598009800a40211598009800a4001148002266e3922010801020408102040800000240fd1598009800a408113370490400219801801980a80144cdc124101010100406600600666e00009203f40fc81f8dc4000c888c8cc110c00cdd698228009982218229823000a5eb80c00c006660806ea0cdc7000a4000660806ea4cc03cc038dc6800800a5eb8122232329800919802919198241ba833794940dd718248009982418249825000a5eb816600260366e3400626608e6ea4c060004cc11cdd49980d180c9b8d0010014bd704590431191801acc004c100006298103d87a80008981b198241ba80014bd70208830030019bad3046003911191acc006600260826eb4c12800694294504544cc02000c8c014c0e0cc128dd4000a5eb8226016600898103d87a800041146eb8c128c12c004cc120dd419b8e00148000cc120dd49980b980b1b8d0010014bd7024446b30013040002898019181b99824800a5eb822b3001303500289801919ba548008cc1240052f5c1159800981f80144c00c8cdd2a40086609200297ae08acc004c10400a26600e464660946ea0cde5250375c6096002660946096609800297ae059800981a9b8d0018998249ba9302200133049375266050604e6e340040052f5c1164114466e95200633049375000297ae08acc004c02c00a26600e464660946ea0cde5250375c6096002660946096609800297ae059800981a9b8d0018998249ba9302200133049375266050604e6e340040052f5c1164114466e952008330493750b3001337100029040400244006266e04004c0192020411497ae08acc004cdc3a40140051300323374a900519824800a5eb822b30013370e900600144cc0048cdd2a40186609200297ae02323304a3750600c6eb4c12c004cc128c12cc1300052f5c064660946ea0cde5250375c6096002660946096609800297ae059800980e9b8d0018998249ba9301a0013304937526603860366e340040052f5c116411515980099b874803800a266002466e95200e330490014bd70119198251ba833794940dd718258009982518259826000a5eb8166002603a6e340062660926ea4c068004cc124dd49980e180d9b8d0010014bd70459045456600266e1d2010002899800919ba548040cc1240052f5c0464660946ea0cde5250375c6096002660946096609800297ae059800980e9b8d0018998249ba9301a0013304937526603860366e340040052f5c116411515980099b874804800a2464646609666e9520123304b304c0014bd701982598261826800a5eb80c004008cc0208c8cc12cdd419bca4a06eb8c130004cc12cc130c1340052f5c0b30013036371a00313304a37526046002660946ea4cc0a4c0a0dc6800800a5eb822c8230966002608400314c103d87980008acc004c0dc006298103d87a80008acc004c104006298103d87b80008acc004c10c006298103d87c80008acc004c034006298103d87d80008b208c41188231046208c8acc004cdc3a40280051300323374a900a19824800a5eb822b30013370e900b00144c00c8cdd2a402c6609200297ae08acc004cdc3a403000513300123374a900c19824800a5eb808c8cc128dd419bca4a06eb8c12c004cc128c12cc1300052f5c0b3001301d371a00313304937526034002660926ea4cc070c06cdc6800800a5eb822c822a2c8221044208841108221044208841108221044208841108220dd718249825003096600266e2000520808080808080808080028800c4cdc0800980124100028208c00c00c323303c375066e3800520003303c37526601660146e340040052f5c06eb8c0f0c0f4004c8cc0ecdd419bca4a06eb8c0f0004cc0ecc0f0c0f40052f5c0b30013017371a00313303a37526024002660746ea4cc054c050dc6800800a5eb822c81b10354590321b8c48008dc0240028b204e375c6058605a002b30013007371a00313302a37526004002660546ea4cc014c010dc6800800a5eb822c8130dd7180c18139baa3301023235980099b8f375c6058002910104b9011a82008919191919191919911981a18111981a181a8039981a1ba90013303430350024bd701981a181a981b00125eb80d660026024003125980099b89002371a00313303337526601e004002660666ea66002005337026e3400400a002b8c25eb822c817a2c8170dd71819981a0011bae3033004375a606400264660626ea0cde5250375c6064002660626064606600297ae059800980e1b8d0018998181ba930090013303037526601e601c6e340040052f5c11640b06eb8c0c0c0c4004d6600294624b30013371290201b8d0018998179ba93300b48100004cc0bcdd4cc005204099b80371a002901fc0057184bd7045902b45902a1bae302f3030001300100259800a51892cc004cdc4a4100026e3400626605a6ea4cc0252080010013302d37533001482000666e00dc6800a40ff0015c612f5c11640a51640a116409c6eb8c0b0c0b400566002600e6e340062660546ea4c008004cc0a8dd49980298021b8d0010014bd704590261bae302a00a300348010c00d2008198010011811000a03e899180118108019bae301f00240746eb0c070c064dd5000c5901719199119801001000912cc004006298103d87a80008992cc004cdd7802180d800c4c030cc078c0700052f5c113300300330200024068603c00280e0dd5980e180e980e980e980e980e980e980e980e980e980c9baa00e3374a90021980d19ba548008cc068c06cc070c070c070c060dd5180d980c1baa0024bd7025eb822c80b0c8cc004004dd61804980b9baa00c2259800800c530103d87a80008992cc006600266ebc00530103d87a8000a50a51405d1001899801801980e801202e325980099912cc00400a294226530013259800980b980e1baa00189bad301d302037566040603a6ea8006290002036301f0019baf301f30200019bab003488966002003159800980100344cc01400d20008a5040751325980099baf301f0014c010140008acc004cc018010dd6981018119bab3020001898019ba630240028a5040791598009980300224001130030078a50407880f0c0880050200ca60020033756601c60386ea8c038c070dd5002488cc080008cc080dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0780066eacc07c00660460069112cc004cdc8a441000038acc004cdc7a441000038998029809198121ba60024bd70000c4cc015300103d87a8000006408119800803c006446600e0046604c66ec0dd48029ba6004001401c8100604200480fa2942294229410201ba63301b337606ea4058dd31980d99bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c566002601060306ea8c070c064dd5180e180c9baa300b30193754003132598009809980c9baa0018992cc004c054c068dd5000c4c8c8c8c8cc8966002604a00713259800980e18109baa0018991919194c004dd69814800cdd718148024dd69814801cdd7181480124444b3001302e00589980a9bab302d00e225980080144cc05c03089660020051301e330300144bd7044c8c8cc070c0bc0084c00cc0d0010dd718178009818801205e899191980d18170010980198190021bae302c001302f00240b51640ac30290013028001302700130223754003164080604800d1640886eb8c088004dd59811001181100098108009810000980d9baa0018b2032301d301a3754003164060601460326ea8c02cc064dd5000c590174530103d87a8000405c603600280c888c96600266e3d22100375c6038603a0031301c0018b202e300200122598009808180a9baa00289919192cc004c07400a26600c603800626600c00200916406860360026036002602c6ea800a2c80a088c8cc00400400c88cc00c004c0080088966002601e60286ea800a2646464b3001301c002899192cc004c0500062b3001301a37540050068b20368acc004c02400626464b3001302000280445901d1bad301e001301a37540051598009809800c56600260346ea800a00d16406d16406080c1018180c1baa001301b0038b20323259800980c000c566002600c602e0031689803980b800a02c8b2032375460340026034002602a6ea800a2c8098dc4a40088acc004c02000e26464b30013009300e37546024602600514a31640346eb4c044004c034dd500245900b2016180618068009806002229344d95900201", + "hash": "0651d8ff35a0146c10f565d2e798ac698d5f4ac0464002b33c8b130a" + } + ], + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "ByteArray": { + "title": "ByteArray", + "dataType": "bytes" + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "Option": { + "title": "Option", + "anyOf": [ + { + "title": "Some", + "description": "An optional value.", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/prediction_market~1types~1BetDirection" + } + ] + }, + { + "title": "None", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "cardano/assets/PolicyId": { + "title": "PolicyId", + "dataType": "bytes" + }, + "cardano/transaction/OutputReference": { + "title": "OutputReference", + "description": "An `OutputReference` is a unique reference to an output on-chain. The `output_index`\n corresponds to the position in the output list of the transaction (identified by its id)\n that produced that output", + "anyOf": [ + { + "title": "OutputReference", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "transaction_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "output_index", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "prediction_market/types/BetDirection": { + "title": "BetDirection", + "anyOf": [ + { + "title": "Yes", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "No", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "prediction_market/types/MarketAction": { + "title": "MarketAction", + "anyOf": [ + { + "title": "Bet", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "direction", + "$ref": "#/definitions/prediction_market~1types~1BetDirection" + }, + { + "title": "amount", + "$ref": "#/definitions/Int" + } + ] + }, + { + "title": "Resolve", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Claim", + "dataType": "constructor", + "index": 2, + "fields": [ + { + "title": "burn_amount", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "prediction_market/types/MarketDatum": { + "title": "MarketDatum", + "anyOf": [ + { + "title": "MarketDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "creator", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "pyth_id", + "$ref": "#/definitions/cardano~1assets~1PolicyId" + }, + { + "title": "feed_id", + "$ref": "#/definitions/Int" + }, + { + "title": "target_price", + "$ref": "#/definitions/Int" + }, + { + "title": "resolution_time", + "$ref": "#/definitions/Int" + }, + { + "title": "token_policy", + "$ref": "#/definitions/cardano~1assets~1PolicyId" + }, + { + "title": "yes_reserve", + "$ref": "#/definitions/Int" + }, + { + "title": "no_reserve", + "$ref": "#/definitions/Int" + }, + { + "title": "k", + "$ref": "#/definitions/Int" + }, + { + "title": "total_yes_minted", + "$ref": "#/definitions/Int" + }, + { + "title": "total_no_minted", + "$ref": "#/definitions/Int" + }, + { + "title": "total_ada", + "$ref": "#/definitions/Int" + }, + { + "title": "resolved", + "$ref": "#/definitions/Bool" + }, + { + "title": "winning_side", + "$ref": "#/definitions/Option" + } + ] + } + ] + }, + "prediction_market/types/MarketParams": { + "title": "MarketParams", + "description": "Parameter for the market validator (makes each market's policy ID unique)", + "anyOf": [ + { + "title": "MarketParams", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "one_shot", + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + ] + } + ] + }, + "prediction_market/types/MintAction": { + "title": "MintAction", + "anyOf": [ + { + "title": "MintTokens", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "BurnTokens", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + } + } +} \ No newline at end of file diff --git a/lazer/cardano/prediction-market/contracts/validators/market.ak b/lazer/cardano/prediction-market/contracts/validators/market.ak new file mode 100644 index 00000000..77f41ea1 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/validators/market.ak @@ -0,0 +1,42 @@ +use cardano/assets.{PolicyId} +use cardano/transaction.{OutputReference, Transaction} +use prediction_market/market_validation.{ + validate_bet, validate_burn_tokens, validate_claim, validate_mint_tokens, + validate_resolve, +} +use prediction_market/types.{ + Bet, BurnTokens, Claim, MarketAction, MarketDatum, MarketParams, + MintAction, MintTokens, Resolve, +} + +// ─── Market validator (dual spend + mint) ──────────────────────────────────── + +validator market(params: MarketParams) { + spend( + datum: Option, + redeemer: MarketAction, + spend_out_ref: OutputReference, + self: Transaction, + ) { + expect Some(market_datum) = datum + + when redeemer is { + Bet { direction, amount } -> + validate_bet(market_datum, direction, amount, spend_out_ref, self) + Resolve -> validate_resolve(market_datum, spend_out_ref, self) + Claim { burn_amount } -> + validate_claim(market_datum, burn_amount, spend_out_ref, self) + } + } + + mint(redeemer: MintAction, policy_id: PolicyId, self: Transaction) { + when redeemer is { + MintTokens -> validate_mint_tokens(params.one_shot, policy_id, self) + BurnTokens -> validate_burn_tokens(policy_id, self) + } + } + + else(_) { + fail + } +} diff --git a/lazer/cardano/prediction-market/contracts/validators/pyth_test.ak b/lazer/cardano/prediction-market/contracts/validators/pyth_test.ak new file mode 100644 index 00000000..b599acf0 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/validators/pyth_test.ak @@ -0,0 +1,37 @@ +use aiken/collection/list +use cardano/address.{Credential} +use cardano/assets.{PolicyId} +use cardano/certificate.{Certificate, RegisterCredential} +use cardano/transaction.{Transaction} +use pyth +use types/u32 + +/// Minimal test validator to verify Pyth Lazer integration works. +/// Parameterized with the Pyth deployment policy ID. +/// Calls pyth.get_updates() and asserts BTC/USD price exists. +validator pyth_test(pyth_id: PolicyId) { + withdraw(_redeemer: Data, _account: Credential, self: Transaction) { + // Get verified price updates from Pyth + expect [update] = pyth.get_updates(pyth_id, self) + + // Find BTC/USD feed (id = 1) + expect Some(btc_feed) = + list.find(update.feeds, fn(f) { u32.as_int(f.feed_id) == 1 }) + + // Assert price exists + expect Some(Some(_price)) = btc_feed.price + + True + } + + publish(_redeemer: Data, certificate: Certificate, _self: Transaction) { + when certificate is { + RegisterCredential { .. } -> True + _ -> fail + } + } + + else(_) { + fail + } +} diff --git a/lazer/cardano/prediction-market/contracts/validators/tests/bet_tests.ak b/lazer/cardano/prediction-market/contracts/validators/tests/bet_tests.ak new file mode 100644 index 00000000..94be0388 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/validators/tests/bet_tests.ak @@ -0,0 +1,227 @@ +use aiken/interval +use cardano/assets +use cardano/transaction.{InlineDatum, Output, OutputReference, Transaction} +use prediction_market/market_validation.{validate_bet} +use prediction_market/mocks.{ + mock_market_datum, mock_market_hash, mock_market_input, mock_market_value, + mock_script_address, mock_transaction, mock_tx_id, +} +use prediction_market/types.{MarketDatum, No, Yes} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +/// Build a valid bet transaction for YES direction +fn bet_yes_tx(datum: MarketDatum, amount: Int) -> Transaction { + // Calculate expected tokens: yes_reserve - k / (no_reserve + amount) + let tokens_out = datum.yes_reserve - datum.k / ( datum.no_reserve + amount ) + + let new_datum = + MarketDatum { + ..datum, + yes_reserve: datum.yes_reserve - tokens_out, + no_reserve: datum.no_reserve + amount, + total_yes_minted: datum.total_yes_minted + tokens_out, + total_ada: datum.total_ada + amount, + } + + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [ + Output { + address: mock_script_address(), + value: mock_market_value(datum.total_ada + amount), + datum: InlineDatum(new_datum), + reference_script: None, + }, + ], + mint: assets.zero |> assets.add(mock_market_hash, "YES", tokens_out), + validity_range: interval.between(0, datum.resolution_time), + } +} + +/// Build a valid bet transaction for NO direction +fn bet_no_tx(datum: MarketDatum, amount: Int) -> Transaction { + let tokens_out = datum.no_reserve - datum.k / ( datum.yes_reserve + amount ) + + let new_datum = + MarketDatum { + ..datum, + yes_reserve: datum.yes_reserve + amount, + no_reserve: datum.no_reserve - tokens_out, + total_no_minted: datum.total_no_minted + tokens_out, + total_ada: datum.total_ada + amount, + } + + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [ + Output { + address: mock_script_address(), + value: mock_market_value(datum.total_ada + amount), + datum: InlineDatum(new_datum), + reference_script: None, + }, + ], + mint: assets.zero |> assets.add(mock_market_hash, "NO", tokens_out), + validity_range: interval.between(0, datum.resolution_time), + } +} + +fn mock_spend_ref() -> OutputReference { + OutputReference { transaction_id: mock_tx_id, output_index: 0 } +} + +// ── YES bet tests ──────────────────────────────────────────────────────────── + +test bet_yes_50_ada() { + let datum = mock_market_datum() + let tx = bet_yes_tx(datum, 50_000_000) + validate_bet(datum, Yes, 50_000_000, mock_spend_ref(), tx) +} + +test bet_yes_small_amount() { + let datum = mock_market_datum() + let tx = bet_yes_tx(datum, 1_000_000) + validate_bet(datum, Yes, 1_000_000, mock_spend_ref(), tx) +} + +// ── NO bet tests ───────────────────────────────────────────────────────────── + +test bet_no_50_ada() { + let datum = mock_market_datum() + let tx = bet_no_tx(datum, 50_000_000) + validate_bet(datum, No, 50_000_000, mock_spend_ref(), tx) +} + +test bet_no_small_amount() { + let datum = mock_market_datum() + let tx = bet_no_tx(datum, 1_000_000) + validate_bet(datum, No, 1_000_000, mock_spend_ref(), tx) +} + +// ── Failure cases ──────────────────────────────────────────────────────────── + +test bet_zero_amount_fails() fail { + let datum = mock_market_datum() + let tx = bet_yes_tx(datum, 0) + validate_bet(datum, Yes, 0, mock_spend_ref(), tx) +} + +test bet_after_resolution_time_fails() fail { + let datum = mock_market_datum() + let tx = + Transaction { + ..bet_yes_tx(datum, 50_000_000), + validity_range: // validity range past resolution time + interval.between(300_001, 400_000), + } + validate_bet(datum, Yes, 50_000_000, mock_spend_ref(), tx) +} + +test bet_on_resolved_market_fails() fail { + let datum = + MarketDatum { + ..mock_market_datum(), + resolved: True, + winning_side: Some(Yes), + } + let tx = bet_yes_tx(datum, 50_000_000) + validate_bet(datum, Yes, 50_000_000, mock_spend_ref(), tx) +} + +test bet_wrong_mint_amount_fails() fail { + let datum = mock_market_datum() + // Build valid tx but tamper with mint amount + let tx = + Transaction { + ..bet_yes_tx(datum, 50_000_000), + mint: assets.zero |> assets.add(mock_market_hash, "YES", 999_999), + } + validate_bet(datum, Yes, 50_000_000, mock_spend_ref(), tx) +} + +test bet_wrong_datum_update_fails() fail { + let datum = mock_market_datum() + // Build tx with wrong output datum (reserves not updated) + let tokens_out = + datum.yes_reserve - datum.k / ( datum.no_reserve + 50_000_000 ) + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [ + Output { + address: mock_script_address(), + value: mock_market_value(datum.total_ada + 50_000_000), + datum: InlineDatum(datum), + reference_script: None, + }, + ], + mint: assets.zero |> assets.add(mock_market_hash, "YES", tokens_out), + validity_range: interval.between(0, datum.resolution_time), + } + validate_bet(datum, Yes, 50_000_000, mock_spend_ref(), tx) +} + +test bet_insufficient_ada_fails() fail { + let datum = mock_market_datum() + let tokens_out = + datum.yes_reserve - datum.k / ( datum.no_reserve + 50_000_000 ) + let new_datum = + MarketDatum { + ..datum, + yes_reserve: datum.yes_reserve - tokens_out, + no_reserve: datum.no_reserve + 50_000_000, + total_yes_minted: datum.total_yes_minted + tokens_out, + total_ada: datum.total_ada + 50_000_000, + } + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [ + Output { + address: mock_script_address(), + // Not enough ADA added + value: mock_market_value(datum.total_ada + 1), + datum: InlineDatum(new_datum), + reference_script: None, + }, + ], + mint: assets.zero |> assets.add(mock_market_hash, "YES", tokens_out), + validity_range: interval.between(0, datum.resolution_time), + } + validate_bet(datum, Yes, 50_000_000, mock_spend_ref(), tx) +} + +test bet_missing_state_thread_in_output_fails() fail { + let datum = mock_market_datum() + let tokens_out = + datum.yes_reserve - datum.k / ( datum.no_reserve + 50_000_000 ) + let new_datum = + MarketDatum { + ..datum, + yes_reserve: datum.yes_reserve - tokens_out, + no_reserve: datum.no_reserve + 50_000_000, + total_yes_minted: datum.total_yes_minted + tokens_out, + total_ada: datum.total_ada + 50_000_000, + } + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [ + Output { + address: mock_script_address(), + value: assets.from_lovelace(datum.total_ada + 50_000_000), + datum: InlineDatum(new_datum), + reference_script: None, + }, + ], + mint: assets.zero |> assets.add(mock_market_hash, "YES", tokens_out), + validity_range: interval.between(0, datum.resolution_time), + } + validate_bet(datum, Yes, 50_000_000, mock_spend_ref(), tx) +} diff --git a/lazer/cardano/prediction-market/contracts/validators/tests/claim_tests.ak b/lazer/cardano/prediction-market/contracts/validators/tests/claim_tests.ak new file mode 100644 index 00000000..9c2aae09 --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/validators/tests/claim_tests.ak @@ -0,0 +1,208 @@ +use aiken/interval +use cardano/assets +use cardano/transaction.{InlineDatum, Output, OutputReference, Transaction} +use prediction_market/market_validation.{validate_claim} +use prediction_market/mocks.{ + mock_market_datum, mock_market_hash, mock_market_input, mock_market_value, + mock_script_address, mock_transaction, mock_tx_id, +} +use prediction_market/types.{MarketDatum, No, Yes} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +fn mock_spend_ref() -> OutputReference { + OutputReference { transaction_id: mock_tx_id, output_index: 0 } +} + +fn resolved_yes_datum() -> MarketDatum { + MarketDatum { ..mock_market_datum(), resolved: True, winning_side: Some(Yes) } +} + +fn resolved_no_datum() -> MarketDatum { + MarketDatum { ..mock_market_datum(), resolved: True, winning_side: Some(No) } +} + +/// Build a valid claim transaction +fn claim_tx( + datum: MarketDatum, + burn_amount: Int, + winning_token: ByteArray, +) -> Transaction { + let total_winning = + when datum.winning_side is { + Some(Yes) -> datum.total_yes_minted + Some(No) -> datum.total_no_minted + None -> 0 + } + + let payout = burn_amount * datum.total_ada / total_winning + + let new_datum = + when datum.winning_side is { + Some(Yes) -> + MarketDatum { + ..datum, + total_ada: datum.total_ada - payout, + total_yes_minted: datum.total_yes_minted - burn_amount, + } + Some(No) -> + MarketDatum { + ..datum, + total_ada: datum.total_ada - payout, + total_no_minted: datum.total_no_minted - burn_amount, + } + None -> datum + } + + let is_last_claim = total_winning == burn_amount + + let outputs = + if is_last_claim { + [] + } else { + [ + Output { + address: mock_script_address(), + value: mock_market_value(datum.total_ada - payout), + datum: InlineDatum(new_datum), + reference_script: None, + }, + ] + } + + let minted = + if is_last_claim { + assets.zero + |> assets.add(mock_market_hash, winning_token, -burn_amount) + |> assets.add(mock_market_hash, "", -1) + } else { + assets.zero |> assets.add(mock_market_hash, winning_token, -burn_amount) + } + + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: outputs, + mint: minted, + validity_range: interval.between(300_000, 400_000), + } +} + +// ── Success cases ──────────────────────────────────────────────────────────── + +test claim_yes_partial() { + let datum = resolved_yes_datum() + // Burn 50 of 100 YES tokens → payout = 50 * 100M / 100M = 50M + let tx = claim_tx(datum, 50_000_000, "YES") + validate_claim(datum, 50_000_000, mock_spend_ref(), tx) +} + +test claim_no_partial() { + let datum = resolved_no_datum() + let tx = claim_tx(datum, 50_000_000, "NO") + validate_claim(datum, 50_000_000, mock_spend_ref(), tx) +} + +test claim_yes_full() { + let datum = resolved_yes_datum() + // Burn all YES tokens — last claimer, no continuing output + let tx = claim_tx(datum, datum.total_yes_minted, "YES") + validate_claim(datum, datum.total_yes_minted, mock_spend_ref(), tx) +} + +test claim_no_full() { + let datum = resolved_no_datum() + let tx = claim_tx(datum, datum.total_no_minted, "NO") + validate_claim(datum, datum.total_no_minted, mock_spend_ref(), tx) +} + +// ── Failure cases ──────────────────────────────────────────────────────────── + +test claim_unresolved_fails() fail { + let datum = mock_market_datum() + let tx = claim_tx(datum, 50_000_000, "YES") + validate_claim(datum, 50_000_000, mock_spend_ref(), tx) +} + +test claim_zero_fails() fail { + let datum = resolved_yes_datum() + let tx = claim_tx(datum, 0, "YES") + validate_claim(datum, 0, mock_spend_ref(), tx) +} + +test claim_wrong_burn_amount_fails() fail { + let datum = resolved_yes_datum() + // Tx burns 50M but we claim 30M → mismatch + let tx = claim_tx(datum, 50_000_000, "YES") + validate_claim(datum, 30_000_000, mock_spend_ref(), tx) +} + +test claim_wrong_token_burned_fails() fail { + let datum = resolved_yes_datum() + // YES wins but we burn NO tokens + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [ + Output { + address: mock_script_address(), + value: mock_market_value(datum.total_ada - 50_000_000), + datum: InlineDatum( + MarketDatum { + ..datum, + total_ada: datum.total_ada - 50_000_000, + total_yes_minted: datum.total_yes_minted - 50_000_000, + }, + ), + reference_script: None, + }, + ], + mint: // Burning NO tokens instead of YES + assets.zero + |> assets.add(mock_market_hash, "NO", -50_000_000), + validity_range: interval.between(300_000, 400_000), + } + validate_claim(datum, 50_000_000, mock_spend_ref(), tx) +} + +test claim_full_without_state_thread_burn_fails() fail { + let datum = resolved_yes_datum() + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [], + mint: assets.zero + |> assets.add(mock_market_hash, "YES", -datum.total_yes_minted), + validity_range: interval.between(300_000, 400_000), + } + validate_claim(datum, datum.total_yes_minted, mock_spend_ref(), tx) +} + +test claim_partial_missing_state_thread_in_output_fails() fail { + let datum = resolved_yes_datum() + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(datum.total_ada)], + outputs: [ + Output { + address: mock_script_address(), + value: assets.from_lovelace(datum.total_ada - 50_000_000), + datum: InlineDatum( + MarketDatum { + ..datum, + total_ada: datum.total_ada - 50_000_000, + total_yes_minted: datum.total_yes_minted - 50_000_000, + }, + ), + reference_script: None, + }, + ], + mint: assets.zero + |> assets.add(mock_market_hash, "YES", -50_000_000), + validity_range: interval.between(300_000, 400_000), + } + validate_claim(datum, 50_000_000, mock_spend_ref(), tx) +} diff --git a/lazer/cardano/prediction-market/contracts/validators/tests/mint_tests.ak b/lazer/cardano/prediction-market/contracts/validators/tests/mint_tests.ak new file mode 100644 index 00000000..aab9da4e --- /dev/null +++ b/lazer/cardano/prediction-market/contracts/validators/tests/mint_tests.ak @@ -0,0 +1,150 @@ +use cardano/address +use cardano/assets +use cardano/transaction.{Input, NoDatum, Output, OutputReference, Transaction} +use prediction_market/market_validation.{ + validate_burn_tokens, validate_mint_tokens, +} +use prediction_market/mocks.{ + mock_market_hash, mock_market_input, mock_transaction, mock_tx_id, +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +fn mock_one_shot() -> OutputReference { + OutputReference { transaction_id: mock_tx_id, output_index: 42 } +} + +fn mock_one_shot_input() -> Input { + Input { + output_reference: mock_one_shot(), + output: Output { + address: address.Address { + payment_credential: address.VerificationKey( + #"01010101010101010101010101010101010101010101010101010101", + ), + stake_credential: None, + }, + value: assets.from_lovelace(5_000_000), + datum: NoDatum, + reference_script: None, + }, + } +} + +fn fake_market_shaped_input() -> Input { + Input { + output_reference: OutputReference { + transaction_id: mock_tx_id, + output_index: 7, + }, + output: Output { + address: address.Address { + payment_credential: address.Script(mock_market_hash), + stake_credential: None, + }, + value: assets.from_lovelace(100_000_000), + datum: NoDatum, + reference_script: None, + }, + } +} + +// ── MintTokens tests ───────────────────────────────────────────────────────── + +test mint_with_one_shot() { + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_one_shot_input()], + mint: assets.zero + |> assets.add(mock_market_hash, "", 1) + |> assets.add(mock_market_hash, "YES", 100_000_000) + |> assets.add(mock_market_hash, "NO", 100_000_000), + } + validate_mint_tokens(mock_one_shot(), mock_market_hash, tx) +} + +test mint_with_market_spend() { + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_market_input(100_000_000)], + mint: assets.zero |> assets.add(mock_market_hash, "YES", 50_000_000), + } + validate_mint_tokens(mock_one_shot(), mock_market_hash, tx) +} + +test mint_without_one_shot_or_market_input_fails() fail { + // No one_shot UTxO consumed and no market script input + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_one_shot_input()], + mint: assets.zero |> assets.add(mock_market_hash, "YES", 100_000_000), + } + // Use a different one_shot that isn't in inputs + let wrong_one_shot = + OutputReference { + transaction_id: #"1111111111111111111111111111111111111111111111111111111111111111", + output_index: 0, + } + validate_mint_tokens(wrong_one_shot, mock_market_hash, tx) +} + +test mint_with_fake_market_shaped_input_fails() fail { + let tx = + Transaction { + ..mock_transaction(), + inputs: [fake_market_shaped_input()], + mint: assets.zero |> assets.add(mock_market_hash, "YES", 50_000_000), + } + validate_mint_tokens(mock_one_shot(), mock_market_hash, tx) +} + +test mint_with_one_shot_missing_state_thread_fails() fail { + let tx = + Transaction { + ..mock_transaction(), + inputs: [mock_one_shot_input()], + mint: assets.zero + |> assets.add(mock_market_hash, "YES", 100_000_000) + |> assets.add(mock_market_hash, "NO", 100_000_000), + } + validate_mint_tokens(mock_one_shot(), mock_market_hash, tx) +} + +// ── BurnTokens tests ───────────────────────────────────────────────────────── + +test burn_valid() { + let tx = + Transaction { + ..mock_transaction(), + mint: assets.zero + |> assets.add(mock_market_hash, "YES", -50_000_000) + |> assets.add(mock_market_hash, "NO", -10_000_000), + } + validate_burn_tokens(mock_market_hash, tx) +} + +test burn_positive_amount_fails() fail { + let tx = + Transaction { + ..mock_transaction(), + mint: // One positive mint — should fail burn validation + assets.zero + |> assets.add(mock_market_hash, "YES", 50_000_000), + } + validate_burn_tokens(mock_market_hash, tx) +} + +test burn_mixed_amounts_fails() fail { + let tx = + Transaction { + ..mock_transaction(), + mint: // One negative, one positive — should fail + assets.zero + |> assets.add(mock_market_hash, "YES", -50_000_000) + |> assets.add(mock_market_hash, "NO", 10_000_000), + } + validate_burn_tokens(mock_market_hash, tx) +} diff --git a/lazer/cardano/prediction-market/offchain/.env.example b/lazer/cardano/prediction-market/offchain/.env.example new file mode 100644 index 00000000..70527016 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/.env.example @@ -0,0 +1,6 @@ +BLOCKFROST_API_KEY=preprodXXX +NETWORK=Preprod +WALLET_MNEMONIC=your 24 word mnemonic here +PYTH_POLICY_ID=d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6 +PYTH_API_KEY=your-pyth-lazer-api-key +FEED_ID=BTC/USD diff --git a/lazer/cardano/prediction-market/offchain/.gitignore b/lazer/cardano/prediction-market/offchain/.gitignore new file mode 100644 index 00000000..713d5006 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env diff --git a/lazer/cardano/prediction-market/offchain/index.ts b/lazer/cardano/prediction-market/offchain/index.ts new file mode 100644 index 00000000..00b10679 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/index.ts @@ -0,0 +1,737 @@ +import * as fs from "fs"; +import * as Core from "@blaze-cardano/core"; +import { HotWallet } from "@blaze-cardano/wallet"; +import { Blockfrost } from "@blaze-cardano/query"; +import { Blaze } from "@blaze-cardano/sdk"; +import { applyParams } from "@blaze-cardano/uplc"; +import { makeUplcEvaluator } from "@blaze-cardano/vm"; + +// ── Config from env (Bun auto-loads .env) ──────────────────────────────────── + +const BLOCKFROST_API_KEY = process.env.BLOCKFROST_API_KEY!; +const NETWORK = process.env.NETWORK ?? "Preprod"; +const WALLET_MNEMONIC = process.env.WALLET_MNEMONIC!; +const PYTH_POLICY_ID = process.env.PYTH_POLICY_ID!; +const PYTH_API_KEY = process.env.PYTH_API_KEY!; +const FEED_ID = process.env.FEED_ID ?? "BTC/USD"; + +// ── Blueprint ──────────────────────────────────────────────────────────────── + +const blueprint = JSON.parse( + fs.readFileSync("../contracts/plutus.json", "utf-8") +); + +function getValidator(title: string): string { + const v = blueprint.validators.find((v: any) => v.title === title); + if (!v) throw new Error(`Validator not found: ${title}`); + return v.compiledCode; +} + +const marketSpendCode = getValidator("market.market.spend"); + +// ── Datum helpers ──────────────────────────────────────────────────────────── + +function constr(idx: number, fields: Core.PlutusData[]): Core.PlutusData { + const list = new Core.PlutusList(); + for (const f of fields) list.add(f); + return Core.PlutusData.newConstrPlutusData( + new Core.ConstrPlutusData(BigInt(idx), list) + ); +} + +function bytes(hex: string): Core.PlutusData { + return Core.PlutusData.newBytes(Buffer.from(hex, "hex")); +} + +function integer(n: bigint): Core.PlutusData { + return Core.PlutusData.newInteger(n); +} + +const FALSE = constr(0, []); +const TRUE = constr(1, []); +const NONE = constr(1, []); +const MINT_TOKENS = constr(0, []); +const BURN_TOKENS = constr(1, []); +const RESOLVE = constr(1, []); + +function mkMarketDatum(p: { + creator: string; + pythId: string; + feedId: number; + targetPrice: bigint; + resolutionTime: bigint; + tokenPolicy: string; + seed: bigint; +}): Core.PlutusData { + return constr(0, [ + bytes(p.creator), + bytes(p.pythId), + integer(BigInt(p.feedId)), + integer(p.targetPrice), + integer(p.resolutionTime), + bytes(p.tokenPolicy), + integer(p.seed), // yes_reserve + integer(p.seed), // no_reserve + integer(p.seed * p.seed), // k + integer(p.seed), // total_yes_minted + integer(p.seed), // total_no_minted + integer(p.seed), // total_ada + FALSE, // resolved + NONE, // winning_side + ]); +} + +// ── Pyth API ───────────────────────────────────────────────────────────────── + +const FEED_NAME_TO_ID: Record = { + "BTC/USD": 1, + "ETH/USD": 2, + "ADA/USD": 16, +}; + +async function getPythUpdate(feedName: string) { + const feedId = FEED_NAME_TO_ID[feedName] ?? parseInt(feedName); + const res = await fetch("https://pyth-lazer.dourolabs.app/v1/latest_price", { + method: "POST", + headers: { + Authorization: `Bearer ${PYTH_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + channel: "fixed_rate@200ms", + formats: ["solana"], + priceFeedIds: [feedId], + properties: ["price", "exponent"], + jsonBinaryEncoding: "hex", + parsed: true, + }), + }); + const data = (await res.json()) as any; + return { + solanaHex: data.solana?.data as string, + price: BigInt(data.parsed?.priceFeeds?.[0]?.price ?? "0"), + exponent: data.parsed?.priceFeeds?.[0]?.exponent ?? -8, + feedId, + }; +} + +// ── Wallet setup ───────────────────────────────────────────────────────────── + +async function setupBlaze() { + const networkId = + NETWORK === "Mainnet" ? Core.NetworkId.Mainnet : Core.NetworkId.Testnet; + const blazeNetwork = `cardano-${NETWORK.toLowerCase()}` as "cardano-preprod" | "cardano-preview" | "cardano-mainnet"; + const provider = new Blockfrost({ + network: blazeNetwork, + projectId: BLOCKFROST_API_KEY, + }); + + const entropy = Core.mnemonicToEntropy(WALLET_MNEMONIC, Core.wordlist); + const masterKey = Core.Bip32PrivateKey.fromBip39Entropy( + Buffer.from(entropy), + "" + ); + const wallet = await HotWallet.fromMasterkey( + masterKey.hex(), + provider, + networkId + ); + + const blaze = await Blaze.from(provider, wallet); + console.log(` Wallet: ${wallet.address.toBech32()}`); + return { blaze, provider, wallet }; +} + +// ── Commands ───────────────────────────────────────────────────────────────── + +async function cmdPrice() { + const feedName = process.argv[3] ?? FEED_ID; + const update = await getPythUpdate(feedName); + const display = Number(update.price) * Math.pow(10, update.exponent); + console.log( + ` ${feedName}: ${display.toFixed(2)} (raw: ${update.price}, exp: ${update.exponent})` + ); +} + +async function cmdDeploy() { + const { blaze, provider, wallet } = await setupBlaze(); + + // Deploy unparameterized market script as reference + const script = Core.Script.newPlutusV3Script( + new Core.PlutusV3Script(Core.HexBlob(marketSpendCode)) + ); + + const tx = await blaze + .newTransaction() + .payToAddressWithData( + wallet.address, + { kind: Core.DatumKind.Inline, value: Core.PlutusData.newBytes(Buffer.from("deploy", "utf-8")) }, + new Core.Value(5_000_000n), + script + ) + .complete(); + + const signed = await blaze.signTransaction(tx); + const txId = await blaze.provider.postTransactionToChain(signed); + console.log(` Deploy tx: ${txId}`); +} + +async function cmdCreate() { + const { blaze, provider, wallet } = await setupBlaze(); + const feedName = process.argv[3] ?? FEED_ID; + const seedAda = BigInt(process.argv[4] ?? "10") * 1_000_000n; + + // Get current price + const update = await getPythUpdate(feedName); + console.log(` Price: ${update.price} (feed ${update.feedId})`); + + // Get one-shot UTxO + const utxos = await provider.getUnspentOutputs(wallet.address); + const oneShot = utxos[0]!; + const oneShotRef = oneShot.input(); + console.log(` One-shot: ${oneShotRef.transactionId()}#${oneShotRef.index()}`); + + // Apply params: MarketParams(constr(0, [OutputReference(constr(0, [txId, idx]))])) + const oneShotParam = constr(0, [ + constr(0, [ + bytes(oneShotRef.transactionId().toString()), + integer(BigInt(oneShotRef.index())), + ]), + ]); + + const parameterizedCode = applyParams( + Core.HexBlob(marketSpendCode), + oneShotParam + ); + const marketScript = Core.Script.newPlutusV3Script( + new Core.PlutusV3Script(Core.HexBlob(parameterizedCode)) + ); + const policyId = Core.PolicyId( + marketScript.hash().toString() as Core.Hash28ByteBase16 + ); + const scriptAddr = Core.addressFromValidator(Core.NetworkId.Testnet, marketScript); + + console.log(` Policy: ${policyId}`); + console.log(` Address: ${scriptAddr.toBech32()}`); + + // Build datum + const paymentHash = wallet.address.getProps().paymentPart!.hash; + const resolutionTime = BigInt(Date.now() + 300_000); + + const datum = mkMarketDatum({ + creator: paymentHash, + pythId: PYTH_POLICY_ID, + feedId: update.feedId, + targetPrice: update.price, + resolutionTime, + tokenPolicy: policyId, + seed: seedAda, + }); + + // Build tx: consume one-shot + lock ADA at script + mint tokens + // Mint + lock in one tx (script in witness set for mint) + const stateThreadName = Core.AssetName(""); + const yesName = Core.AssetName("594553"); + const noName = Core.AssetName("4e4f"); + + const mintAssets = new Map(); + mintAssets.set(stateThreadName, 1n); + mintAssets.set(yesName, seedAda); + mintAssets.set(noName, seedAda); + + const stateThreadAsset = Core.AssetId.fromParts(policyId, stateThreadName); + const lockValue = new Core.Value(seedAda); + lockValue.setMultiasset(new Map([[stateThreadAsset, 1n]])); + + const tx = await blaze + .newTransaction() + .addInput(oneShot) + .lockAssets(scriptAddr, lockValue, datum) + .addMint(policyId, mintAssets, MINT_TOKENS) + .provideScript(marketScript) + .complete(); + + const signed = await blaze.signTransaction(tx); + const txId = await blaze.provider.postTransactionToChain(signed); + console.log(`\n Market created: ${txId}`); + console.log(` Policy: ${policyId}`); + console.log(` One-shot: ${oneShotRef.transactionId()}#${oneShotRef.index()}`); + console.log(`\n Commands (use policy + oneshot for all):`); + console.log(` bun run index.ts bet ${policyId} ${oneShotRef.transactionId()} ${oneShotRef.index()} yes 2`); + console.log(` bun run index.ts resolve ${policyId} ${oneShotRef.transactionId()} ${oneShotRef.index()}`); + console.log(` bun run index.ts claim ${policyId} ${oneShotRef.transactionId()} ${oneShotRef.index()}`); +} + +async function cmdResolve() { + const { blaze, provider, wallet } = await setupBlaze(); + const policyIdHex = process.argv[3]!; + const oneShotTxHash = process.argv[4]!; + const oneShotIdx = parseInt(process.argv[5] ?? "0"); + + if (!policyIdHex || !oneShotTxHash) { + throw new Error("Usage: resolve "); + } + + const { marketScript, policyId, scriptAddr, marketUtxo, df } = await deriveMarket( + provider, policyIdHex, oneShotTxHash, oneShotIdx + ); + console.log(` Market UTxO: ${marketUtxo.input().transactionId()}#${marketUtxo.input().index()}`); + + const feedId = Number(df.get(2)!.asInteger()!); + const targetPrice = df.get(3)!.asInteger()!; + const resolutionTime = Number(df.get(4)!.asInteger()!); + const totalAda = df.get(11)!.asInteger()!; + const inputLovelace = marketUtxo.output().amount().coin(); + console.log(` Feed: ${feedId}, Target: ${targetPrice}, Resolution: ${new Date(resolutionTime).toISOString()}`); + console.log(` Input lovelace: ${inputLovelace}, Datum total_ada: ${totalAda}`); + + // Get Pyth price + const feedName = Object.entries(FEED_NAME_TO_ID).find(([_, id]) => id === feedId)?.[0] ?? String(feedId); + const update = await getPythUpdate(feedName); + console.log(` Pyth price: ${update.price}, solana hex len: ${update.solanaHex.length}`); + + const yesWins = update.price > targetPrice; + console.log(` Winner: ${yesWins ? "YES" : "NO"} (price ${update.price} ${yesWins ? ">" : "<="} target ${targetPrice})`); + + // Build resolved datum: copy fields 0-11 unchanged, set resolved=True + winning_side + // Option: Some(Yes)=constr(0,[constr(0,[])]), Some(No)=constr(0,[constr(1,[])]) + const winningSide = constr(0, [yesWins ? constr(0, []) : constr(1, [])]); + const resolvedDatum = constr(0, [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, + df.get(5)!, df.get(6)!, df.get(7)!, df.get(8)!, df.get(9)!, + df.get(10)!, df.get(11)!, + TRUE, winningSide, + ]); + + // Find Pyth State UTxO + const pythStateAddr = Core.addressFromBech32( + "addr_test1wrm3tr5zpw9k2nefjtsz66wfzn6flnphr5kd6ak9ufrl3wcqqfyn8" + ); + const pythUtxos = await provider.getUnspentOutputs(pythStateAddr); + const pythStateUtxo = pythUtxos.find((u) => { + const ma = u.output().amount().multiasset(); + if (!ma) return false; + for (const [assetId] of ma) { + if (assetId.includes(PYTH_POLICY_ID)) return true; + } + return false; + }); + if (!pythStateUtxo) throw new Error("Pyth State UTxO not found"); + console.log(` Pyth State: ${pythStateUtxo.input().transactionId()}#${pythStateUtxo.input().index()}`); + + // Get Pyth withdraw script from ref + const pythRefScript = pythStateUtxo.output().scriptRef()!; + const pythWithdrawHash = pythRefScript.hash(); + console.log(` Pyth withdraw: ${pythWithdrawHash}`); + + // Build Pyth redeemer: List + const updateBuf = Buffer.from(update.solanaHex, "hex"); + const pythRedeemerList = new Core.PlutusList(); + pythRedeemerList.add(Core.PlutusData.newBytes(updateBuf)); + const pythRedeemer = Core.PlutusData.newList(pythRedeemerList); + + // Lock value: use max(inputLovelace, totalAda) to satisfy output_lovelace >= input_lovelace + const outputLovelace = inputLovelace > totalAda ? inputLovelace : totalAda; + const stateThreadAsset = Core.AssetId.fromParts(policyId, Core.AssetName("")); + const lockValue = new Core.Value(outputLovelace); + lockValue.setMultiasset(new Map([[stateThreadAsset, 1n]])); + + // Pyth reward address for withdraw-0 + const pythRewardAddrObj = Core.RewardAddress.fromCredentials( + Core.NetworkId.Testnet, + { type: Core.CredentialType.ScriptHash, hash: pythWithdrawHash as Core.Hash28ByteBase16 } + ); + const pythRewardAccount = Core.RewardAccount(pythRewardAddrObj.toAddress().toBech32()); + + // Build tx: spend market + Pyth withdraw-0 + lock resolved datum + const spendRedeemer = RESOLVE; // constr(1, []) + + // Slot config + posixToSlot with ceil to ensure lower bound >= resolution_time + const slotConfig = Core.SLOT_CONFIG_NETWORK[NETWORK as keyof typeof Core.SLOT_CONFIG_NETWORK] + ?? Core.SLOT_CONFIG_NETWORK.Preprod; + const posixToSlot = (posixMs: number) => + Math.ceil((posixMs - slotConfig.zeroTime) / slotConfig.slotLength) + slotConfig.zeroSlot; + + // validFrom must be >= resolution_time AND within the current slot range + const now = Date.now() - 60_000; + const effectiveTime = Math.max(resolutionTime, now); + const validFrom = Core.Slot(posixToSlot(effectiveTime)); + const validUntil = Core.Slot(validFrom + 300); + console.log(` Valid from slot: ${validFrom}, until: ${validUntil}`); + + // Use real UPLC VM evaluator — will run Plutus scripts locally and surface errors + const params = await provider.getParameters(); + const uplcEvaluator = makeUplcEvaluator(params, 1.2, 1.2, slotConfig); + + let tx; + try { + tx = await blaze + .newTransaction() + .useEvaluator(uplcEvaluator) + .addInput(marketUtxo, spendRedeemer) + .addReferenceInput(pythStateUtxo) + .lockAssets(scriptAddr, lockValue, resolvedDatum) + .addWithdrawal(pythRewardAccount, 0n, pythRedeemer) + .addRequiredSigner(Core.Ed25519KeyHashHex(wallet.address.getProps().paymentPart!.hash)) + .setValidFrom(validFrom) + .setValidUntil(validUntil) + .provideScript(marketScript) + .complete(); + } catch (e: any) { + console.error(`\n Build/eval error:`, e.message ?? e); + throw e; + } + + console.log(` Tx built, signing...`); + const signed = await blaze.signTransaction(tx); + + // Submit via Blockfrost + const txCbor = signed.toCbor(); + const submitRes = await fetch( + `https://cardano-${NETWORK.toLowerCase()}.blockfrost.io/api/v0/tx/submit`, + { + method: "POST", + headers: { + project_id: BLOCKFROST_API_KEY, + "Content-Type": "application/cbor", + }, + body: Buffer.from(txCbor, "hex"), + } + ); + const submitBody = await submitRes.text(); + if (!submitRes.ok) { + console.error(` Submit failed (${submitRes.status}): ${submitBody}`); + throw new Error(submitBody); + } + console.log(`\n Resolved: ${submitBody}`); +} + +// ── Shared: derive market script + find UTxO ───────────────────────────── + +async function deriveMarket(provider: any, policyIdHex: string, oneShotTxHash: string, oneShotIdx: number) { + const oneShotParam = constr(0, [ + constr(0, [bytes(oneShotTxHash), integer(BigInt(oneShotIdx))]), + ]); + const parameterizedCode = applyParams(Core.HexBlob(marketSpendCode), oneShotParam); + const marketScript = Core.Script.newPlutusV3Script( + new Core.PlutusV3Script(Core.HexBlob(parameterizedCode)) + ); + const policyId = Core.PolicyId(policyIdHex as Core.Hash28ByteBase16); + const scriptAddr = Core.addressFromValidator(Core.NetworkId.Testnet, marketScript); + + // Find market UTxO at script address (has the state thread token) + const utxos = await provider.getUnspentOutputs(scriptAddr); + const stateThreadAssetId = Core.AssetId.fromParts(policyId, Core.AssetName("")); + const marketUtxo = utxos.find((u: Core.TransactionUnspentOutput) => { + const ma = u.output().amount().multiasset(); + if (!ma) return false; + return (ma.get(stateThreadAssetId) ?? 0n) === 1n; + }); + if (!marketUtxo) throw new Error("Market UTxO not found at script address"); + + const datumData = marketUtxo.output().datum()!.asInlineData()!; + const df = datumData.asConstrPlutusData()!.getData(); + + return { marketScript, policyId, scriptAddr, marketUtxo, df }; +} + +// ── Bet command ────────────────────────────────────────────────────────── + +async function cmdBet() { + const { blaze, provider, wallet } = await setupBlaze(); + const policyIdHex = process.argv[3]!; + const oneShotTxHash = process.argv[4]!; + const oneShotIdx = parseInt(process.argv[5] ?? "0"); + const direction = (process.argv[6] ?? "yes").toLowerCase(); + const amountAda = BigInt(process.argv[7] ?? "2"); + const amountLovelace = amountAda * 1_000_000n; + + if (!policyIdHex || !oneShotTxHash) { + throw new Error("Usage: bet "); + } + + const { marketScript, policyId, scriptAddr, marketUtxo, df } = await deriveMarket( + provider, policyIdHex, oneShotTxHash, oneShotIdx + ); + console.log(` Market: ${marketUtxo.input().transactionId()}#${marketUtxo.input().index()}`); + + // Parse datum fields + const yesReserve = df.get(6)!.asInteger()!; + const noReserve = df.get(7)!.asInteger()!; + const k = df.get(8)!.asInteger()!; + const totalYesMinted = df.get(9)!.asInteger()!; + const totalNoMinted = df.get(10)!.asInteger()!; + const totalAda = df.get(11)!.asInteger()!; + const resolutionTime = Number(df.get(4)!.asInteger()!); + const inputLovelace = marketUtxo.output().amount().coin(); + + console.log(` Reserves: YES=${yesReserve}, NO=${noReserve}, k=${k}`); + console.log(` Betting ${direction.toUpperCase()} with ${amountAda} ADA (${amountLovelace} lovelace)`); + + // AMM calculation — must match contract's integer division exactly + let tokensOut: bigint; + let newDatumFields: Core.PlutusData[]; + if (direction === "yes") { + tokensOut = yesReserve - k / (noReserve + amountLovelace); + if (tokensOut <= 0n) throw new Error(`tokens_out=${tokensOut}, bet too small`); + newDatumFields = [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, df.get(5)!, + integer(yesReserve - tokensOut), // yes_reserve + integer(noReserve + amountLovelace), // no_reserve + df.get(8)!, // k unchanged + integer(totalYesMinted + tokensOut), // total_yes_minted + df.get(10)!, // total_no_minted unchanged + integer(totalAda + amountLovelace), // total_ada + df.get(12)!, df.get(13)!, // resolved, winning_side unchanged + ]; + } else { + tokensOut = noReserve - k / (yesReserve + amountLovelace); + if (tokensOut <= 0n) throw new Error(`tokens_out=${tokensOut}, bet too small`); + newDatumFields = [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, df.get(5)!, + integer(yesReserve + amountLovelace), // yes_reserve + integer(noReserve - tokensOut), // no_reserve + df.get(8)!, // k unchanged + df.get(9)!, // total_yes_minted unchanged + integer(totalNoMinted + tokensOut), // total_no_minted + integer(totalAda + amountLovelace), // total_ada + df.get(12)!, df.get(13)!, // resolved, winning_side unchanged + ]; + } + console.log(` Tokens out: ${tokensOut}`); + + const newDatum = constr(0, newDatumFields); + + // Mint tokens + const tokenName = direction === "yes" ? Core.AssetName("594553") : Core.AssetName("4e4f"); + const mintAssets = new Map(); + mintAssets.set(tokenName, tokensOut); + + // Lock value: input lovelace + bet amount + state thread + const stateThreadAssetId = Core.AssetId.fromParts(policyId, Core.AssetName("")); + const lockValue = new Core.Value(inputLovelace + amountLovelace); + lockValue.setMultiasset(new Map([[stateThreadAssetId, 1n]])); + + // Bet redeemer: Bet { direction, amount } = constr(0, [dir, amount]) + const betRedeemer = constr(0, [ + direction === "yes" ? constr(0, []) : constr(1, []), + integer(amountLovelace), + ]); + + // Validity: upper bound <= resolution_time (use floor for <=) + const slotConfig = Core.SLOT_CONFIG_NETWORK[NETWORK as keyof typeof Core.SLOT_CONFIG_NETWORK] + ?? Core.SLOT_CONFIG_NETWORK.Preprod; + const posixToSlotFloor = (posixMs: number) => + Math.floor((posixMs - slotConfig.zeroTime) / slotConfig.slotLength) + slotConfig.zeroSlot; + const validFrom = Core.Slot(posixToSlotFloor(Date.now() - 60_000)); + const validUntil = Core.Slot(posixToSlotFloor(resolutionTime)); + console.log(` Valid from: ${validFrom}, until: ${validUntil} (resolution)`); + + const params = await provider.getParameters(); + const uplcEvaluator = makeUplcEvaluator(params, 1.2, 1.2, slotConfig); + + let tx; + try { + tx = await blaze + .newTransaction() + .useEvaluator(uplcEvaluator) + .addInput(marketUtxo, betRedeemer) + .lockAssets(scriptAddr, lockValue, newDatum) + .addMint(policyId, mintAssets, MINT_TOKENS) + .setValidFrom(validFrom) + .setValidUntil(validUntil) + .provideScript(marketScript) + .complete(); + } catch (e: any) { + console.error(`\n Build/eval error:`, e.message ?? e); + throw e; + } + + console.log(` Tx built, signing...`); + const signed = await blaze.signTransaction(tx); + const txCbor = signed.toCbor(); + const submitRes = await fetch( + `https://cardano-${NETWORK.toLowerCase()}.blockfrost.io/api/v0/tx/submit`, + { + method: "POST", + headers: { project_id: BLOCKFROST_API_KEY, "Content-Type": "application/cbor" }, + body: Buffer.from(txCbor, "hex"), + } + ); + const submitBody = await submitRes.text(); + if (!submitRes.ok) { + console.error(` Submit failed (${submitRes.status}): ${submitBody}`); + throw new Error(submitBody); + } + console.log(`\n Bet placed: ${submitBody}`); + console.log(` Got ${tokensOut} ${direction.toUpperCase()} tokens`); +} + +// ── Claim command ──────────────────────────────────────────────────────── + +async function cmdClaim() { + const { blaze, provider, wallet } = await setupBlaze(); + const policyIdHex = process.argv[3]!; + const oneShotTxHash = process.argv[4]!; + const oneShotIdx = parseInt(process.argv[5] ?? "0"); + const burnAmountArg = process.argv[6]; // optional — defaults to all winning tokens in wallet + + if (!policyIdHex || !oneShotTxHash) { + throw new Error("Usage: claim [burn_amount]"); + } + + const { marketScript, policyId, scriptAddr, marketUtxo, df } = await deriveMarket( + provider, policyIdHex, oneShotTxHash, oneShotIdx + ); + console.log(` Market: ${marketUtxo.input().transactionId()}#${marketUtxo.input().index()}`); + + // Market must be resolved + const resolvedField = df.get(12)!.asConstrPlutusData()!; + if (resolvedField.getAlternative() !== 1n) throw new Error("Market not resolved yet"); + + // Determine winning side from datum field 13 + const winningSideField = df.get(13)!.asConstrPlutusData()!; // Some(direction) + const innerDir = winningSideField.getData().get(0)!.asConstrPlutusData()!; + const yesWon = innerDir.getAlternative() === 0n; + console.log(` Winner: ${yesWon ? "YES" : "NO"}`); + + const winningTokenName = yesWon ? Core.AssetName("594553") : Core.AssetName("4e4f"); + const winningAssetId = Core.AssetId.fromParts(policyId, winningTokenName); + const totalWinningMinted = yesWon ? df.get(9)!.asInteger()! : df.get(10)!.asInteger()!; + const totalAda = df.get(11)!.asInteger()!; + const inputLovelace = marketUtxo.output().amount().coin(); + + // Find how many winning tokens the wallet holds + const walletUtxos = await provider.getUnspentOutputs(wallet.address); + let walletTokens = 0n; + for (const u of walletUtxos) { + const ma = u.output().amount().multiasset(); + if (ma) walletTokens += ma.get(winningAssetId) ?? 0n; + } + console.log(` Wallet has ${walletTokens} ${yesWon ? "YES" : "NO"} tokens`); + + const burnAmount = burnAmountArg ? BigInt(burnAmountArg) : walletTokens; + if (burnAmount <= 0n) throw new Error("No winning tokens to burn"); + + const payout = burnAmount * totalAda / totalWinningMinted; + const isLastClaim = totalWinningMinted === burnAmount; + console.log(` Burning: ${burnAmount}, Payout: ${payout} lovelace (${Number(payout) / 1_000_000} ADA)`); + console.log(` Last claim: ${isLastClaim}`); + + // Claim redeemer: Claim { burn_amount } = constr(2, [burn_amount]) + const claimRedeemer = constr(2, [integer(burnAmount)]); + + // Burn: negative quantities + const burnAssets = new Map(); + burnAssets.set(winningTokenName, -burnAmount); + if (isLastClaim) { + burnAssets.set(Core.AssetName(""), -1n); // burn state thread + } + + // Build updated datum for continuing output (not needed for last claim) + const stateThreadAssetId = Core.AssetId.fromParts(policyId, Core.AssetName("")); + + const slotConfig = Core.SLOT_CONFIG_NETWORK[NETWORK as keyof typeof Core.SLOT_CONFIG_NETWORK] + ?? Core.SLOT_CONFIG_NETWORK.Preprod; + const params = await provider.getParameters(); + const uplcEvaluator = makeUplcEvaluator(params, 1.2, 1.2, slotConfig); + + let txBuilder = blaze + .newTransaction() + .useEvaluator(uplcEvaluator) + .addInput(marketUtxo, claimRedeemer) + .addMint(policyId, burnAssets, BURN_TOKENS) + .provideScript(marketScript); + + if (!isLastClaim) { + // Continuing output with reduced datum + const newDatumFields = []; + for (let i = 0; i < 14; i++) newDatumFields.push(df.get(i)!); + // Update total_ada and winning minted count + newDatumFields[11] = integer(totalAda - payout); + if (yesWon) { + newDatumFields[9] = integer(totalWinningMinted - burnAmount); + } else { + newDatumFields[10] = integer(totalWinningMinted - burnAmount); + } + const newDatum = constr(0, newDatumFields); + + const lockValue = new Core.Value(inputLovelace - payout); + lockValue.setMultiasset(new Map([[stateThreadAssetId, 1n]])); + txBuilder = txBuilder.lockAssets(scriptAddr, lockValue, newDatum); + } + + let tx; + try { + tx = await txBuilder.complete(); + } catch (e: any) { + console.error(`\n Build/eval error:`, e.message ?? e); + throw e; + } + + console.log(` Tx built, signing...`); + const signed = await blaze.signTransaction(tx); + const txCbor = signed.toCbor(); + const submitRes = await fetch( + `https://cardano-${NETWORK.toLowerCase()}.blockfrost.io/api/v0/tx/submit`, + { + method: "POST", + headers: { project_id: BLOCKFROST_API_KEY, "Content-Type": "application/cbor" }, + body: Buffer.from(txCbor, "hex"), + } + ); + const submitBody = await submitRes.text(); + if (!submitRes.ok) { + console.error(` Submit failed (${submitRes.status}): ${submitBody}`); + throw new Error(submitBody); + } + console.log(`\n Claimed: ${submitBody}`); + console.log(` Payout: ${Number(payout) / 1_000_000} ADA`); +} + +// ── Main ───────────────────────────────────────────────────────────────────── + +const command = process.argv[2] ?? "help"; + +async function main() { + console.log(` Network: ${NETWORK}`); + console.log(` Command: ${command}`); + + switch (command) { + case "price": + await cmdPrice(); + break; + case "deploy": + await cmdDeploy(); + break; + case "create": + await cmdCreate(); + break; + case "resolve": + await cmdResolve(); + break; + case "bet": + await cmdBet(); + break; + case "claim": + await cmdClaim(); + break; + case "help": + default: + console.log(` + Prediction Market CLI (TypeScript + Blaze) + + Commands: + price [feed] Check Pyth price + create [feed] [ada] Create market (5-min, default: BTC/USD 10 ADA) + bet [ada] Place bet (default: 2 ADA) + resolve Resolve after 5 min + claim [burn_amount] Claim winnings + `); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/lazer/cardano/prediction-market/offchain/package.json b/lazer/cardano/prediction-market/offchain/package.json new file mode 100644 index 00000000..9bd653d1 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/package.json @@ -0,0 +1,18 @@ +{ + "name": "offchain", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@blaze-cardano/data": "^0.6.6", + "@blaze-cardano/sdk": "^0.2.48", + "@blaze-cardano/uplc": "^0.4.3", + "@pythnetwork/pyth-lazer-sdk": "^6.2.1" + } +} diff --git a/lazer/cardano/prediction-market/offchain/tsconfig.json b/lazer/cardano/prediction-market/offchain/tsconfig.json new file mode 100644 index 00000000..bfa0fead --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From 2cf9289fad62336ab4b70f6227cd63908bf6432e Mon Sep 17 00:00:00 2001 From: Mercurial Date: Mon, 23 Mar 2026 07:51:18 +0800 Subject: [PATCH 2/2] Add market-maker bot, live frontend with wallet integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bot: creates 5-min BTC/USD markets, auto-resolves via Pyth Lazer - Frontend: real-time price streaming, countdown timer, YES/NO betting - Wallet: CIP-30 via Bifrost (Nami/Eternl/Lace), real tx signing - Server: Bun.serve() with API routes, WebSocket pub/sub, Pyth streaming - Refactored CLI into shared lib/ modules (cardano, pyth, market, types) Bet flow: wallet UTxOs → server builds unsigned tx → wallet signs → finalize → submit --- lazer/cardano/prediction-market/README.md | 87 ++-- .../prediction-market/offchain/.env.example | 7 + .../prediction-market/offchain/.gitignore | 1 + .../cardano/prediction-market/offchain/bot.ts | 105 +++++ .../cardano/prediction-market/offchain/cli.ts | 83 ++++ .../prediction-market/offchain/frontend.tsx | 436 ++++++++++++++++++ .../prediction-market/offchain/index.html | 29 ++ .../offchain/lib/bifrost-global.d.ts | 9 + .../offchain/lib/bifrost-types.ts | 50 ++ .../prediction-market/offchain/lib/bifrost.ts | 19 + .../prediction-market/offchain/lib/cardano.ts | 139 ++++++ .../prediction-market/offchain/lib/market.ts | 411 +++++++++++++++++ .../prediction-market/offchain/lib/pyth.ts | 77 ++++ .../prediction-market/offchain/lib/types.ts | 60 +++ .../prediction-market/offchain/package.json | 16 +- .../prediction-market/offchain/server.ts | 182 ++++++++ 16 files changed, 1664 insertions(+), 47 deletions(-) create mode 100644 lazer/cardano/prediction-market/offchain/bot.ts create mode 100644 lazer/cardano/prediction-market/offchain/cli.ts create mode 100644 lazer/cardano/prediction-market/offchain/frontend.tsx create mode 100644 lazer/cardano/prediction-market/offchain/index.html create mode 100644 lazer/cardano/prediction-market/offchain/lib/bifrost-global.d.ts create mode 100644 lazer/cardano/prediction-market/offchain/lib/bifrost-types.ts create mode 100644 lazer/cardano/prediction-market/offchain/lib/bifrost.ts create mode 100644 lazer/cardano/prediction-market/offchain/lib/cardano.ts create mode 100644 lazer/cardano/prediction-market/offchain/lib/market.ts create mode 100644 lazer/cardano/prediction-market/offchain/lib/pyth.ts create mode 100644 lazer/cardano/prediction-market/offchain/lib/types.ts create mode 100644 lazer/cardano/prediction-market/offchain/server.ts diff --git a/lazer/cardano/prediction-market/README.md b/lazer/cardano/prediction-market/README.md index 94c8cd8e..951b812c 100644 --- a/lazer/cardano/prediction-market/README.md +++ b/lazer/cardano/prediction-market/README.md @@ -2,6 +2,8 @@ A decentralized prediction market on Cardano that uses **Pyth Lazer** oracle for real-time price feeds. Users create markets on any Pyth-supported asset (BTC, ETH, ADA, etc.), place bets through a constant-product AMM, and settle based on verified on-chain prices. +Includes a **market-maker bot** that continuously creates 1-minute markets and a **live frontend** with real-time Pyth price streaming and CIP-30 wallet integration for placing bets. + ## Team Cliley - **Clark Alesna** (Captain) — clark@saib.dev @@ -13,36 +15,44 @@ The contract integrates Pyth Lazer via the **withdraw-0 pattern** — the standa 1. **Market Creation** — The creator sets a target price and a Pyth feed ID (e.g., BTC/USD = 1). The Pyth deployment policy ID is stored in the on-chain datum. -2. **Market Resolution** — After the 5-minute window, anyone can resolve the market: - - The off-chain CLI fetches a signed price update from the Pyth Lazer REST API - - The signed message is included as a withdrawal redeemer in the transaction - - The Pyth State UTxO (holding the Pyth NFT + withdraw script) is added as a reference input - - On-chain, the validator calls `pyth.get_updates()` which: - - Finds the Pyth State UTxO via the stored policy ID - - Extracts the withdraw script hash from the Pyth state datum - - Reads the signed price update from the withdrawal redeemer - - Parses the Pyth Lazer message format to extract the verified price - - The contract compares `oracle_price > target_price` to determine the winning side - -3. **Key Pyth Integration Points:** +2. **Market Resolution** — After the window closes, the bot resolves the market: + - Fetches a signed price update from the Pyth Lazer REST API + - Includes the signed message as a withdrawal redeemer in the transaction + - On-chain, the validator calls `pyth.get_updates()` to extract the verified price + - Compares `oracle_price > target_price` to determine the winning side + +3. **Real-time Price Streaming** — The frontend uses `@pythnetwork/pyth-lazer-sdk` WebSocket streaming for live BTC price display (sub-second updates). + +4. **Key Pyth Integration Points:** - `pyth-network/pyth-lazer-cardano` Aiken library (contract side) - - Pyth Lazer REST API at `https://pyth-lazer.dourolabs.app/v1/latest_price` (off-chain side) + - Pyth Lazer REST API — `POST /v1/latest_price` (resolve transactions) + - Pyth Lazer WebSocket — `wss://pyth-lazer-0.dourolabs.app/v1/stream` (live price feed) - Pyth State UTxO on Preprod: policy `d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6` ## Architecture ``` -contracts/ Aiken smart contracts (Plutus V3) -├── lib/ Validation logic + types -│ └── prediction_market/ -│ ├── types.ak MarketDatum, redeemers, params -│ └── market_validation.ak Bet, Resolve, Claim, Mint/Burn +contracts/ Aiken smart contracts (Plutus V3) +├── lib/prediction_market/ +│ ├── types.ak MarketDatum, redeemers, params +│ └── market_validation.ak Bet, Resolve, Claim, Mint/Burn validation ├── validators/ -│ ├── market.ak Main validator (dual spend + mint) -│ └── pyth_test.ak Pyth integration test validator -│ └── tests/ Unit tests -offchain/ TypeScript CLI (Bun + blaze-cardano) -├── index.ts Full CLI: create, bet, resolve, claim, price +│ ├── market.ak Main validator (dual spend + mint) +│ ├── pyth_test.ak Pyth integration test validator +│ └── tests/ Unit tests + +offchain/ TypeScript (Bun + blaze-cardano) +├── lib/ +│ ├── cardano.ts Blaze setup, datum helpers, tx submission +│ ├── pyth.ts Pyth HTTP API + WebSocket streaming +│ ├── market.ts Market operations (create, resolve, bet, claim) +│ ├── bifrost.ts CIP-30 wallet connector (Nami, Eternl, Lace) +│ └── types.ts Shared types +├── cli.ts CLI for manual operations +├── bot.ts Market-maker bot (1-min cycles) +├── server.ts Bun.serve() — API, WebSocket, frontend +├── index.html Frontend shell +└── frontend.tsx React app with live price + wallet + betting ``` ## Contract Design @@ -56,8 +66,6 @@ offchain/ TypeScript CLI (Bun + blaze-cardano) | **Resolve** | Pyth Lazer price check, set `resolved=True` + `winning_side` | | **Claim** | Burn winning tokens, receive proportional ADA payout | -The state thread token (empty asset name) ensures continuity across transactions. The AMM uses `k = yes_reserve * no_reserve` invariant. - ## Verified on Preprod Full end-to-end flow tested on Cardano Preprod: @@ -86,38 +94,29 @@ aiken build aiken check # run tests ``` -### Run Off-chain CLI +### Run the Frontend + Bot ```bash cd offchain cp .env.example .env # fill in your keys bun install -bun run index.ts help +bun --hot server.ts # starts bot + frontend at http://localhost:3000 ``` -### Full Flow +The bot automatically creates 1-minute BTC/USD markets, resolves them via Pyth, and loops. The frontend shows real-time prices and lets users bet via their browser wallet. -```bash -# Check current BTC price -bun run index.ts price BTC/USD +### CLI (Manual Operations) -# Create a 5-min market (10 ADA seed, BTC/USD) -bun run index.ts create BTC/USD 10 -# → prints policy + oneshot params for subsequent commands - -# Place a bet (2 ADA on YES) -bun run index.ts bet yes 2 - -# Wait 5 minutes, then resolve -bun run index.ts resolve - -# Claim winnings -bun run index.ts claim +```bash +bun cli.ts price BTC/USD +bun cli.ts create BTC/USD 10 +bun cli.ts bet yes 2 +bun cli.ts resolve ``` ## Supported Feeds -Any Pyth Lazer feed ID works. Common ones: +Any Pyth Lazer feed ID works: | Feed | ID | |------|----| diff --git a/lazer/cardano/prediction-market/offchain/.env.example b/lazer/cardano/prediction-market/offchain/.env.example index 70527016..53bfd8e1 100644 --- a/lazer/cardano/prediction-market/offchain/.env.example +++ b/lazer/cardano/prediction-market/offchain/.env.example @@ -1,6 +1,13 @@ +# Cardano (Preprod) BLOCKFROST_API_KEY=preprodXXX NETWORK=Preprod + +# Wallet (bot uses this for creating/resolving markets) WALLET_MNEMONIC=your 24 word mnemonic here + +# Pyth Lazer PYTH_POLICY_ID=d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6 PYTH_API_KEY=your-pyth-lazer-api-key + +# Feed FEED_ID=BTC/USD diff --git a/lazer/cardano/prediction-market/offchain/.gitignore b/lazer/cardano/prediction-market/offchain/.gitignore index 713d5006..b23cd046 100644 --- a/lazer/cardano/prediction-market/offchain/.gitignore +++ b/lazer/cardano/prediction-market/offchain/.gitignore @@ -1,2 +1,3 @@ node_modules/ .env +bun.lock diff --git a/lazer/cardano/prediction-market/offchain/bot.ts b/lazer/cardano/prediction-market/offchain/bot.ts new file mode 100644 index 00000000..60642f57 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/bot.ts @@ -0,0 +1,105 @@ +import { setupBlaze } from "./lib/cardano.ts"; +import { createMarket, resolveMarket, deriveMarket, parseDatum } from "./lib/market.ts"; +import type { BotState, MarketState } from "./lib/types.ts"; + +const MARKET_DURATION_MS = 5 * 60_000; // 5-minute markets +const SEED_ADA = 10n; +const CONFIRM_WAIT_MS = 30_000; +const MAX_HISTORY = 20; + +export const botState: BotState = { + currentMarket: null, + marketHistory: [], + latestPrice: null, + botStatus: "idle", + cycleCount: 0, + lastError: null, +}; + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export async function startBot(onStateChange: () => void) { + const { blaze, provider, wallet } = await setupBlaze(); + console.log(`[bot] Wallet: ${wallet.address.toBech32()}`); + + while (true) { + try { + // ── CREATE ── + botState.botStatus = "creating"; + botState.lastError = null; + onStateChange(); + console.log(`[bot] Cycle ${botState.cycleCount + 1}: Creating market...`); + + const result = await createMarket({ + blaze, provider, wallet, + feedName: "BTC/USD", + seedAda: SEED_ADA, + resolutionMs: MARKET_DURATION_MS, + }); + console.log(`[bot] Created: ${result.txId} (policy: ${result.policyId})`); + + // Poll until market UTxO appears on-chain + let derived; + for (let attempt = 1; attempt <= 12; attempt++) { + await sleep(10_000); + try { + derived = await deriveMarket(provider, result.policyId, result.oneShotTx, result.oneShotIdx); + break; + } catch { + console.log(`[bot] Waiting for confirmation... (${attempt * 10}s)`); + } + } + if (!derived) throw new Error("Market UTxO not confirmed after 120s"); + botState.currentMarket = parseDatum(derived.df, result.policyId, result.oneShotTx, result.oneShotIdx); + botState.botStatus = "waiting"; + onStateChange(); + + // ── WAIT for resolution ── + const waitMs = botState.currentMarket.resolutionTime - Date.now(); + if (waitMs > 0) { + console.log(`[bot] Waiting ${Math.round(waitMs / 1000)}s for resolution...`); + await sleep(waitMs); + } + + // ── RESOLVE ── + botState.botStatus = "resolving"; + onStateChange(); + console.log(`[bot] Resolving...`); + + const resolveResult = await resolveMarket({ + blaze, provider, wallet, + policyId: result.policyId, + oneShotTx: result.oneShotTx, + oneShotIdx: result.oneShotIdx, + }); + console.log(`[bot] Resolved: ${resolveResult.txId} (winner: ${resolveResult.winner})`); + + // Archive + if (botState.currentMarket) { + botState.currentMarket.resolved = true; + botState.currentMarket.winningSide = resolveResult.winner; + botState.marketHistory.unshift({ ...botState.currentMarket }); + if (botState.marketHistory.length > MAX_HISTORY) botState.marketHistory.pop(); + } + botState.currentMarket = null; + botState.cycleCount++; + onStateChange(); + + // Wait for resolve confirmation before next cycle + console.log(`[bot] Waiting for resolve confirmation...`); + await sleep(CONFIRM_WAIT_MS); + // Extra wait to ensure wallet UTxOs are updated for next create + await sleep(10_000); + + } catch (err: any) { + botState.botStatus = "error"; + botState.lastError = err.message ?? String(err); + onStateChange(); + console.error(`[bot] Error:`, err.message); + + const delay = Math.min(5000 * Math.pow(2, Math.min(botState.cycleCount, 3)), 30000); + console.log(`[bot] Retrying in ${delay / 1000}s...`); + await sleep(delay); + } + } +} diff --git a/lazer/cardano/prediction-market/offchain/cli.ts b/lazer/cardano/prediction-market/offchain/cli.ts new file mode 100644 index 00000000..ed16ba89 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/cli.ts @@ -0,0 +1,83 @@ +import { setupBlaze, submitTx } from "./lib/cardano.ts"; +import { getPythUpdate, FEED_NAME_TO_ID } from "./lib/pyth.ts"; +import { createMarket, resolveMarket, buildBetTx, deriveMarket, parseDatum } from "./lib/market.ts"; + +const FEED_ID = process.env.FEED_ID ?? "BTC/USD"; +const command = process.argv[2] ?? "help"; + +async function main() { + console.log(` Command: ${command}`); + + switch (command) { + case "price": { + const feedName = process.argv[3] ?? FEED_ID; + const update = await getPythUpdate(feedName); + const display = Number(update.price) * Math.pow(10, update.exponent); + console.log(` ${feedName}: ${display.toFixed(2)} (raw: ${update.price}, exp: ${update.exponent})`); + break; + } + + case "create": { + const { blaze, provider, wallet } = await setupBlaze(); + const feedName = process.argv[3] ?? FEED_ID; + const seedAda = BigInt(process.argv[4] ?? "10"); + const result = await createMarket({ + blaze, provider, wallet, feedName, seedAda, resolutionMs: 300_000, + }); + console.log(`\n Market created: ${result.txId}`); + console.log(` Policy: ${result.policyId}`); + console.log(` One-shot: ${result.oneShotTx}#${result.oneShotIdx}`); + console.log(`\n bun cli.ts bet ${result.policyId} ${result.oneShotTx} ${result.oneShotIdx} yes 2`); + console.log(` bun cli.ts resolve ${result.policyId} ${result.oneShotTx} ${result.oneShotIdx}`); + break; + } + + case "resolve": { + const { blaze, provider, wallet } = await setupBlaze(); + const result = await resolveMarket({ + blaze, provider, wallet, + policyId: process.argv[3]!, + oneShotTx: process.argv[4]!, + oneShotIdx: parseInt(process.argv[5] ?? "0"), + }); + console.log(` Resolved: ${result.txId} (winner: ${result.winner})`); + break; + } + + case "bet": { + const { blaze, provider, wallet } = await setupBlaze(); + const { tx, tokensOut } = await buildBetTx({ + blaze, provider, + policyId: process.argv[3]!, + oneShotTx: process.argv[4]!, + oneShotIdx: parseInt(process.argv[5] ?? "0"), + direction: (process.argv[6] ?? "yes") as "yes" | "no", + amountAda: BigInt(process.argv[7] ?? "2"), + }); + const signed = await blaze.signTransaction(tx); + const txId = await submitTx(signed); + console.log(` Bet placed: ${txId} (got ${tokensOut} tokens)`); + break; + } + + case "help": + default: + console.log(` + Prediction Market CLI + + Commands: + price [feed] Check Pyth price + create [feed] [ada] Create market (5-min, default: BTC/USD 10 ADA) + bet [ada] Place bet (default: 2 ADA) + resolve Resolve market + + Server: + bun --hot server.ts Start bot + frontend + `); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/lazer/cardano/prediction-market/offchain/frontend.tsx b/lazer/cardano/prediction-market/offchain/frontend.tsx new file mode 100644 index 00000000..5edf90d9 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/frontend.tsx @@ -0,0 +1,436 @@ +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { createRoot } from "react-dom/client"; +import { getWallets } from "./lib/bifrost.ts"; +import type { CardanoWalletApi, CardanoWallet, CborHex, Transaction } from "./lib/bifrost-types.ts"; + +// ── Hooks ──────────────────────────────────────────────────────────────── + +function useMarketSocket() { + const [price, setPrice] = useState(null); + const [market, setMarket] = useState(null); + const [history, setHistory] = useState([]); + const [status, setStatus] = useState({ botStatus: "idle", cycleCount: 0, lastError: null as string | null }); + + useEffect(() => { + let ws: WebSocket; + function connect() { + ws = new WebSocket(`ws://${window.location.host}/ws`); + ws.onmessage = (e) => { + const msg = JSON.parse(e.data); + switch (msg.type) { + case "price": setPrice(msg.data); break; + case "market": setMarket(msg.data); break; + case "history": setHistory(msg.data); break; + case "status": setStatus(msg.data); break; + } + }; + ws.onclose = () => setTimeout(connect, 2000); + } + connect(); + return () => ws?.close(); + }, []); + + return { price, market, history, status }; +} + +function useWallet() { + const [wallets, setWallets] = useState([]); + const [api, setApi] = useState(null); + const [address, setAddress] = useState(null); + const [walletName, setWalletName] = useState(null); + + const connectWallet = useCallback(async (wallet: CardanoWallet) => { + const walletApi = await wallet.enable(); + setApi(walletApi); + setWalletName(wallet.name); + localStorage.setItem("wallet_id", wallet.id); + const addrs = await walletApi.getUsedAddresses(); + if (addrs.length > 0) { + try { + const res = await fetch(`/api/hex-to-bech32?hex=${addrs[0]}`); + const { bech32 } = await res.json(); + setAddress(bech32.slice(0, 12) + "..." + bech32.slice(-4)); + } catch { + setAddress(addrs[0]!.slice(0, 8) + "..."); + } + } + }, []); + + useEffect(() => { + setTimeout(() => { + const detected = getWallets(); + setWallets(detected); + // Auto-reconnect from localStorage + const savedId = localStorage.getItem("wallet_id"); + if (savedId) { + const saved = detected.find((w) => w.id === savedId); + if (saved) connectWallet(saved).catch(() => localStorage.removeItem("wallet_id")); + } + }, 500); + }, [connectWallet]); + + const disconnect = useCallback(() => { + setApi(null); setAddress(null); setWalletName(null); + localStorage.removeItem("wallet_id"); + }, []); + + return { wallets, api, address, walletName, connect: connectWallet, disconnect }; +} + +// ── Formatters ─────────────────────────────────────────────────────────── + +const fmtUsd = (n: number) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +const fmtUsdShort = (n: number) => n.toLocaleString(undefined, { maximumFractionDigits: 0 }); + +// ── Components ─────────────────────────────────────────────────────────── + +function PriceDisplay({ price }: { price: any }) { + const prev = useRef(null); + const [dir, setDir] = useState<"up" | "down" | null>(null); + + useEffect(() => { + if (price && prev.current !== null && price.price !== prev.current) { + setDir(price.price > prev.current ? "up" : "down"); + const t = setTimeout(() => setDir(null), 400); + return () => clearTimeout(t); + } + if (price) prev.current = price.price; + }, [price?.price]); + + if (!price) return ( +
+
+

Connecting to Pyth Lazer...

+
+ ); + + return ( +
+

Bitcoin

+

+ ${fmtUsd(price.price)} +

+
+ + + + + Live via Pyth Lazer +
+
+ ); +} + +function StatusBar({ status }: { status: any }) { + const cfg: Record = { + idle: { bg: "bg-gray-900/80", dot: "bg-gray-500", text: "Idle" }, + creating: { bg: "bg-amber-950/40", dot: "bg-amber-400 animate-pulse", text: "Creating market..." }, + waiting: { bg: "bg-emerald-950/40", dot: "bg-emerald-400", text: "Market open" }, + resolving: { bg: "bg-blue-950/40", dot: "bg-blue-400 animate-pulse", text: "Resolving..." }, + error: { bg: "bg-rose-950/40", dot: "bg-rose-400", text: "Error" }, + }; + const c = cfg[status.botStatus] ?? cfg.idle!; + + return ( +
+
+ + {c.text} +
+ Cycle {status.cycleCount} +
+ ); +} + +function Countdown({ resolutionTime }: { resolutionTime: number }) { + const [remaining, setRemaining] = useState(0); + + useEffect(() => { + const tick = () => setRemaining(Math.max(0, resolutionTime - Date.now())); + tick(); + const iv = setInterval(tick, 100); + return () => clearInterval(iv); + }, [resolutionTime]); + + const totalSecs = Math.ceil(remaining / 1000); + const mins = Math.floor(totalSecs / 60); + const secs = totalSecs % 60; + const pct = Math.max(0, Math.min(100, (remaining / (5 * 60_000)) * 100)); + const urgent = totalSecs <= 30; + + return ( +
+
+ Closes in + + {mins}:{secs.toString().padStart(2, "0")} + +
+
+
+
+
+ ); +} + +function MarketCard({ market, price }: { market: any; price: any }) { + if (!market) return ( +
+
+

Waiting for next market...

+
+ ); + + const target = Number(BigInt(market.targetPrice)) * Math.pow(10, -8); + const yesR = BigInt(market.yesReserve); + const noR = BigInt(market.noReserve); + const total = yesR + noR; + const yesPct = total > 0n ? Number(noR * 100n / total) : 50; + const noPct = 100 - yesPct; + const totalAda = (Number(BigInt(market.totalAda)) / 1_000_000).toFixed(0); + const currentPrice = price?.price; + const above = currentPrice ? currentPrice > target : null; + + return ( +
+
+
+

Will BTC be above

+

${fmtUsd(target)}?

+
+
+

Pool

+

{totalAda} ADA

+
+
+ + + + {above !== null && ( +
+ {above ? "\u2191" : "\u2193"} + Currently {above ? "above" : "below"} — ${fmtUsd(currentPrice)} +
+ )} + +
+
+ YES {yesPct}% + {noPct}% NO +
+
+
+
+
+
+
+ ); +} + +function BetPanel({ market, walletApi }: { market: any; walletApi: CardanoWalletApi | null }) { + const [direction, setDirection] = useState<"yes" | "no">("yes"); + const [amount, setAmount] = useState("2"); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + + const canBet = walletApi && market && !market.resolved; + + const placeBet = async () => { + if (!walletApi || !canBet) return; + setLoading(true); + setResult(null); + try { + const utxos = await walletApi.getUtxos(undefined, undefined); + const changeAddress = await walletApi.getChangeAddress(); + if (!utxos || utxos.length === 0) throw new Error("No UTxOs in wallet"); + + const buildRes = await fetch("/api/build-bet", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ direction, amountAda: parseInt(amount), utxos, changeAddress }), + }); + const buildData = await buildRes.json(); + if (buildData.error) throw new Error(buildData.error); + + const witness = await walletApi.signTx(buildData.txCbor as CborHex, true); + + const finalizeRes = await fetch("/api/finalize", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ txCbor: buildData.txCbor, witness }), + }); + const finalizeData = await finalizeRes.json(); + if (finalizeData.error) throw new Error(finalizeData.error); + + const txId = await walletApi.submitTx(finalizeData.txCbor); + setResult(`+${buildData.tokensOut} ${direction.toUpperCase()} tokens (${txId.slice(0, 12)}...)`); + } catch (err: any) { + setResult(`Error: ${err.message}`); + } finally { + setLoading(false); + } + }; + + return ( +
+

Place bet

+ +
+ {(["yes", "no"] as const).map((d) => ( + + ))} +
+ +
+ setAmount(e.target.value)} + className="flex-1 bg-transparent text-xl font-semibold focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" + /> + ADA +
+ + + + {result && ( +

+ {result} +

+ )} +
+ ); +} + +function WalletButton({ wallets, address, walletName, onConnect, onDisconnect }: { + wallets: CardanoWallet[]; address: string | null; walletName: string | null; + onConnect: (w: CardanoWallet) => void; onDisconnect: () => void; +}) { + const [open, setOpen] = useState(false); + + if (address) { + return ( + + ); + } + + return ( +
+ + {open && ( +
+ {wallets.length > 0 ? wallets.map((w) => ( + + )) : ( +
No CIP-30 wallets detected
+ )} +
+ )} +
+ ); +} + +function MarketHistory({ history }: { history: any[] }) { + if (history.length === 0) return null; + + return ( +
+

History

+ {history.slice(0, 8).map((m, i) => { + const target = Number(BigInt(m.targetPrice)) * Math.pow(10, -8); + const won = m.winningSide === "yes"; + return ( +
+ BTC > ${fmtUsdShort(target)} + + {m.winningSide?.toUpperCase()} + +
+ ); + })} +
+ ); +} + +// ── App ────────────────────────────────────────────────────────────────── + +function App() { + const { price, market, history, status } = useMarketSocket(); + const { wallets, api, address, walletName, connect, disconnect } = useWallet(); + + return ( +
+ + +
+
+
+

Prediction Market

+

Pyth Lazer + Cardano

+
+ +
+ + + + + + +
+ Team Cliley — Pythathon 2026 +
+
+
+ ); +} + +createRoot(document.getElementById("root")!).render(); diff --git a/lazer/cardano/prediction-market/offchain/index.html b/lazer/cardano/prediction-market/offchain/index.html new file mode 100644 index 00000000..e8703144 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/index.html @@ -0,0 +1,29 @@ + + + + + + BTC Prediction Market + + + + +
+ + + diff --git a/lazer/cardano/prediction-market/offchain/lib/bifrost-global.d.ts b/lazer/cardano/prediction-market/offchain/lib/bifrost-global.d.ts new file mode 100644 index 00000000..13aa4667 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/lib/bifrost-global.d.ts @@ -0,0 +1,9 @@ +import { CardanoWallet } from "./bifrost-types"; + +declare global { + interface Window { + cardano: Record; + } +} + +export { }; diff --git a/lazer/cardano/prediction-market/offchain/lib/bifrost-types.ts b/lazer/cardano/prediction-market/offchain/lib/bifrost-types.ts new file mode 100644 index 00000000..f725b92c --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/lib/bifrost-types.ts @@ -0,0 +1,50 @@ +export type Paginate = { + page: number, + limit: number, +}; + +type Brand = Base & { __brand: Tag }; + +export type Value = Brand; +export type Transaction = Brand; +export type TransactionUnspentOutput = Brand; +export type TransactionWitnessSet = Brand; +export type CoseSign1 = Brand; +export type CoseKey = Brand; + +declare const CborHexBrand: unique symbol; +export type CborHex = string & { readonly [CborHexBrand]: T }; + +export type AddressHex = string; +export type AddressBech32 = string; +export type DataSignature = { + signature: CborHex, + key: CborHex, +}; + +export type CardanoWallet = { + id: string; + name: string; + apiVersion: string; + icon: string; + enable(): Promise; + isEnabled(): Promise; +}; + +export type CardanoWalletApi = { + getNetworkId: () => Promise; + getUsedAddresses: () => Promise>; + getUnusedAddresses: () => Promise>; + getChangeAddress: () => Promise; + getRewardAddresses: () => Promise>; + getBalance: () => Promise>; + getUtxos: (amount: CborHex | undefined, paginate: Paginate | undefined) => Promise>>; + signTx: (tx: CborHex, partialSign?: boolean) => Promise>; + signData: (address: AddressHex | AddressBech32, payload: string) => Promise; + submitTx: (txHex: string) => Promise; + on(eventName: string, callback: (...args: unknown[]) => unknown): void; + off(eventName: string, callback: (...args: unknown[]) => unknown): void; + experimental: { + getCollateral: () => Promise>>; + }; +}; diff --git a/lazer/cardano/prediction-market/offchain/lib/bifrost.ts b/lazer/cardano/prediction-market/offchain/lib/bifrost.ts new file mode 100644 index 00000000..38e06c60 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/lib/bifrost.ts @@ -0,0 +1,19 @@ +import { CardanoWallet } from "./bifrost-types.ts"; + +export function getWallets(): Array { + const result: Array = []; + const cardanoNamespace = window.cardano; + + if (!cardanoNamespace) return result; + + Object.entries(cardanoNamespace).forEach(([key, wallet]) => { + if (wallet?.name && wallet?.apiVersion) { + result.push({...wallet, id: key}); + } + }); + + return result; +} + +export type { CardanoWallet } from "./bifrost-types.ts"; +export type { CardanoWalletApi, CborHex, Transaction, TransactionWitnessSet, AddressHex } from "./bifrost-types.ts"; diff --git a/lazer/cardano/prediction-market/offchain/lib/cardano.ts b/lazer/cardano/prediction-market/offchain/lib/cardano.ts new file mode 100644 index 00000000..2fb95feb --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/lib/cardano.ts @@ -0,0 +1,139 @@ +import * as path from "path"; +import * as Core from "@blaze-cardano/core"; +import { HotWallet } from "@blaze-cardano/wallet"; +import { Blockfrost } from "@blaze-cardano/query"; +import { Blaze } from "@blaze-cardano/sdk"; +import { applyParams } from "@blaze-cardano/uplc"; +import { makeUplcEvaluator } from "@blaze-cardano/vm"; + +// ── Config ─────────────────────────────────────────────────────────────── + +export const BLOCKFROST_API_KEY = process.env.BLOCKFROST_API_KEY!; +export const NETWORK = process.env.NETWORK ?? "Preprod"; +export const WALLET_MNEMONIC = process.env.WALLET_MNEMONIC!; +export const PYTH_POLICY_ID = process.env.PYTH_POLICY_ID!; + +// ── Blueprint ──────────────────────────────────────────────────────────── + +const blueprintPath = path.resolve(import.meta.dir, "../../contracts/plutus.json"); +const blueprint = JSON.parse(await Bun.file(blueprintPath).text()); + +function getValidator(title: string): string { + const v = blueprint.validators.find((v: any) => v.title === title); + if (!v) throw new Error(`Validator not found: ${title}`); + return v.compiledCode; +} + +export const marketSpendCode = getValidator("market.market.spend"); + +// ── Datum helpers ──────────────────────────────────────────────────────── + +export function constr(idx: number, fields: Core.PlutusData[]): Core.PlutusData { + const list = new Core.PlutusList(); + for (const f of fields) list.add(f); + return Core.PlutusData.newConstrPlutusData( + new Core.ConstrPlutusData(BigInt(idx), list) + ); +} + +export function bytes(hex: string): Core.PlutusData { + return Core.PlutusData.newBytes(Buffer.from(hex, "hex")); +} + +export function integer(n: bigint): Core.PlutusData { + return Core.PlutusData.newInteger(n); +} + +export const FALSE = constr(0, []); +export const TRUE = constr(1, []); +export const NONE = constr(1, []); +export const MINT_TOKENS = constr(0, []); +export const BURN_TOKENS = constr(1, []); +export const RESOLVE = constr(1, []); + +export function mkMarketDatum(p: { + creator: string; + pythId: string; + feedId: number; + targetPrice: bigint; + resolutionTime: bigint; + tokenPolicy: string; + seed: bigint; +}): Core.PlutusData { + return constr(0, [ + bytes(p.creator), + bytes(p.pythId), + integer(BigInt(p.feedId)), + integer(p.targetPrice), + integer(p.resolutionTime), + bytes(p.tokenPolicy), + integer(p.seed), + integer(p.seed), + integer(p.seed * p.seed), + integer(p.seed), + integer(p.seed), + integer(p.seed), + FALSE, + NONE, + ]); +} + +// ── Blaze setup ────────────────────────────────────────────────────────── + +export async function setupBlaze() { + const networkId = + NETWORK === "Mainnet" ? Core.NetworkId.Mainnet : Core.NetworkId.Testnet; + const blazeNetwork = `cardano-${NETWORK.toLowerCase()}` as "cardano-preprod" | "cardano-preview" | "cardano-mainnet"; + const provider = new Blockfrost({ + network: blazeNetwork, + projectId: BLOCKFROST_API_KEY, + }); + + const entropy = Core.mnemonicToEntropy(WALLET_MNEMONIC, Core.wordlist); + const masterKey = Core.Bip32PrivateKey.fromBip39Entropy(Buffer.from(entropy), ""); + const wallet = await HotWallet.fromMasterkey(masterKey.hex(), provider, networkId); + const blaze = await Blaze.from(provider, wallet); + return { blaze, provider, wallet }; +} + +export function getSlotConfig() { + return Core.SLOT_CONFIG_NETWORK[NETWORK as keyof typeof Core.SLOT_CONFIG_NETWORK] + ?? Core.SLOT_CONFIG_NETWORK.Preprod; +} + +export async function makeEvaluator(provider: Blockfrost) { + const params = await provider.getParameters(); + return makeUplcEvaluator(params, 1.2, 1.2, getSlotConfig()); +} + +export async function submitTx(signed: Core.Transaction) { + const txCbor = signed.toCbor(); + const res = await fetch( + `https://cardano-${NETWORK.toLowerCase()}.blockfrost.io/api/v0/tx/submit`, + { + method: "POST", + headers: { project_id: BLOCKFROST_API_KEY, "Content-Type": "application/cbor" }, + body: Buffer.from(txCbor, "hex"), + } + ); + const body = await res.text(); + if (!res.ok) throw new Error(`Submit failed (${res.status}): ${body}`); + return JSON.parse(body) as string; +} + +// ── Script derivation ──────────────────────────────────────────────────── + +export function deriveScript(oneShotTxHash: string, oneShotIdx: number) { + const oneShotParam = constr(0, [ + constr(0, [bytes(oneShotTxHash), integer(BigInt(oneShotIdx))]), + ]); + const parameterizedCode = applyParams(Core.HexBlob(marketSpendCode), oneShotParam); + const marketScript = Core.Script.newPlutusV3Script( + new Core.PlutusV3Script(Core.HexBlob(parameterizedCode)) + ); + const policyId = Core.PolicyId(marketScript.hash().toString() as Core.Hash28ByteBase16); + const scriptAddr = Core.addressFromValidator(Core.NetworkId.Testnet, marketScript); + return { marketScript, policyId, scriptAddr }; +} + +export { Core, applyParams, Blaze, Blockfrost, HotWallet }; diff --git a/lazer/cardano/prediction-market/offchain/lib/market.ts b/lazer/cardano/prediction-market/offchain/lib/market.ts new file mode 100644 index 00000000..e0038fca --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/lib/market.ts @@ -0,0 +1,411 @@ +import { + Core, Blaze, Blockfrost, HotWallet, + constr, bytes, integer, mkMarketDatum, submitTx, makeEvaluator, deriveScript, getSlotConfig, + PYTH_POLICY_ID, NETWORK, MINT_TOKENS, BURN_TOKENS, RESOLVE, TRUE, FALSE, NONE, +} from "./cardano.ts"; +import { getPythUpdate, FEED_NAME_TO_ID } from "./pyth.ts"; +import type { MarketState } from "./types.ts"; + +// ── Derive + find market UTxO ──────────────────────────────────────────── + +export async function deriveMarket(provider: Blockfrost, policyIdHex: string, oneShotTxHash: string, oneShotIdx: number) { + const { marketScript, policyId, scriptAddr } = deriveScript(oneShotTxHash, oneShotIdx); + + const utxos = await provider.getUnspentOutputs(scriptAddr); + const stateThreadAssetId = Core.AssetId.fromParts(policyId, Core.AssetName("")); + const marketUtxo = utxos.find((u: Core.TransactionUnspentOutput) => { + const ma = u.output().amount().multiasset(); + if (!ma) return false; + return (ma.get(stateThreadAssetId) ?? 0n) === 1n; + }); + if (!marketUtxo) throw new Error("Market UTxO not found at script address"); + + const datumData = marketUtxo.output().datum()!.asInlineData()!; + const df = datumData.asConstrPlutusData()!.getData(); + + return { marketScript, policyId, scriptAddr, marketUtxo, df }; +} + +// ── Parse datum to MarketState ─────────────────────────────────────────── + +export function parseDatum(df: Core.PlutusList, policyId: string, oneShotTx: string, oneShotIdx: number): MarketState { + const resolvedField = df.get(12)!.asConstrPlutusData()!; + const isResolved = resolvedField.getAlternative() === 1n; + + let winningSide: "yes" | "no" | null = null; + if (isResolved) { + const wsField = df.get(13)!.asConstrPlutusData()!; + if (wsField.getAlternative() === 0n) { + const inner = wsField.getData().get(0)!.asConstrPlutusData()!; + winningSide = inner.getAlternative() === 0n ? "yes" : "no"; + } + } + + return { + policyId, + oneShotTx, + oneShotIdx, + creator: Buffer.from(df.get(0)!.asBoundedBytes()!).toString("hex"), + feedId: Number(df.get(2)!.asInteger()!), + targetPrice: df.get(3)!.asInteger()!, + resolutionTime: Number(df.get(4)!.asInteger()!), + yesReserve: df.get(6)!.asInteger()!, + noReserve: df.get(7)!.asInteger()!, + k: df.get(8)!.asInteger()!, + totalYesMinted: df.get(9)!.asInteger()!, + totalNoMinted: df.get(10)!.asInteger()!, + totalAda: df.get(11)!.asInteger()!, + resolved: isResolved, + winningSide, + }; +} + +// ── Create market ──────────────────────────────────────────────────────── + +export async function createMarket(opts: { + blaze: Blaze; + provider: Blockfrost; + wallet: HotWallet; + feedName: string; + seedAda: bigint; + resolutionMs: number; +}) { + const { blaze, provider, wallet, feedName, resolutionMs } = opts; + const seedLovelace = opts.seedAda * 1_000_000n; + + const update = await getPythUpdate(feedName); + console.log(`[market] Price: ${update.price} (feed ${update.feedId})`); + + const utxos = await provider.getUnspentOutputs(wallet.address); + const oneShot = utxos[0]!; + const oneShotRef = oneShot.input(); + + const { marketScript, policyId, scriptAddr } = deriveScript( + oneShotRef.transactionId().toString(), + Number(oneShotRef.index()) + ); + + const paymentHash = wallet.address.getProps().paymentPart!.hash; + const resolutionTime = BigInt(Date.now() + resolutionMs); + + const datum = mkMarketDatum({ + creator: paymentHash, + pythId: PYTH_POLICY_ID, + feedId: update.feedId, + targetPrice: update.price, + resolutionTime, + tokenPolicy: policyId, + seed: seedLovelace, + }); + + const stateThreadName = Core.AssetName(""); + const mintAssets = new Map(); + mintAssets.set(stateThreadName, 1n); + mintAssets.set(Core.AssetName("594553"), seedLovelace); + mintAssets.set(Core.AssetName("4e4f"), seedLovelace); + + const stateThreadAsset = Core.AssetId.fromParts(policyId, stateThreadName); + const lockValue = new Core.Value(seedLovelace); + lockValue.setMultiasset(new Map([[stateThreadAsset, 1n]])); + + const tx = await blaze + .newTransaction() + .addInput(oneShot) + .lockAssets(scriptAddr, lockValue, datum) + .addMint(policyId, mintAssets, MINT_TOKENS) + .provideScript(marketScript) + .complete(); + + const signed = await blaze.signTransaction(tx); + const txId = await submitTx(signed); + + return { + txId, + policyId, + oneShotTx: oneShotRef.transactionId().toString(), + oneShotIdx: Number(oneShotRef.index()), + targetPrice: update.price, + resolutionTime: Number(resolutionTime), + }; +} + +// ── Resolve market ─────────────────────────────────────────────────────── + +export async function resolveMarket(opts: { + blaze: Blaze; + provider: Blockfrost; + wallet: HotWallet; + policyId: string; + oneShotTx: string; + oneShotIdx: number; +}) { + const { blaze, provider, wallet, policyId: policyIdHex, oneShotTx, oneShotIdx } = opts; + + const { marketScript, policyId, scriptAddr, marketUtxo, df } = await deriveMarket( + provider, policyIdHex, oneShotTx, oneShotIdx + ); + + const feedId = Number(df.get(2)!.asInteger()!); + const targetPrice = df.get(3)!.asInteger()!; + const resolutionTime = Number(df.get(4)!.asInteger()!); + const totalAda = df.get(11)!.asInteger()!; + const inputLovelace = marketUtxo.output().amount().coin(); + + const feedName = Object.entries(FEED_NAME_TO_ID).find(([_, id]) => id === feedId)?.[0] ?? String(feedId); + const update = await getPythUpdate(feedName); + + const yesWins = update.price > targetPrice; + const winningSide = constr(0, [yesWins ? constr(0, []) : constr(1, [])]); + const resolvedDatum = constr(0, [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, + df.get(5)!, df.get(6)!, df.get(7)!, df.get(8)!, df.get(9)!, + df.get(10)!, df.get(11)!, + TRUE, winningSide, + ]); + + // Pyth State UTxO + const pythStateAddr = Core.addressFromBech32( + "addr_test1wrm3tr5zpw9k2nefjtsz66wfzn6flnphr5kd6ak9ufrl3wcqqfyn8" + ); + const pythUtxos = await provider.getUnspentOutputs(pythStateAddr); + const pythStateUtxo = pythUtxos.find((u) => { + const ma = u.output().amount().multiasset(); + if (!ma) return false; + for (const [assetId] of ma) { + if (assetId.includes(PYTH_POLICY_ID)) return true; + } + return false; + }); + if (!pythStateUtxo) throw new Error("Pyth State UTxO not found"); + + const pythRefScript = pythStateUtxo.output().scriptRef()!; + const pythWithdrawHash = pythRefScript.hash(); + + const updateBuf = Buffer.from(update.solanaHex, "hex"); + const pythRedeemerList = new Core.PlutusList(); + pythRedeemerList.add(Core.PlutusData.newBytes(updateBuf)); + const pythRedeemer = Core.PlutusData.newList(pythRedeemerList); + + const outputLovelace = inputLovelace > totalAda ? inputLovelace : totalAda; + const stateThreadAsset = Core.AssetId.fromParts(policyId, Core.AssetName("")); + const lockValue = new Core.Value(outputLovelace); + lockValue.setMultiasset(new Map([[stateThreadAsset, 1n]])); + + const pythRewardAddrObj = Core.RewardAddress.fromCredentials( + Core.NetworkId.Testnet, + { type: Core.CredentialType.ScriptHash, hash: pythWithdrawHash as Core.Hash28ByteBase16 } + ); + const pythRewardAccount = Core.RewardAccount(pythRewardAddrObj.toAddress().toBech32()); + + const slotConfig = getSlotConfig(); + const posixToSlotCeil = (posixMs: number) => + Math.ceil((posixMs - slotConfig.zeroTime) / slotConfig.slotLength) + slotConfig.zeroSlot; + + const now = Date.now() - 60_000; + const effectiveTime = Math.max(resolutionTime, now); + const validFrom = Core.Slot(posixToSlotCeil(effectiveTime)); + const validUntil = Core.Slot(validFrom + 300); + + const uplcEvaluator = await makeEvaluator(provider); + + const tx = await blaze + .newTransaction() + .useEvaluator(uplcEvaluator) + .addInput(marketUtxo, RESOLVE) + .addReferenceInput(pythStateUtxo) + .lockAssets(scriptAddr, lockValue, resolvedDatum) + .addWithdrawal(pythRewardAccount, 0n, pythRedeemer) + .addRequiredSigner(Core.Ed25519KeyHashHex(wallet.address.getProps().paymentPart!.hash)) + .setValidFrom(validFrom) + .setValidUntil(validUntil) + .provideScript(marketScript) + .complete(); + + const signed = await blaze.signTransaction(tx); + const txId = await submitTx(signed); + + return { txId, winner: (yesWins ? "yes" : "no") as "yes" | "no" }; +} + +// ── Build bet tx (unsigned — for wallet signing) ───────────────────────── + +export async function buildBetTx(opts: { + blaze: Blaze; + provider: Blockfrost; + policyId: string; + oneShotTx: string; + oneShotIdx: number; + direction: "yes" | "no"; + amountAda: bigint; +}) { + const { blaze, provider, policyId: policyIdHex, oneShotTx, oneShotIdx, direction, amountAda } = opts; + const amountLovelace = amountAda * 1_000_000n; + + const { marketScript, policyId, scriptAddr, marketUtxo, df } = await deriveMarket( + provider, policyIdHex, oneShotTx, oneShotIdx + ); + + const yesReserve = df.get(6)!.asInteger()!; + const noReserve = df.get(7)!.asInteger()!; + const k = df.get(8)!.asInteger()!; + const totalYesMinted = df.get(9)!.asInteger()!; + const totalNoMinted = df.get(10)!.asInteger()!; + const totalAda = df.get(11)!.asInteger()!; + const resolutionTime = Number(df.get(4)!.asInteger()!); + const inputLovelace = marketUtxo.output().amount().coin(); + + let tokensOut: bigint; + let newDatumFields: Core.PlutusData[]; + if (direction === "yes") { + tokensOut = yesReserve - k / (noReserve + amountLovelace); + if (tokensOut <= 0n) throw new Error("Bet too small"); + newDatumFields = [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, df.get(5)!, + integer(yesReserve - tokensOut), integer(noReserve + amountLovelace), + df.get(8)!, integer(totalYesMinted + tokensOut), df.get(10)!, + integer(totalAda + amountLovelace), df.get(12)!, df.get(13)!, + ]; + } else { + tokensOut = noReserve - k / (yesReserve + amountLovelace); + if (tokensOut <= 0n) throw new Error("Bet too small"); + newDatumFields = [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, df.get(5)!, + integer(yesReserve + amountLovelace), integer(noReserve - tokensOut), + df.get(8)!, df.get(9)!, integer(totalNoMinted + tokensOut), + integer(totalAda + amountLovelace), df.get(12)!, df.get(13)!, + ]; + } + + const newDatum = constr(0, newDatumFields); + const tokenName = direction === "yes" ? Core.AssetName("594553") : Core.AssetName("4e4f"); + const mintAssets = new Map(); + mintAssets.set(tokenName, tokensOut); + + const stateThreadAssetId = Core.AssetId.fromParts(policyId, Core.AssetName("")); + const lockValue = new Core.Value(inputLovelace + amountLovelace); + lockValue.setMultiasset(new Map([[stateThreadAssetId, 1n]])); + + const betRedeemer = constr(0, [ + direction === "yes" ? constr(0, []) : constr(1, []), + integer(amountLovelace), + ]); + + const slotConfig = getSlotConfig(); + const posixToSlotFloor = (posixMs: number) => + Math.floor((posixMs - slotConfig.zeroTime) / slotConfig.slotLength) + slotConfig.zeroSlot; + const validFrom = Core.Slot(posixToSlotFloor(Date.now() - 60_000)); + const validUntil = Core.Slot(posixToSlotFloor(resolutionTime)); + + const uplcEvaluator = await makeEvaluator(provider); + + const tx = await blaze + .newTransaction() + .useEvaluator(uplcEvaluator) + .addInput(marketUtxo, betRedeemer) + .lockAssets(scriptAddr, lockValue, newDatum) + .addMint(policyId, mintAssets, MINT_TOKENS) + .setValidFrom(validFrom) + .setValidUntil(validUntil) + .provideScript(marketScript) + .complete(); + + return { tx, tokensOut }; +} + +// ── Build bet tx for browser wallet (user's UTxOs + change address) ────── + +export async function buildBetTxForWallet(opts: { + provider: Blockfrost; + policyId: string; + oneShotTx: string; + oneShotIdx: number; + direction: "yes" | "no"; + amountAda: bigint; + walletUtxos: string[]; // CIP-30 CBOR hex UTxOs + changeAddress: string; // CIP-30 hex address +}) { + const { provider, policyId: policyIdHex, oneShotTx, oneShotIdx, direction, amountAda } = opts; + const amountLovelace = amountAda * 1_000_000n; + + const { marketScript, policyId, scriptAddr, marketUtxo, df } = await deriveMarket( + provider, policyIdHex, oneShotTx, oneShotIdx + ); + + const yesReserve = df.get(6)!.asInteger()!; + const noReserve = df.get(7)!.asInteger()!; + const k = df.get(8)!.asInteger()!; + const totalYesMinted = df.get(9)!.asInteger()!; + const totalNoMinted = df.get(10)!.asInteger()!; + const totalAda = df.get(11)!.asInteger()!; + const resolutionTime = Number(df.get(4)!.asInteger()!); + const inputLovelace = marketUtxo.output().amount().coin(); + + let tokensOut: bigint; + let newDatumFields: Core.PlutusData[]; + if (direction === "yes") { + tokensOut = yesReserve - k / (noReserve + amountLovelace); + if (tokensOut <= 0n) throw new Error("Bet too small"); + newDatumFields = [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, df.get(5)!, + integer(yesReserve - tokensOut), integer(noReserve + amountLovelace), + df.get(8)!, integer(totalYesMinted + tokensOut), df.get(10)!, + integer(totalAda + amountLovelace), df.get(12)!, df.get(13)!, + ]; + } else { + tokensOut = noReserve - k / (yesReserve + amountLovelace); + if (tokensOut <= 0n) throw new Error("Bet too small"); + newDatumFields = [ + df.get(0)!, df.get(1)!, df.get(2)!, df.get(3)!, df.get(4)!, df.get(5)!, + integer(yesReserve + amountLovelace), integer(noReserve - tokensOut), + df.get(8)!, df.get(9)!, integer(totalNoMinted + tokensOut), + integer(totalAda + amountLovelace), df.get(12)!, df.get(13)!, + ]; + } + + const newDatum = constr(0, newDatumFields); + const tokenName = direction === "yes" ? Core.AssetName("594553") : Core.AssetName("4e4f"); + const mintAssets = new Map(); + mintAssets.set(tokenName, tokensOut); + + const stateThreadAssetId = Core.AssetId.fromParts(policyId, Core.AssetName("")); + const lockValue = new Core.Value(inputLovelace + amountLovelace); + lockValue.setMultiasset(new Map([[stateThreadAssetId, 1n]])); + + const betRedeemer = constr(0, [ + direction === "yes" ? constr(0, []) : constr(1, []), + integer(amountLovelace), + ]); + + const slotConfig = getSlotConfig(); + const posixToSlotFloor = (posixMs: number) => + Math.floor((posixMs - slotConfig.zeroTime) / slotConfig.slotLength) + slotConfig.zeroSlot; + const validFrom = Core.Slot(posixToSlotFloor(Date.now() - 60_000)); + const validUntil = Core.Slot(posixToSlotFloor(resolutionTime)); + + // Parse user's CIP-30 UTxOs and change address + const userUtxos = opts.walletUtxos.map((hex) => + Core.TransactionUnspentOutput.fromCbor(Core.TxCBOR(hex)) + ); + const changeAddr = Core.Address.fromBytes(Core.HexBlob(opts.changeAddress)); + + const uplcEvaluator = await makeEvaluator(provider); + const params = await provider.getParameters(); + + // Build tx with user's UTxOs for coin selection + const { TxBuilder } = await import("@blaze-cardano/tx"); + const txBuilder = new TxBuilder(params); + txBuilder + .setNetworkId(Core.NetworkId.Testnet) + .useEvaluator(uplcEvaluator) + .setChangeAddress(changeAddr) + .addInput(marketUtxo, betRedeemer) + .lockAssets(scriptAddr, lockValue, newDatum) + .addMint(policyId, mintAssets, MINT_TOKENS) + .setValidFrom(validFrom) + .setValidUntil(validUntil) + .provideScript(marketScript) + .addUnspentOutputs(userUtxos); + + const tx = await txBuilder.complete(); + return { tx, tokensOut }; +} diff --git a/lazer/cardano/prediction-market/offchain/lib/pyth.ts b/lazer/cardano/prediction-market/offchain/lib/pyth.ts new file mode 100644 index 00000000..2e9abce3 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/lib/pyth.ts @@ -0,0 +1,77 @@ +import { PythLazerClient } from "@pythnetwork/pyth-lazer-sdk"; +import type { PythUpdate, PriceSnapshot } from "./types.ts"; + +const PYTH_API_KEY = process.env.PYTH_API_KEY!; + +export const FEED_NAME_TO_ID: Record = { + "BTC/USD": 1, + "ETH/USD": 2, + "ADA/USD": 16, +}; + +export async function getPythUpdate(feedName: string): Promise { + const feedId = FEED_NAME_TO_ID[feedName] ?? parseInt(feedName); + const res = await fetch("https://pyth-lazer.dourolabs.app/v1/latest_price", { + method: "POST", + headers: { + Authorization: `Bearer ${PYTH_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + channel: "fixed_rate@200ms", + formats: ["solana"], + priceFeedIds: [feedId], + properties: ["price", "exponent"], + jsonBinaryEncoding: "hex", + parsed: true, + }), + }); + const data = (await res.json()) as any; + return { + solanaHex: data.solana?.data as string, + price: BigInt(data.parsed?.priceFeeds?.[0]?.price ?? "0"), + exponent: data.parsed?.priceFeeds?.[0]?.exponent ?? -8, + feedId, + }; +} + +export async function createPythStream( + feedIds: number[], + onPrice: (feedId: number, snapshot: PriceSnapshot) => void +): Promise { + const client = await PythLazerClient.create({ + token: PYTH_API_KEY, + webSocketPoolConfig: { + numConnections: 1, + }, + }); + + client.subscribe({ + type: "subscribe", + subscriptionId: 1, + priceFeedIds: feedIds, + properties: ["price", "exponent"], + formats: ["solana"], + channel: "fixed_rate@200ms", + parsed: true, + jsonBinaryEncoding: "hex", + }); + + client.addMessageListener((msg: any) => { + if (msg.type === "json" && msg.value?.parsed) { + for (const feed of msg.value.parsed.priceFeeds ?? []) { + if (feed.price !== undefined && feed.exponent !== undefined) { + const rawPrice = BigInt(feed.price); + onPrice(feed.priceFeedId, { + price: Number(rawPrice) * Math.pow(10, feed.exponent), + rawPrice, + exponent: feed.exponent, + timestamp: Date.now(), + }); + } + } + } + }); + + return client; +} diff --git a/lazer/cardano/prediction-market/offchain/lib/types.ts b/lazer/cardano/prediction-market/offchain/lib/types.ts new file mode 100644 index 00000000..63fbbedb --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/lib/types.ts @@ -0,0 +1,60 @@ +export interface MarketState { + policyId: string; + oneShotTx: string; + oneShotIdx: number; + creator: string; + feedId: number; + targetPrice: bigint; + resolutionTime: number; + yesReserve: bigint; + noReserve: bigint; + k: bigint; + totalYesMinted: bigint; + totalNoMinted: bigint; + totalAda: bigint; + resolved: boolean; + winningSide: "yes" | "no" | null; +} + +export interface PythUpdate { + solanaHex: string; + price: bigint; + exponent: number; + feedId: number; +} + +export interface PriceSnapshot { + price: number; + rawPrice: bigint; + exponent: number; + timestamp: number; +} + +export interface BotState { + currentMarket: MarketState | null; + marketHistory: MarketState[]; + latestPrice: PriceSnapshot | null; + botStatus: "idle" | "creating" | "waiting" | "resolving" | "error"; + cycleCount: number; + lastError: string | null; +} + +export type WsMessage = + | { type: "price"; data: any } + | { type: "market"; data: any } + | { type: "status"; data: any } + | { type: "history"; data: any }; + +export function serializeMarket(m: MarketState | null) { + if (!m) return null; + return { + ...m, + targetPrice: m.targetPrice.toString(), + yesReserve: m.yesReserve.toString(), + noReserve: m.noReserve.toString(), + k: m.k.toString(), + totalYesMinted: m.totalYesMinted.toString(), + totalNoMinted: m.totalNoMinted.toString(), + totalAda: m.totalAda.toString(), + }; +} diff --git a/lazer/cardano/prediction-market/offchain/package.json b/lazer/cardano/prediction-market/offchain/package.json index 9bd653d1..43dc2373 100644 --- a/lazer/cardano/prediction-market/offchain/package.json +++ b/lazer/cardano/prediction-market/offchain/package.json @@ -1,10 +1,17 @@ { - "name": "offchain", + "name": "prediction-market-offchain", "module": "index.ts", "type": "module", "private": true, + "scripts": { + "cli": "bun run cli.ts", + "bot": "bun run bot.ts", + "dev": "bun --hot server.ts" + }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3" }, "peerDependencies": { "typescript": "^5" @@ -13,6 +20,9 @@ "@blaze-cardano/data": "^0.6.6", "@blaze-cardano/sdk": "^0.2.48", "@blaze-cardano/uplc": "^0.4.3", - "@pythnetwork/pyth-lazer-sdk": "^6.2.1" + "@blaze-cardano/vm": "^0.2.3", + "@pythnetwork/pyth-lazer-sdk": "^6.2.1", + "react": "^19.2.4", + "react-dom": "^19.2.4" } } diff --git a/lazer/cardano/prediction-market/offchain/server.ts b/lazer/cardano/prediction-market/offchain/server.ts new file mode 100644 index 00000000..45a91190 --- /dev/null +++ b/lazer/cardano/prediction-market/offchain/server.ts @@ -0,0 +1,182 @@ +import index from "./index.html"; +import { startBot, botState } from "./bot.ts"; +import { createPythStream, FEED_NAME_TO_ID } from "./lib/pyth.ts"; +import { setupBlaze, Core } from "./lib/cardano.ts"; +import { buildBetTxForWallet } from "./lib/market.ts"; +import { serializeMarket } from "./lib/types.ts"; +import type { PriceSnapshot } from "./lib/types.ts"; + +// ── Pyth Streaming ────────────────────────────────────────────────────── + +async function initPythStream() { + try { + await createPythStream( + [FEED_NAME_TO_ID["BTC/USD"]], + (_feedId, snapshot) => { + botState.latestPrice = snapshot; + server.publish("prices", JSON.stringify({ + type: "price", + data: { ...snapshot, rawPrice: snapshot.rawPrice.toString() }, + })); + } + ); + console.log("[pyth] Streaming connected"); + } catch (err: any) { + console.error("[pyth] Stream init failed:", err.message); + } +} + +// ── Server ────────────────────────────────────────────────────────────── + +const server = Bun.serve({ + port: 4000, + + routes: { + "/": index, + + "/api/market": () => Response.json(serializeMarket(botState.currentMarket)), + + "/api/price": () => { + if (!botState.latestPrice) return Response.json(null); + return Response.json({ + ...botState.latestPrice, + rawPrice: botState.latestPrice.rawPrice.toString(), + }); + }, + + "/api/history": () => Response.json(botState.marketHistory.map(serializeMarket)), + + "/api/status": () => Response.json({ + botStatus: botState.botStatus, + cycleCount: botState.cycleCount, + lastError: botState.lastError, + }), + + "/api/hex-to-bech32": (req: Request) => { + const hex = new URL(req.url).searchParams.get("hex"); + if (!hex) return Response.json({ error: "missing hex" }, { status: 400 }); + try { + const addr = Core.Address.fromBytes(Core.HexBlob(hex)); + return Response.json({ bech32: addr.toBech32() }); + } catch (e: any) { + return Response.json({ error: e.message }, { status: 400 }); + } + }, + + "/api/build-bet": { + async POST(req) { + try { + const body = await req.json() as { + direction: "yes" | "no"; + amountAda: number; + utxos: string[]; // CIP-30 CBOR hex UTxOs from wallet + changeAddress: string; // CIP-30 hex address + }; + if (!botState.currentMarket || botState.currentMarket.resolved) { + return Response.json({ error: "No active market" }, { status: 400 }); + } + const m = botState.currentMarket; + const { provider } = await setupBlaze(); + const { tx, tokensOut } = await buildBetTxForWallet({ + provider, + policyId: m.policyId, + oneShotTx: m.oneShotTx, + oneShotIdx: m.oneShotIdx, + direction: body.direction, + amountAda: BigInt(body.amountAda), + walletUtxos: body.utxos, + changeAddress: body.changeAddress, + }); + return Response.json({ + txCbor: tx.toCbor(), + tokensOut: tokensOut.toString(), + }); + } catch (err: any) { + return Response.json({ error: err.message }, { status: 500 }); + } + }, + }, + + "/api/finalize": { + async POST(req) { + try { + const body = await req.json() as { txCbor: string; witness: string }; + const tx = Core.Transaction.fromCbor(Core.TxCBOR(body.txCbor)); + const signed = Core.TransactionWitnessSet.fromCbor(Core.HexBlob(body.witness)); + // Merge wallet vkeys into tx — same pattern as blaze signTransaction + const ws = tx.witnessSet(); + const existing = ws.vkeys()?.toCore() ?? []; + const walletKeys = signed.vkeys(); + if (walletKeys) { + ws.setVkeys( + Core.CborSet.fromCore( + [...walletKeys.toCore(), ...existing], + Core.VkeyWitness.fromCore, + ), + ); + } + tx.setWitnessSet(ws); + return Response.json({ txCbor: tx.toCbor() }); + } catch (err: any) { + return Response.json({ error: err.message }, { status: 500 }); + } + }, + }, + }, + + fetch(req, server) { + if (new URL(req.url).pathname === "/ws") { + if (server.upgrade(req)) return undefined; + return new Response("WebSocket upgrade failed", { status: 500 }); + } + return new Response("Not found", { status: 404 }); + }, + + websocket: { + open(ws) { + ws.subscribe("prices"); + ws.subscribe("market"); + // Send initial state + ws.send(JSON.stringify({ type: "market", data: serializeMarket(botState.currentMarket) })); + ws.send(JSON.stringify({ type: "history", data: botState.marketHistory.map(serializeMarket) })); + ws.send(JSON.stringify({ type: "status", data: { + botStatus: botState.botStatus, + cycleCount: botState.cycleCount, + lastError: botState.lastError, + }})); + if (botState.latestPrice) { + ws.send(JSON.stringify({ + type: "price", + data: { ...botState.latestPrice, rawPrice: botState.latestPrice.rawPrice.toString() }, + })); + } + }, + message() {}, + close(ws) { + ws.unsubscribe("prices"); + ws.unsubscribe("market"); + }, + }, +}); + +console.log(`Server running at ${server.url}`); + +// ── Start services ────────────────────────────────────────────────────── + +initPythStream(); + +startBot(() => { + server.publish("market", JSON.stringify({ + type: "market", data: serializeMarket(botState.currentMarket), + })); + server.publish("market", JSON.stringify({ + type: "status", data: { + botStatus: botState.botStatus, + cycleCount: botState.cycleCount, + lastError: botState.lastError, + }, + })); + server.publish("market", JSON.stringify({ + type: "history", data: botState.marketHistory.map(serializeMarket), + })); +});