pi_rust/stellar_sdk/endpoints/
server.rs

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        // If the opts parameter provided we are unwrapping, if not then we are giving default value
52        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        // Non https not allowed in production
60        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        // Test can easily fail because someone drained the wallet, but it's okay for now later can be used .env or always asking the friendbot with new random wallet
411        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}