1use anyhow::anyhow;
2use chrono::prelude::*;
3use std::collections::HashMap;
4
5use crate::stellar_sdk::api_call::api_call;
6use crate::stellar_sdk::endpoints::{
7 AccountCallBuilder, AssetCallBuilder, ClaimableBalanceCallbuilder, LedgerCallBuilder,
8 LiquidityPoolCallBuilder, OfferCallBuilder, OperationCallBuilder, OrderBookCallBuilder,
9 PaymentCallBuilder, StrictReceiveCallBuilder, StrictSendCallBuilder,
10 TradeAggregationCallBuilder, TradeCallBuilder, TransactionCallBuilder,
11};
12use crate::stellar_sdk::types::{
13 Account, Asset, ClaimableBalance, FeeStats, Ledger, LiquidityPool, Offer, Operation,
14 StrictPathSource, SubmitTransactionResponse, Transaction,
15};
16use crate::stellar_sdk::utils::request::get_current_server_time;
17
18use super::EffectCallBuilder;
19
20use stellar_base::{transaction::Transaction as TransactionSBase, xdr::XDRSerialize};
21
22#[derive(Debug, Clone)]
23pub struct Server {
24 pub server_url: String,
25 pub options: ServerOptions,
26 pub timebounds: Option<Timebounds>,
27 pub submit_transaction_options: Option<SubmitTransactionOptions>,
28}
29
30#[derive(Debug, Clone)]
31pub struct ServerOptions {
32 pub allow_http: Option<bool>,
33 pub app_name: Option<String>,
34 pub app_version: Option<String>,
35 pub auth_token: Option<String>,
36}
37
38#[derive(Debug, Clone)]
39pub struct Timebounds {
40 pub min_time: i64,
41 pub max_time: i64,
42}
43
44#[derive(Debug, Clone)]
45pub struct SubmitTransactionOptions {
46 pub skip_memo_required_check: Option<bool>,
47}
48
49impl Server {
50 pub fn new(server_url: String, opts: Option<ServerOptions>) -> Result<Self, anyhow::Error> {
51 let options = opts.unwrap_or_else(|| ServerOptions {
53 allow_http: Some(false),
54 app_name: None,
55 app_version: None,
56 auth_token: None,
57 });
58
59 if &server_url.trim()[..5] != "https" && !options.allow_http.unwrap() {
61 return Err(anyhow!("Cannot connect to insecure horizon server"));
62 }
63
64 Ok(Server {
65 server_url,
66 options: options,
67 timebounds: None,
68 submit_transaction_options: None,
69 })
70 }
71
72 pub fn set_auth_token(&mut self, token: String) {
73 self.options.auth_token = Option::from(token);
74 }
75
76 pub fn load_account(&self, account_id: &str) -> Result<Account, anyhow::Error> {
77 let url = format!("{}/accounts/{}", self.server_url, account_id);
78 api_call::<Account>(
79 url,
80 crate::stellar_sdk::types::HttpMethod::GET,
81 &HashMap::new(),
82 &self.options.auth_token,
83 )
84 }
85
86 pub fn accounts(&self) -> AccountCallBuilder {
87 AccountCallBuilder::new(self)
88 }
89
90 pub fn load_transaction(&self, hash: &str) -> Result<Transaction, anyhow::Error> {
91 let url = format!("{}/transactions/{}", self.server_url, hash);
92 api_call::<Transaction>(
93 url,
94 crate::stellar_sdk::types::HttpMethod::GET,
95 &HashMap::new(),
96 &self.options.auth_token,
97 )
98 }
99
100 pub fn transactions(&self) -> TransactionCallBuilder {
101 TransactionCallBuilder::new(self)
102 }
103
104 pub fn load_ledger(&self, sequence: u64) -> Result<Ledger, anyhow::Error> {
105 let url = format!("{}/ledgers/{}", self.server_url, sequence);
106 api_call::<Ledger>(
107 url,
108 crate::stellar_sdk::types::HttpMethod::GET,
109 &HashMap::new(),
110 &self.options.auth_token,
111 )
112 }
113
114 pub fn ledgers(&self) -> LedgerCallBuilder {
115 LedgerCallBuilder::new(self)
116 }
117
118 pub fn load_offer(&self, offer_id: &str) -> Result<Offer, anyhow::Error> {
119 let url = format!("{}/offers/{}", self.server_url, offer_id);
120 api_call::<Offer>(
121 url,
122 crate::stellar_sdk::types::HttpMethod::GET,
123 &HashMap::new(),
124 &self.options.auth_token,
125 )
126 }
127
128 pub fn offers(&self) -> OfferCallBuilder {
129 OfferCallBuilder::new(self)
130 }
131
132 pub fn load_operation(&self, operation_id: &str) -> Result<Operation, anyhow::Error> {
133 let url = format!("{}/operations/{}", self.server_url, operation_id);
134 api_call::<Operation>(
135 url,
136 crate::stellar_sdk::types::HttpMethod::GET,
137 &HashMap::new(),
138 &self.options.auth_token,
139 )
140 }
141
142 pub fn operations(&self) -> OperationCallBuilder {
143 OperationCallBuilder::new(self)
144 }
145
146 pub fn load_liquidity_pool(
147 &self,
148 liquidity_pool_id: &str,
149 ) -> Result<LiquidityPool, anyhow::Error> {
150 let url = format!("{}/liquidity_pools/{}", self.server_url, liquidity_pool_id);
151 api_call::<LiquidityPool>(
152 url,
153 crate::stellar_sdk::types::HttpMethod::GET,
154 &HashMap::new(),
155 &self.options.auth_token,
156 )
157 }
158
159 pub fn liquidity_pools(&self) -> LiquidityPoolCallBuilder {
160 LiquidityPoolCallBuilder::new(self)
161 }
162
163 pub fn load_claimable_balance(
164 &self,
165 claimable_balance_id: &str,
166 ) -> Result<ClaimableBalance, anyhow::Error> {
167 let url = format!(
168 "{}/claimable_balances/{}",
169 self.server_url, claimable_balance_id
170 );
171 api_call::<ClaimableBalance>(
172 url,
173 crate::stellar_sdk::types::HttpMethod::GET,
174 &HashMap::new(),
175 &self.options.auth_token,
176 )
177 }
178
179 pub fn claimable_balances(&self) -> ClaimableBalanceCallbuilder {
180 ClaimableBalanceCallbuilder::new(self)
181 }
182
183 pub fn trade_aggregations<'a>(
184 &'a self,
185 base: &'a Asset,
186 counter: &'a Asset,
187 resolution: &'a str,
188 ) -> TradeAggregationCallBuilder {
189 TradeAggregationCallBuilder::new(self, base, counter, resolution)
190 }
191
192 pub fn order_books(&self, selling: Asset, buying: Asset) -> OrderBookCallBuilder {
193 OrderBookCallBuilder::new(self, selling, buying)
194 }
195
196 pub fn strict_receive_paths<'a>(
197 &'a self,
198 source: &StrictPathSource,
199 destination_asset: Asset,
200 destination_amount: String,
201 ) -> StrictReceiveCallBuilder {
202 StrictReceiveCallBuilder::new(self, source, &destination_asset, &destination_amount)
203 }
204
205 pub fn strict_send_paths<'a>(
206 &'a self,
207 destination: &StrictPathSource,
208 source_asset: &'a Asset,
209 source_amount: &'a str,
210 ) -> StrictSendCallBuilder {
211 StrictSendCallBuilder::new(self, destination, source_asset, source_amount)
212 }
213
214 pub fn trades(&self) -> TradeCallBuilder {
215 TradeCallBuilder::new(self)
216 }
217
218 pub fn payments(&self) -> PaymentCallBuilder {
219 PaymentCallBuilder::new(self)
220 }
221
222 pub fn assets(&self) -> AssetCallBuilder {
223 AssetCallBuilder::new(self)
224 }
225
226 pub fn fee_stats(&self) -> Result<FeeStats, anyhow::Error> {
227 let url = format!("{}/fee_stats", self.server_url);
228 api_call::<FeeStats>(
229 url,
230 crate::stellar_sdk::types::HttpMethod::GET,
231 &HashMap::new(),
232 &self.options.auth_token,
233 )
234 }
235
236 pub fn fetch_base_fee(&self) -> Result<String, anyhow::Error> {
237 let fee_stats = self.fee_stats()?;
238 let base_fee = fee_stats.last_ledger_base_fee;
239 Ok(base_fee)
240 }
241
242 pub fn fetch_timebounds(
243 &mut self,
244 seconds: i64,
245 is_retry: bool,
246 ) -> Result<Timebounds, anyhow::Error> {
247 let current_server_time = get_current_server_time(&self.server_url);
248
249 if !current_server_time.is_none() && is_retry == false {
250 Ok(Timebounds {
251 min_time: 0,
252 max_time: current_server_time.unwrap() + seconds,
253 })
254 } else if is_retry == true {
255 let local_now: DateTime<Local> = Local::now();
256 let local_timestamp = local_now.timestamp();
257 Ok(Timebounds {
258 min_time: 0,
259 max_time: local_timestamp + seconds,
260 })
261 } else {
262 self.fetch_timebounds(seconds, true)
263 }
264 }
265
266 pub fn submit_transaction(
267 &self,
268 transaction: TransactionSBase,
269 ) -> Result<SubmitTransactionResponse, anyhow::Error> {
270 let tx = transaction.into_envelope().xdr_base64()?;
271 let url = format!("{}/transactions", self.server_url);
272
273 let mut query = HashMap::new();
274 query.insert("tx".to_string(), tx.to_string());
275
276 api_call::<SubmitTransactionResponse>(
277 url,
278 crate::stellar_sdk::types::HttpMethod::POST,
279 &query,
280 &self.options.auth_token,
281 )
282 }
283
284 pub fn effects(&self) -> EffectCallBuilder {
285 EffectCallBuilder::new(self)
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use crate::stellar_sdk::{endpoints::call_builder::CallBuilder, utils::Endpoint};
292 use std::str::FromStr;
293 use stellar_base::{
294 amount::Amount,
295 asset::Asset,
296 crypto::SodiumKeyPair,
297 memo::Memo,
298 operations::Operation,
299 transaction::{Transaction, MIN_BASE_FEE},
300 Network, PublicKey,
301 };
302
303 use super::*;
304
305 #[test]
306 fn test_load_account() {
307 let s = Server::new(String::from("https://horizon.stellar.org"), None)
308 .expect("Cannot connect to insecure horizon server");
309
310 let tx = s
311 .load_account("GAUZUPTHOMSZEV65VNSRMUDAAE4VBMSRYYAX3UOWYU3BQUZ6OK65NOWM")
312 .unwrap();
313
314 assert_eq!(tx.id, tx.account_id);
315 }
316
317 #[test]
318 fn test_load_transaction() {
319 let s = Server::new(String::from("https://horizon.stellar.org"), None)
320 .expect("Cannot connect to insecure horizon server");
321
322 let tx = s
323 .load_transaction("3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889")
324 .unwrap();
325
326 assert_eq!(tx.id, tx.hash);
327 }
328
329 #[test]
330 fn test_load_ledger() {
331 let s = Server::new(String::from("https://horizon.stellar.org"), None)
332 .expect("Cannot connect to insecure horizon server");
333
334 let ledger3 = s.load_ledger(3).unwrap();
335 let ledger4 = s.load_ledger(4).unwrap();
336
337 assert_eq!(ledger3.hash, ledger4.prev_hash);
338 }
339
340 #[test]
341 fn test_load_fee_stats() {
342 let s = Server::new(String::from("https://horizon.stellar.org"), None)
343 .expect("Cannot connect to insecure horizon server");
344
345 let _fee_stats = s.fee_stats().unwrap();
346 }
347
348 #[test]
349 fn test_load_base_fee() {
350 let s = Server::new(String::from("https://horizon.stellar.org"), None)
351 .expect("Cannot connect to insecure horizon server");
352
353 let _base_fee = s.fetch_base_fee().unwrap();
354 }
355
356 #[test]
357 fn load_operation() {
358 let s = Server::new(String::from("https://horizon.stellar.org"), None)
359 .expect("Cannot connect to insecure horizon server");
360
361 let op = s.load_operation("33676838572033").unwrap();
362
363 assert_eq!(op.id, op.paging_token);
364 }
365
366 #[test]
367 fn load_some_operations() {
368 let s = Server::new(String::from("https://horizon.stellar.org"), None)
369 .expect("Cannot connect to insecure horizon server");
370
371 let my_account = "GAUZUPTHOMSZEV65VNSRMUDAAE4VBMSRYYAX3UOWYU3BQUZ6OK65NOWM";
372
373 let my_ops = s
374 .operations()
375 .include_failed(true)
376 .for_endpoint(Endpoint::Accounts(String::from(my_account)))
377 .limit(2)
378 .call()
379 .unwrap();
380
381 assert_eq!(my_ops._embedded.records.len(), 2);
382 }
383
384 #[test]
385 fn test_load_trade() {
386 let s = Server::new(String::from("https://horizon.stellar.org"), None)
387 .expect("Cannot connect to insecure horizon server");
388
389 let my_trade = s.trades().for_offer("4").limit(1).call().unwrap();
390
391 assert_eq!("4", my_trade._embedded.records[0].base_offer_id)
392 }
393
394 #[test]
395 fn test_fetch_timebounds() {
396 let mut s = Server::new(String::from("https://horizon.stellar.org"), None)
397 .expect("Cannot connect to insecure horizon server");
398
399 let timebounds = s.fetch_timebounds(10000, false).unwrap();
400 let local_now: DateTime<Local> = Local::now();
401 let local_timestamp = local_now.timestamp();
402 assert!(timebounds.min_time + local_timestamp < timebounds.max_time);
403 }
404
405 #[test]
406 fn test_submit_transaction() {
407 let s = Server::new(String::from("https://horizon-testnet.stellar.org"), None)
408 .expect("Cannot connect to insecure horizon server");
409
410 let source_keypair = SodiumKeyPair::from_secret_seed(
412 "SCPQMOR2R2PGTFGBHXTSP4KB47Y6XVLAZEOCCMSAU6QXP3KPLXRVXZBV",
413 )
414 .unwrap();
415
416 let destination =
417 PublicKey::from_account_id("GAST24JSPH5S5Z2HC5PKEVQYDZIPFLOEC26KLVDNPVFVNNRALVTM6SCN")
418 .unwrap();
419
420 let payment_amount = Amount::from_str("0.1").unwrap();
421
422 let payment = Operation::new_payment()
423 .with_destination(destination.clone())
424 .with_amount(payment_amount)
425 .unwrap()
426 .with_asset(Asset::new_native())
427 .build()
428 .unwrap();
429
430 let account = s
431 .load_account(&source_keypair.public_key().clone().to_string())
432 .unwrap();
433
434 let sequence = account.sequence.parse::<i64>().unwrap() + 1;
435
436 let mut tx =
437 Transaction::builder(source_keypair.public_key().clone(), sequence, MIN_BASE_FEE)
438 .with_memo(Memo::Text("stellar_sdk_test".to_string()))
439 .add_operation(payment)
440 .into_transaction()
441 .unwrap();
442
443 let _ = tx.sign(&source_keypair.as_ref(), &Network::new_test());
444
445 let response = s.submit_transaction(tx);
446 assert_eq!(response.is_ok(), true);
447 }
448}