Skip to content

Commit cf98420

Browse files
committed
feat: simple polymarket example
1 parent a32aec2 commit cf98420

3 files changed

Lines changed: 131 additions & 114 deletions

File tree

scripts/post-dr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async function main() {
1616
method: 'none'
1717
},
1818
execProgramId: process.env.ORACLE_PROGRAM_ID,
19-
execInputs: Buffer.from('KXSB-26-BUF,540209'),
19+
execInputs: Buffer.from('27824'),
2020
tallyInputs: Buffer.from([]),
2121
memo: Buffer.from(new Date().toISOString()),
2222
};

src/execution_phase.rs

Lines changed: 65 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -6,129 +6,109 @@ use serde::{Deserialize, Serialize};
66
// DATA STRUCTURES
77
// ============================================================================
88

9+
#[derive(Serialize, Deserialize)]
10+
struct PolyMarketEvent {
11+
closed: bool,
12+
markets: Vec<PolyMarketMarket>,
13+
}
14+
915
#[derive(Serialize, Deserialize)]
1016
struct PolyMarketMarket {
1117
#[serde(rename = "outcomePrices")]
1218
outcome_prices: String, // PolyMarket returns this as a JSON string, not an array
13-
volume: String,
1419
}
1520

16-
#[derive(Serialize, Deserialize)]
17-
struct KalshiMarket {
18-
yes_bid_dollars: String,
19-
volume: u64,
20-
}
2121

2222
#[derive(Serialize, Deserialize)]
23-
struct KalshiMarketResponse {
24-
market: KalshiMarket,
23+
struct Response {
24+
prices: Vec<f64>,
25+
market_status: String,
2526
}
2627

2728
// ============================================================================
28-
// EXECUTION PHASE - FETCHES LIVE DATA FROM KALSHI & POLYMARKET - TAKES VOLUME WEIGHTED PRICE
29+
// EXECUTION PHASE - FETCHES LIVE DATA FROM POLYMARKET ONLY
2930
// ============================================================================
3031

3132
/**
3233
* Executes the data request phase within the SEDA network.
33-
* This phase fetches price data from both Kalshi and PolyMarket for the same market,
34-
* then calculates a volume-weighted average price between the two platforms.
35-
* Currently works with binary prediction markets and focuses on the "yes" outcome price.
36-
* Returns the volume-weighted average price as the final result.
34+
* This phase fetches event data from PolyMarket for a specific event.
35+
* Takes a single input: event_id
36+
* Loops through all markets in the event and extracts the first outcome price from each market.
37+
* Returns a JSON response with a vector of first outcome prices and whether the event is closed.
3738
*/
3839
pub fn execution_phase() -> Result<()> {
3940
// Retrieve the input parameters for the data request (DR).
40-
// Expected to be a market identifier that works for both Kalshi and PolyMarket APIs.
41+
// Expected to be a single event_id
4142
let dr_inputs_raw = String::from_utf8(Process::get_inputs())?;
42-
let dr_inputs_trimmed = dr_inputs_raw.trim();
43-
44-
let market_tickers: Vec<&str> = dr_inputs_trimmed.split(',').collect();
43+
let event_id = dr_inputs_raw.trim();
4544

46-
log!("Fetching market data from Kalshi and PolyMarket for market: {} and {}", market_tickers[0], market_tickers[1]);
45+
log!("Fetching event data from PolyMarket for event: {}", event_id);
4746

48-
// Step 1: Fetch Kalshi market data (yes bid price and volume)
49-
let kalshi_market_response = http_fetch(
50-
format!("https://api.elections.kalshi.com/trade-api/v2/markets/{}", market_tickers[0]),
47+
// Fetch PolyMarket event data
48+
let polymarket_event_response = http_fetch(
49+
format!("https://gamma-api.polymarket.com/events/{}", event_id),
5150
None,
5251
);
5352

54-
55-
// Check if the market request was successful
56-
if !kalshi_market_response.is_ok() {
53+
// Check if the event request was successful
54+
if !polymarket_event_response.is_ok() {
5755
elog!(
58-
"market HTTP Response was rejected: {} - {}",
59-
kalshi_market_response.status,
60-
String::from_utf8(kalshi_market_response.bytes)?
56+
"PolyMarket HTTP Response was rejected: {} - {}",
57+
polymarket_event_response.status,
58+
String::from_utf8(polymarket_event_response.bytes)?
6159
);
62-
Process::error("Error while fetching market information".as_bytes());
60+
Process::error("Error while fetching PolyMarket event information".as_bytes());
6361
return Ok(());
6462
}
6563

64+
// Parse event information
65+
let poly_market_event_data = serde_json::from_slice::<PolyMarketEvent>(&polymarket_event_response.bytes)?;
6666

67-
// Parse market informationmarket_response
68-
let kalshi_market_data = serde_json::from_slice::<KalshiMarketResponse>(&kalshi_market_response.bytes)?;
69-
log!(
70-
"Fetched Kalshi Price (YES BID): {} cents with volume {}",
71-
kalshi_market_data.market.yes_bid_dollars,
72-
kalshi_market_data.market.volume
73-
);
74-
75-
let kalshi_yes_bid_dollars = kalshi_market_data.market.yes_bid_dollars.parse::<f64>()?;
76-
77-
78-
// Step 2: Fetch PolyMarket market data (yes outcome price and volume)
79-
let polymarket_market_response = http_fetch(
80-
format!("https://gamma-api.polymarket.com/markets/{}", market_tickers[1]),
81-
None,
82-
);
83-
84-
85-
// Check if the market request was successful
86-
if !polymarket_market_response.is_ok() {
87-
elog!(
88-
"market HTTP Response was rejected: {} - {}",
89-
polymarket_market_response.status,
90-
String::from_utf8(polymarket_market_response.bytes)?
91-
);
92-
Process::error("Error while fetching market information".as_bytes());
67+
// Validate that the event has at least one market
68+
if poly_market_event_data.markets.is_empty() {
69+
elog!("Event has no markets available");
70+
Process::error("Error: Event has no markets".as_bytes());
9371
return Ok(());
9472
}
9573

96-
// Parse market informationmarket_response
97-
let poly_market_market_data = serde_json::from_slice::<PolyMarketMarket>(&polymarket_market_response.bytes)?;
98-
let outcome_prices_array: Vec<String> = serde_json::from_str(&poly_market_market_data.outcome_prices)?;
99-
let polymarket_yes_price = outcome_prices_array[0].parse::<f64>()?; // 0 = yes price
100-
101-
log!(
102-
"Fetched Price (YES PRICE): {} cents with volume {}",
103-
polymarket_yes_price,
104-
poly_market_market_data.volume
105-
);
106-
107-
108-
let poly_market_volume = poly_market_market_data.volume.parse::<f64>()?;
109-
110-
// Step 3: Calculate volume-weighted average price between Kalshi and PolyMarket
111-
let total_volume = kalshi_market_data.market.volume as f64 + poly_market_volume;
74+
// Loop through all markets and extract the first outcome price from each
75+
let mut yes_prices: Vec<f64> = Vec::new();
11276

113-
if total_volume == 0.0 {
114-
elog!("Total volume is zero, cannot calculate volume weighted average price");
115-
Process::error("Error: Total volume is zero".as_bytes());
116-
return Ok(());
77+
for (i, market) in poly_market_event_data.markets.iter().enumerate() {
78+
// Parse the outcome_prices JSON string into a vector
79+
let outcome_prices_array: Vec<String> = serde_json::from_str(&market.outcome_prices)?;
80+
81+
if outcome_prices_array.is_empty() {
82+
elog!("Market {} has no outcome prices", i);
83+
continue;
84+
}
85+
86+
// Get the first outcome price and parse it as f64
87+
let yes_price = outcome_prices_array[0].parse::<f64>().map_err(|e| {
88+
elog!("Failed to parse first outcome price for market {}: {}", i, e);
89+
anyhow::anyhow!("Invalid price format")
90+
})?;
91+
92+
yes_prices.push(yes_price);
93+
log!("Market {}: First outcome price = {}", i, yes_price);
11794
}
11895

119-
// Calculate weighted prices: price × volume for each platform
120-
let kalshi_weighted_price = kalshi_yes_bid_dollars * (kalshi_market_data.market.volume as f64);
121-
let polymarket_weighted_price = polymarket_yes_price * poly_market_volume;
122-
123-
// Final volume-weighted average: (sum of weighted prices) / (total volume)
124-
let volume_weighted_average_price = (kalshi_weighted_price + polymarket_weighted_price) / total_volume;
125-
12696
log!(
127-
"Volume Weighted Average Price: {:.8} cents",
128-
volume_weighted_average_price
97+
"Collected {} first outcome prices from all markets",
98+
yes_prices.len()
12999
);
130100

131-
// Return the volume-weighted average price as the execution result
132-
Process::success(volume_weighted_average_price.to_string().as_bytes());
101+
let market_status = if poly_market_event_data.closed {
102+
"closed".to_string()
103+
} else {
104+
"open".to_string()
105+
};
106+
107+
108+
// Return the PolyMarket first outcome prices and event status as JSON
109+
Process::success(&serde_json::to_vec(&Response {
110+
prices: yes_prices,
111+
market_status: market_status,
112+
})?);
133113
Ok(())
134114
}

src/tally_phase.rs

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
use anyhow::Result;
22
use seda_sdk_rs::{elog, get_reveals, log, Process};
3+
use serde::{Deserialize, Serialize};
4+
5+
6+
#[derive(Serialize, Deserialize)]
7+
struct Response {
8+
prices: Vec<f64>,
9+
market_status: String,
10+
}
311

412
/**
513
* Executes the tally phase within the SEDA network.
614
* This phase aggregates the results (e.g., price data) revealed during the execution phase,
7-
* calculates the median value, and submits it as the final result.
15+
* calculates the median value for each index position across all reveals, and submits the final result.
816
* Note: The number of reveals depends on the replication factor set in the data request parameters.
917
*/
1018
pub fn tally_phase() -> Result<()> {
@@ -13,43 +21,72 @@ pub fn tally_phase() -> Result<()> {
1321

1422
// Retrieve consensus reveals from the tally phase.
1523
let reveals = get_reveals()?;
16-
let mut prices: Vec<f64> = Vec::new();
24+
let mut market_status: Option<String> = None;
25+
let mut all_price_vectors: Vec<Vec<f64>> = Vec::new();
1726

18-
// Iterate over each reveal, parse its content as a floating-point number (f64), and store it in the prices array.
27+
// Iterate over each reveal and collect price vectors
1928
for reveal in reveals {
20-
let price_str = match String::from_utf8(reveal.body.reveal) {
21-
Ok(value) => value,
22-
Err(_err) => {
23-
// We should always handle a reveal body with care and not exit/panic when parsing went wrong
24-
// It's better to skip that reveal
25-
elog!("Reveal body could not be converted to string");
26-
continue;
27-
}
28-
};
29-
30-
let price = match price_str.trim().parse::<f64>() {
31-
Ok(value) => value,
32-
Err(_err) => {
33-
elog!("Reveal body could not be parsed as f64: {}", price_str);
34-
continue;
35-
}
36-
};
37-
38-
log!("Received price: {}", price);
39-
prices.push(price);
29+
let response = serde_json::from_slice::<Response>(&reveal.body.reveal)?;
30+
31+
// Check market_status consensus
32+
if market_status.is_none() {
33+
market_status = Some(response.market_status);
34+
} else if market_status.as_ref().unwrap() != &response.market_status {
35+
elog!("Market status is inconsistent between reveals");
36+
Process::error("Market status is inconsistent between reveals".as_bytes());
37+
return Ok(());
38+
}
39+
40+
// Collect price vectors for median calculation
41+
log!("Received prices: {:?}", response.prices);
42+
all_price_vectors.push(response.prices);
4043
}
4144

42-
if prices.is_empty() {
45+
if all_price_vectors.is_empty() {
4346
// If no valid prices were revealed, report an error indicating no consensus.
4447
Process::error("No consensus among revealed results".as_bytes());
4548
return Ok(());
4649
}
4750

48-
// If there are valid prices revealed, calculate the median price from price reports.
49-
let final_price = median(prices);
51+
// Find the maximum vector length to determine how many indices we need to process
52+
let max_length = all_price_vectors.iter().map(|v| v.len()).max().unwrap_or(0);
53+
54+
if max_length == 0 {
55+
elog!("All price vectors are empty");
56+
Process::error("All price vectors are empty".as_bytes());
57+
return Ok(());
58+
}
59+
60+
// Calculate median for each index position
61+
let mut median_prices: Vec<f64> = Vec::new();
62+
63+
for index in 0..max_length {
64+
let mut values_at_index: Vec<f64> = Vec::new();
65+
66+
// Collect all values at this index from all reveals
67+
for price_vector in &all_price_vectors {
68+
if index < price_vector.len() {
69+
values_at_index.push(price_vector[index]);
70+
}
71+
}
72+
73+
if values_at_index.is_empty() {
74+
log!("No values found at index {}, skipping", index);
75+
continue;
76+
}
77+
78+
let median_value = median(values_at_index);
79+
median_prices.push(median_value);
80+
log!("Index {}: Median = {}", index, median_value);
81+
}
82+
83+
log!("Final median prices: {:?}", median_prices);
5084

51-
// Report the successful result in the tally phase, encoding the result as bytes.
52-
Process::success(&final_price.to_string().as_bytes());
85+
// Return the median result
86+
Process::success(&serde_json::to_vec(&Response {
87+
prices: median_prices,
88+
market_status: market_status.unwrap(),
89+
})?);
5390

5491
Ok(())
5592
}

0 commit comments

Comments
 (0)