1use crate::error::{Error, Result};
15use crate::networks::{get_network, Endpoints, NetworkConfig};
16use crate::query::{QorClient, RestClient};
17use serde_json::{json, Value};
18
19#[derive(Debug, Clone, Default)]
21pub struct EndpointOverrides {
22 pub rest: Option<String>,
24 pub grpc: Option<String>,
26 pub rpc: Option<String>,
28 pub evm_rpc: Option<String>,
30 pub evm_ws: Option<String>,
32 pub svm_rpc: Option<String>,
34}
35
36#[derive(Debug, Clone)]
38pub struct ClientBuilder {
39 network: String,
40 overrides: EndpointOverrides,
41 chain_id: Option<String>,
42 http: Option<reqwest::Client>,
43}
44
45impl Default for ClientBuilder {
46 fn default() -> Self {
47 Self {
48 network: "testnet".into(),
49 overrides: EndpointOverrides::default(),
50 chain_id: None,
51 http: None,
52 }
53 }
54}
55
56impl ClientBuilder {
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn network(mut self, network: impl Into<String>) -> Self {
64 self.network = network.into();
65 self
66 }
67
68 pub fn rest(mut self, url: impl Into<String>) -> Self {
70 self.overrides.rest = Some(url.into());
71 self
72 }
73
74 pub fn grpc(mut self, url: impl Into<String>) -> Self {
76 self.overrides.grpc = Some(url.into());
77 self
78 }
79
80 pub fn rpc(mut self, url: impl Into<String>) -> Self {
82 self.overrides.rpc = Some(url.into());
83 self
84 }
85
86 pub fn evm_rpc(mut self, url: impl Into<String>) -> Self {
88 self.overrides.evm_rpc = Some(url.into());
89 self
90 }
91
92 pub fn evm_ws(mut self, url: impl Into<String>) -> Self {
94 self.overrides.evm_ws = Some(url.into());
95 self
96 }
97
98 pub fn svm_rpc(mut self, url: impl Into<String>) -> Self {
100 self.overrides.svm_rpc = Some(url.into());
101 self
102 }
103
104 pub fn chain_id(mut self, chain_id: impl Into<String>) -> Self {
106 self.chain_id = Some(chain_id.into());
107 self
108 }
109
110 pub fn http_client(mut self, http: reqwest::Client) -> Self {
112 self.http = Some(http);
113 self
114 }
115
116 pub fn build(self) -> Result<Client> {
121 let resolved = resolve_network(&self.network, &self.overrides, self.chain_id.as_deref())?;
122 let eps = resolved
123 .endpoints
124 .as_ref()
125 .ok_or_else(|| Error::MissingEndpoint("rest".to_string()))?;
126
127 let rest_url = require_endpoint("rest", &eps.rest)?;
128 let evm_url = require_endpoint("evm_rpc", &eps.evm_rpc)?;
129
130 let http = self.http.unwrap_or_default();
131 let rest = RestClient::with_client(rest_url, http.clone());
132 let qor = QorClient::from_jsonrpc(crate::query::JsonRpcClient::with_client(evm_url, http));
133 let fees = Fees { rest: rest.clone() };
134
135 Ok(Client {
136 network: resolved,
137 rest,
138 qor,
139 fees,
140 })
141 }
142}
143
144#[derive(Debug, Clone)]
146pub struct Client {
147 pub network: NetworkConfig,
149 pub rest: RestClient,
151 pub qor: QorClient,
153 pub fees: Fees,
155}
156
157#[derive(Debug, Clone)]
159pub struct Fees {
160 rest: RestClient,
161}
162
163const STATIC_FALLBACK_GAS_PRICE: &str = "0.025";
165const STATIC_FALLBACK_DENOM: &str = "uqor";
166const STATIC_FALLBACK_GAS: &str = "200000";
167
168impl Fees {
169 pub async fn estimate(&self, urgency: &str) -> Result<Value> {
174 let urgency = if urgency.is_empty() {
175 "normal"
176 } else {
177 urgency
178 };
179 if let Ok(raw) = self.rest.get_fee_estimate(urgency).await {
180 if let Some(amount) = raw
181 .get("suggested_fee_uqor")
182 .map(value_to_amount_string)
183 .filter(|a| !a.is_empty() && a != "0")
184 {
185 return static_fee(STATIC_FALLBACK_GAS, "", STATIC_FALLBACK_DENOM, &amount);
186 }
187 }
188 static_fee(
189 STATIC_FALLBACK_GAS,
190 STATIC_FALLBACK_GAS_PRICE,
191 STATIC_FALLBACK_DENOM,
192 "",
193 )
194 }
195}
196
197fn value_to_amount_string(v: &Value) -> String {
198 match v {
199 Value::String(s) => s.clone(),
200 Value::Number(n) => n.to_string(),
201 _ => String::new(),
202 }
203}
204
205fn static_fee(gas: &str, gas_price: &str, denom: &str, amount: &str) -> Result<Value> {
208 let amount = if amount.is_empty() {
209 compute_ceil_fee(gas, gas_price)?
210 } else {
211 amount.to_string()
212 };
213 Ok(json!({
214 "amount": [{ "denom": denom, "amount": amount }],
215 "gas": gas,
216 }))
217}
218
219fn compute_ceil_fee(gas: &str, gas_price: &str) -> Result<String> {
222 let gas_units: u128 = gas
223 .parse()
224 .map_err(|_| Error::Denom(format!("invalid gas: {gas}")))?;
225 let (int_part, frac_part) = match gas_price.split_once('.') {
226 Some((i, f)) => (i, f),
227 None => (gas_price, ""),
228 };
229 let ip: u128 = or_zero(int_part)
230 .parse()
231 .map_err(|_| Error::Denom(format!("invalid gas price: {gas_price}")))?;
232 let fp: u128 = or_zero(frac_part)
233 .parse()
234 .map_err(|_| Error::Denom(format!("invalid gas price: {gas_price}")))?;
235 let scale = 10u128.pow(frac_part.len() as u32);
236 let numerator = ip * scale + fp;
237 let raw = gas_units * numerator;
238 Ok(raw.div_ceil(scale).to_string())
240}
241
242fn or_zero(s: &str) -> &str {
243 if s.is_empty() {
244 "0"
245 } else {
246 s
247 }
248}
249
250fn resolve_network(
251 network: &str,
252 overrides: &EndpointOverrides,
253 chain_id: Option<&str>,
254) -> Result<NetworkConfig> {
255 let mut resolved = get_network(network)?;
258 if let Some(eps) = resolved.endpoints.as_mut() {
259 overlay(eps, overrides);
260 }
261 if let Some(cid) = chain_id {
262 resolved.chain_id = Some(cid.to_string());
263 }
264 Ok(resolved)
265}
266
267fn overlay(eps: &mut Endpoints, o: &EndpointOverrides) {
268 if let Some(v) = &o.rest {
269 eps.rest = v.clone();
270 }
271 if let Some(v) = &o.grpc {
272 eps.grpc = v.clone();
273 }
274 if let Some(v) = &o.rpc {
275 eps.rpc = v.clone();
276 }
277 if let Some(v) = &o.evm_rpc {
278 eps.evm_rpc = v.clone();
279 }
280 if let Some(v) = &o.evm_ws {
281 eps.evm_ws = v.clone();
282 }
283 if let Some(v) = &o.svm_rpc {
284 eps.svm_rpc = v.clone();
285 }
286}
287
288fn require_endpoint(key: &str, value: &str) -> Result<String> {
289 if value.is_empty() {
290 return Err(Error::MissingEndpoint(key.to_string()));
291 }
292 Ok(value.to_string())
293}
294
295pub fn create_client(network: &str) -> Result<Client> {
301 ClientBuilder::new().network(network).build()
302}