Skip to content

Commit 7b40592

Browse files
dev : add pragma api to compute fees in STRK (#1633)
Co-authored-by: 0xevolve <Artevolve@yahoo.com>
1 parent f5dce7e commit 7b40592

File tree

6 files changed

+197
-8
lines changed

6 files changed

+197
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,4 @@
348348
- dev : clean contracts and compiled files
349349
- fix: add from_address in calldata of l1 message
350350
- test: add starkgate related testcase
351+
- feat: add pragma api to compute fees

crates/client/eth-client/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use ethers::types::{Address, H160};
1919
use serde::{Deserialize, Serialize};
2020

2121
use crate::error::Error;
22+
use crate::oracle::OracleConfig;
2223

2324
/// Default Anvil local endpoint
2425
pub const DEFAULT_RPC_ENDPOINT: &str = "http://127.0.0.1:8545";
@@ -37,6 +38,8 @@ pub struct EthereumClientConfig {
3738
pub wallet: Option<EthereumWalletConfig>,
3839
#[serde(default)]
3940
pub contracts: StarknetContracts,
41+
#[serde(default)]
42+
pub oracle: OracleConfig,
4043
}
4144

4245
#[derive(Debug, Clone, Serialize, Deserialize)]

crates/client/eth-client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
pub mod config;
1111
pub mod error;
12+
pub mod oracle;
1213

1314
use std::time::Duration;
1415

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use std::fmt;
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
pub const DEFAULT_API_URL: &str = "https://api.dev.pragma.build/node/v1/data/";
6+
7+
#[derive(Debug, Clone, Serialize, Deserialize)]
8+
#[serde(tag = "oracle_name", content = "config")]
9+
pub enum OracleConfig {
10+
Pragma(PragmaOracle),
11+
}
12+
13+
impl OracleConfig {
14+
pub fn get_fetch_url(&self, base: String, quote: String) -> String {
15+
match self {
16+
OracleConfig::Pragma(pragma_oracle) => pragma_oracle.get_fetch_url(base, quote),
17+
}
18+
}
19+
20+
pub fn get_api_key(&self) -> &String {
21+
match self {
22+
OracleConfig::Pragma(oracle) => &oracle.api_key,
23+
}
24+
}
25+
26+
pub fn is_in_bounds(&self, price: u128) -> bool {
27+
match self {
28+
OracleConfig::Pragma(oracle) => oracle.price_bounds.low <= price && price <= oracle.price_bounds.high,
29+
}
30+
}
31+
}
32+
33+
impl Default for OracleConfig {
34+
fn default() -> Self {
35+
Self::Pragma(PragmaOracle::default())
36+
}
37+
}
38+
39+
#[derive(Debug, Clone, Serialize, Deserialize)]
40+
pub struct PragmaOracle {
41+
#[serde(default = "default_oracle_api_url")]
42+
pub api_url: String,
43+
#[serde(default)]
44+
pub api_key: String,
45+
#[serde(default)]
46+
pub aggregation_method: AggregationMethod,
47+
#[serde(default)]
48+
pub interval: Interval,
49+
#[serde(default)]
50+
pub price_bounds: PriceBounds,
51+
}
52+
53+
impl Default for PragmaOracle {
54+
fn default() -> Self {
55+
Self {
56+
api_url: default_oracle_api_url(),
57+
api_key: String::default(),
58+
aggregation_method: AggregationMethod::Median,
59+
interval: Interval::OneMinute,
60+
price_bounds: Default::default(),
61+
}
62+
}
63+
}
64+
65+
impl PragmaOracle {
66+
fn get_fetch_url(&self, base: String, quote: String) -> String {
67+
format!("{}{}/{}?interval={}&aggregation={}", self.api_url, base, quote, self.interval, self.aggregation_method)
68+
}
69+
}
70+
71+
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
72+
/// Supported Aggregation Methods
73+
pub enum AggregationMethod {
74+
#[serde(rename = "median")]
75+
Median,
76+
#[serde(rename = "mean")]
77+
Mean,
78+
#[serde(rename = "twap")]
79+
#[default]
80+
Twap,
81+
}
82+
83+
impl fmt::Display for AggregationMethod {
84+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85+
let name = match self {
86+
AggregationMethod::Median => "median",
87+
AggregationMethod::Mean => "mean",
88+
AggregationMethod::Twap => "twap",
89+
};
90+
write!(f, "{}", name)
91+
}
92+
}
93+
94+
/// Supported Aggregation Intervals
95+
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
96+
pub enum Interval {
97+
#[serde(rename = "1min")]
98+
OneMinute,
99+
#[serde(rename = "15min")]
100+
FifteenMinutes,
101+
#[serde(rename = "1h")]
102+
OneHour,
103+
#[serde(rename = "2h")]
104+
#[default]
105+
TwoHours,
106+
}
107+
108+
impl fmt::Display for Interval {
109+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110+
let name = match self {
111+
Interval::OneMinute => "1min",
112+
Interval::FifteenMinutes => "15min",
113+
Interval::OneHour => "1h",
114+
Interval::TwoHours => "2h",
115+
};
116+
write!(f, "{}", name)
117+
}
118+
}
119+
120+
#[derive(Debug, Clone, Serialize, Deserialize)]
121+
pub struct PriceBounds {
122+
pub low: u128,
123+
pub high: u128,
124+
}
125+
126+
impl Default for PriceBounds {
127+
fn default() -> Self {
128+
Self { low: 0, high: u128::MAX }
129+
}
130+
}
131+
132+
fn default_oracle_api_url() -> String {
133+
DEFAULT_API_URL.into()
134+
}

crates/client/l1-gas-price/src/worker.rs

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,32 @@ use std::sync::Arc;
33
use std::time::Duration;
44

55
use anyhow::{format_err, Result};
6+
use ethers::types::U256;
67
use ethers::utils::__serde_json::json;
78
use futures::lock::Mutex;
89
use mc_eth_client::config::EthereumClientConfig;
10+
use mc_eth_client::oracle::OracleConfig;
911
use mp_starknet_inherent::L1GasPrices;
12+
use serde::Deserialize;
1013
use tokio::time::sleep;
1114

1215
use crate::types::{EthRpcResponse, FeeHistory};
1316

1417
const DEFAULT_GAS_PRICE_POLL_MS: u64 = 10_000;
1518

19+
#[derive(Deserialize, Debug)]
20+
struct OracleApiResponse {
21+
price: String,
22+
decimals: u32,
23+
}
24+
1625
pub async fn run_worker(config: Arc<EthereumClientConfig>, gas_price: Arc<Mutex<L1GasPrices>>, infinite_loop: bool) {
1726
let rpc_endpoint = config.provider.rpc_endpoint().clone();
1827
let client = reqwest::Client::new();
1928
let poll_time = config.provider.gas_price_poll_ms().unwrap_or(DEFAULT_GAS_PRICE_POLL_MS);
2029

2130
loop {
22-
match update_gas_price(rpc_endpoint.clone(), &client, gas_price.clone()).await {
31+
match update_gas_price(rpc_endpoint.clone(), &client, gas_price.clone(), config.oracle.clone()).await {
2332
Ok(_) => log::trace!("Updated gas prices"),
2433
Err(e) => log::error!("Failed to update gas prices: {:?}", e),
2534
}
@@ -52,6 +61,7 @@ async fn update_gas_price(
5261
rpc_endpoint: String,
5362
client: &reqwest::Client,
5463
gas_price: Arc<Mutex<L1GasPrices>>,
64+
oracle: OracleConfig,
5565
) -> Result<()> {
5666
let fee_history: EthRpcResponse<FeeHistory> = client
5767
.post(rpc_endpoint.clone())
@@ -88,18 +98,45 @@ async fn update_gas_price(
8898
16,
8999
)?;
90100

91-
// TODO: fetch this from the oracle
92-
let eth_strk_price = 2425;
101+
let response = reqwest::Client::new()
102+
.get(oracle.get_fetch_url(String::from("eth"), String::from("strk")))
103+
.header("x-api-key", oracle.get_api_key())
104+
.send()
105+
.await?;
106+
107+
let oracle_api_response = response.json::<OracleApiResponse>().await;
93108

94109
let mut gas_price = gas_price.lock().await;
110+
111+
match oracle_api_response {
112+
Ok(api_response) => {
113+
log::trace!("Retrieved ETH/STRK price from Oracle");
114+
let eth_strk_price = u128::from_str_radix(api_response.price.trim_start_matches("0x"), 16)?;
115+
if oracle.is_in_bounds(eth_strk_price) {
116+
let stark_gas = ((U256::from(eth_gas_price) * U256::from(eth_strk_price))
117+
/ 10u64.pow(api_response.decimals))
118+
.as_u128();
119+
let stark_data_gas = ((U256::from(avg_blob_base_fee) * U256::from(eth_strk_price))
120+
/ 10u64.pow(api_response.decimals))
121+
.as_u128();
122+
gas_price.strk_l1_gas_price = NonZeroU128::new(stark_gas)
123+
.ok_or(format_err!("Failed to convert `strk_l1_gas_price` to NonZeroU128"))?;
124+
gas_price.strk_l1_data_gas_price = NonZeroU128::new(stark_data_gas)
125+
.ok_or(format_err!("Failed to convert `strk_l1_data_gas_price` to NonZeroU128"))?;
126+
} else {
127+
log::error!("⚠️ Retrieved price is outside of bounds");
128+
}
129+
}
130+
Err(e) => {
131+
log::error!("Failed to retrieve ETH/STRK price: {:?}", e);
132+
}
133+
};
134+
95135
gas_price.eth_l1_gas_price =
96136
NonZeroU128::new(eth_gas_price).ok_or(format_err!("Failed to convert `eth_gas_price` to NonZeroU128"))?;
97137
gas_price.eth_l1_data_gas_price = NonZeroU128::new(avg_blob_base_fee)
98138
.ok_or(format_err!("Failed to convert `eth_l1_data_gas_price` to NonZeroU128"))?;
99-
gas_price.strk_l1_gas_price = NonZeroU128::new(eth_gas_price.saturating_mul(eth_strk_price))
100-
.ok_or(format_err!("Failed to convert `strk_l1_gas_price` to NonZeroU128"))?;
101-
gas_price.strk_l1_data_gas_price = NonZeroU128::new(avg_blob_base_fee.saturating_mul(eth_strk_price))
102-
.ok_or(format_err!("Failed to convert `strk_l1_data_gas_price` to NonZeroU128"))?;
139+
103140
gas_price.last_update_timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_millis();
104141
// explicitly dropping gas price here to avoid long waits when fetching the value
105142
// on the inherent side which would increase block time

examples/messaging/eth-config.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
{
22
"provider": {
3-
"rpc_endpoint": "http://127.0.0.1:8545",
3+
"rpc_endpoint": "https://ethereum-rpc.publicnode.com",
44
"gas_price_poll_ms": 10000
55
},
66
"contracts": {
77
"core_contract": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"
8+
},
9+
"oracle": {
10+
"oracle_name": "Pragma",
11+
"config": {
12+
"api_url": "https://api.dev.pragma.build/node/v1/data/",
13+
"api_key": "",
14+
"aggregation_method": "twap",
15+
"interval": "2h",
16+
"price_bounds": {
17+
"low": 3000000000000000000000,
18+
"high": 6000000000000000000000
19+
}
20+
}
821
}
922
}

0 commit comments

Comments
 (0)