Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
default-members = ["xtask"]
members = [
"examples/blocksize-bidask",
"examples/jup-price-feed",
"examples/blocksize-vwap",
"examples/caplight-eod-market-price",
"examples/single-commodity-price",
Expand Down
22 changes: 22 additions & 0 deletions examples/jup-price-feed/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "jup-price-feed"
version = "0.1.0"
edition = "2024"
# rust-version = ""

[features]
default = ["env-testnet"]
seda-hide = ["seda-sdk-rs/hide-panic-paths"]
env-testnet = ["testnet", "seda-hide"]
env-mainnet = ["mainnet", "seda-hide"]
test = ["testnet"]

testnet = []
mainnet = []

[dependencies]
serde = { version = "1.0" }
anyhow = "1.0"
serde_json = { version = "1.0"}
seda-sdk-rs = { version = "1.1", default-features = false }
ethabi = "18.0"
64 changes: 64 additions & 0 deletions examples/jup-price-feed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Jupiter Price Feed

Deployments:
- [Testnet](https://testnet.explorer.seda.xyz/oracle-programs/6115dbd31500fda073eaf1f8d3a7abff5f2e1e9f434982e7a466e7789dfd3406)
- [Mainnet](https://explorer.seda.xyz/oracle-programs/f7076891e558ff8fc14bfaf4de16f015fd780d279bf2ed2e0f4e915c169c6850)
Comment on lines +4 to +5

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of these urls match the OP ID in the example command: 739a91df635cca0bad6fe510ba46737ade9eed81f9b431198889cfd687abc03d

we should comment out the mainnet one if it's not deployed there :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. is deployed to mainnet
  2. it does match the mainnet one for the example command?


## Overview

This Oracle Program gets the price of a specified Solana token in USD by leveraging the Jupiter Lite API and returns the raw price as a string. This program handles one token at a time.

You can test this Oracle Program on testnet with the following command:

```sh
cargo post-dr jup-price-feed -n seda So11111111111111111111111111111111111111112 -i f7076891e558ff8fc14bfaf4de16f015fd780d279bf2ed2e0f4e915c169c6850
```

## Execution Phase:

### Input format

The Execution Phase expects a single Solana token contract address i.e. `So11111111111111111111111111111111111111112` (SOL).

### Process

1. Validates the Data Request execution argument is not empty.
1. Makes a HTTP call to the Jupiter Lite API.
1. Extracts the `usdPrice` field from the response for the specified token.
1. Returns the raw price as a string.

### Example

Input: `"So11111111111111111111111111111111111111112"`

Output: `"245.67"` (raw USD price as string)

## Tally Phase

### Input

No additional input is required for this Oracle Program as the Tally Phase only uses the reveals from the Execution Phase.

### Process

1. Collects all price reveals from oracle nodes.
1. Parses each reveal as a raw f64 price value.
1. Calculates the median price from all the collected prices.
1. Returns the final median price as a string.

### Output Format

The result is returned as a string representation of the median price.

### Example

If execution phase ran with a replication factor of 3 and the prices were:
- 245.50
- 245.67
- 245.80

The tally phase would return `"245.67"` as the median price.

## Supported Data

Any token supported by the Jupiter Lite API that has a valid Solana contract address.
63 changes: 63 additions & 0 deletions examples/jup-price-feed/src/execution_phase.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use anyhow::Result;
#[cfg(any(feature = "testnet", feature = "mainnet"))]
use seda_sdk_rs::{Process, elog, http_fetch, log};

// Response:
// {
// "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v":{
// "usdPrice":0.996,
// "blockId":365626874,
// "decimals":9,
// "priceChange24h":0.03667753493441
// }
// }

pub fn execution_phase() -> Result<()> {
// Expected to be in the format "tokenContractAddressA,..." (e.g., "So11111111111111111111111111111111111111112").
let dr_inputs_raw = String::from_utf8(Process::get_inputs())?;

// If no input is provided, log an error and return.
if dr_inputs_raw.is_empty() {
elog!("No input provided for the price feed request.");
Process::error("No input provided".as_bytes());
return Ok(());
}

// Log the asset pair being fetched as part of the Execution Standard Out.
log!("Fetching price for asset: {dr_inputs_raw}");

let url: String = ["https://lite-api.jup.ag/price/v3?ids=", &dr_inputs_raw].concat();
let response = http_fetch(url, None);

// Handle the case where the HTTP request failed or was rejected.
if !response.is_ok() {
elog!(
"HTTP Response was rejected: {} - {}",
response.status,
String::from_utf8(response.bytes)?
);
Process::error("Error while fetching symbol prices".as_bytes());
return Ok(());
}

// Parse the API response as defined earlier.
let response_data = serde_json::from_slice::<
serde_json::value::Map<String, serde_json::value::Value>,
>(&response.bytes)?;

// Extract the prices for each symbol from the response data.
let price = if let Some(price_data) = response_data.get(&dr_inputs_raw) {
price_data["usdPrice"].as_f64().unwrap_or_default()
} else {
elog!("Price not found for token: {}", dr_inputs_raw);
Process::error("Token price not found".as_bytes());
return Ok(());
};

log!("Fetched price: {price:?}");

// Report the successful result back to the SEDA network.
Process::success(&price.to_le_bytes());

Ok(())
}
17 changes: 17 additions & 0 deletions examples/jup-price-feed/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use execution_phase::execution_phase;
use seda_sdk_rs::oracle_program;
use tally_phase::tally_phase;

mod execution_phase;
mod tally_phase;

#[oracle_program]
impl PriceFeed {
fn execute() {
execution_phase().unwrap();
}

fn tally() {
tally_phase().unwrap();
}
}
51 changes: 51 additions & 0 deletions examples/jup-price-feed/src/tally_phase.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use anyhow::Result;
use seda_sdk_rs::{Process, elog, get_reveals, log};

pub fn tally_phase() -> Result<()> {
// Retrieve consensus reveals from the tally phase.
let reveals = get_reveals()?;
let mut revealed_prices: Vec<f64> = Vec::with_capacity(reveals.len());

for reveal in reveals {
let reveal_bytes = &reveal.body.reveal;
let price = match reveal_bytes.as_slice().try_into() {
Ok(bytes) => f64::from_le_bytes(bytes),
Err(_) => {
elog!("Failed to parse revealed price: expected 8 bytes for f64, got {} bytes", reveal_bytes.len());
continue;
}
};

revealed_prices.push(price);
}

if revealed_prices.is_empty() {
// If no valid prices were revealed, report an error indicating no consensus.
Process::error("No consensus among revealed results".as_bytes());
return Ok(());
}

// If there are valid prices revealed, calculate the median price from price reports.
let final_price = median(&revealed_prices);
log!("Final median prices: {final_price:?}");

// Report the successful result in the tally phase.
Process::success(&final_price.to_string().as_bytes());

Ok(())
}

/// Finds the median of a list of prices per price report using f64 values.
fn median(data: &[f64]) -> f64 {
Comment thread
gluax marked this conversation as resolved.
let mut vals = data.to_vec();
vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = vals.len() / 2;

let median_price = if vals.len() % 2 == 0 {
(vals[mid - 1] + vals[mid]) / 2.0
} else {
vals[mid]
};

median_price
}
11 changes: 11 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct Cli {
/// The oracle programs that can be managed.
#[derive(Clone, ValueEnum)]
enum OracleProgram {
JupPriceFeed,
BlocksizeBidask,
BlocksizeVwap,
CaplightEodMarketPrice,
Expand All @@ -32,6 +33,7 @@ impl OracleProgram {
fn as_str(&self) -> &str {
match self {
OracleProgram::BlocksizeBidask => "blocksize-bidask",
OracleProgram::JupPriceFeed => "jup-price-feed",
OracleProgram::BlocksizeVwap => "blocksize-vwap",
OracleProgram::CaplightEodMarketPrice => "caplight-eod-market-price",
OracleProgram::SingleCommodityPrice => "single-commodity-price",
Expand All @@ -49,6 +51,9 @@ impl OracleProgram {
/// The oracle programs that can have a data request posted to a network.
#[derive(Subcommand)]
enum PostableOracleProgram {
JupPriceFeed {
symbol: String,
},
BlocksizeBidask {
symbol: String,
},
Expand Down Expand Up @@ -376,6 +381,7 @@ impl PostDataRequest {
};

match self.oracle_program {
PostableOracleProgram::JupPriceFeed { symbol } => post_jup_price_feed(cmd, &symbol),
PostableOracleProgram::BlocksizeBidask { symbol } => {
post_blocksize_bidask(cmd, &symbol)
}
Expand Down Expand Up @@ -412,6 +418,11 @@ fn post_blocksize_bidask(cmd: Cmd<'_>, symbol: &str) -> std::result::Result<(),
Ok(())
}

fn post_jup_price_feed(cmd: Cmd<'_>, id: &str) -> std::result::Result<(), anyhow::Error> {
cmd.arg("--exec-inputs").arg(id).run()?;
Ok(())
}

fn post_blocksize_vwap(cmd: Cmd<'_>, pair: &str) -> std::result::Result<(), anyhow::Error> {
cmd.arg("--exec-inputs")
.arg(pair)
Expand Down
Loading