soroban_client/
server.rs

1use crate::jsonrpc::{JsonRpc, Response};
2use crate::transaction::assemble_transaction;
3use crate::{error, soroban_rpc::*};
4use crate::{error::*, friendbot};
5use futures::TryFutureExt;
6use serde_json::json;
7use std::option::Option;
8use std::time::Duration;
9use std::{collections::HashMap, str::FromStr};
10use stellar_baselib::account::Account;
11use stellar_baselib::account::AccountBehavior;
12use stellar_baselib::address::{Address, AddressTrait};
13use stellar_baselib::keypair::KeypairBehavior;
14use stellar_baselib::transaction::{Transaction, TransactionBehavior};
15use stellar_baselib::xdr::{
16    ContractDataDurability, LedgerEntryData, LedgerKey, LedgerKeyAccount, LedgerKeyContractData,
17    Limits, ScVal, WriteXdr,
18};
19use tokio::time::{sleep, Instant};
20
21/// The default transaction submission timeout for RPC requests, in milliseconds.
22pub const SUBMIT_TRANSACTION_TIMEOUT: u32 = 60 * 1000;
23
24/// Representation of the ledger entry durability to be used with [Server::get_contract_data]
25#[derive(Debug, PartialEq, Eq)]
26pub enum Durability {
27    /// Temporary storage, cannot be restored
28    Temporary,
29    /// Persistent storage, archived when the TTL is expired, can be restored
30    Persistent,
31}
32
33impl Durability {
34    fn to_xdr(&self) -> ContractDataDurability {
35        match self {
36            Durability::Temporary => ContractDataDurability::Temporary,
37            Durability::Persistent => ContractDataDurability::Persistent,
38        }
39    }
40}
41
42/// Set the boundaries while fetching data from the RPC
43///
44/// `From(start) and FromTo(start, end)`
45/// `start` is the ledger sequence number to start fetching responses from (inclusive). This
46/// method will return an error if startLedger is less than the oldest ledger stored in this node,
47/// or greater than the latest ledger seen by this node.
48///
49/// `end` is the ledger sequence number represents the end of search window (exclusive)
50///
51/// `Cursor(cursor)`
52/// A unique identifier (specifically, a [TOID]) that points to a specific location in a collection
53/// of responses and is pulled from the paging_token value of a record. When a cursor is provided,
54/// RPC will not include the element whose ID matches the cursor in the response: only elements
55/// which appear after the cursor will be included.
56///
57/// [TOID]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0035.md#specification
58pub enum Pagination {
59    /// Fetch events starting at this ledger sequence
60    From(u32),
61    /// Fetch events from and up to these ledger sequences
62    FromTo(u32, u32),
63    /// Fetch events after this cursor
64    Cursor(String),
65}
66/// List of filters for the returned events. Events matching any of the filters are included.
67/// To match a filter, an event must match both a contractId and a topic. Maximum 5 filters are
68/// allowed per request.
69pub struct EventFilter {
70    event_type: EventType,
71    contract_ids: Vec<String>,
72    topics: Vec<Vec<Topic>>,
73}
74
75/// Topic to match on in the filter
76#[derive(Clone, Debug)]
77pub enum Topic {
78    /// Match this topic `ScVal`
79    Val(ScVal),
80    /// Match any topic
81    Any,
82    /// Match any topic including more topics (can only be the last [Topic])
83    Greedy,
84}
85impl EventFilter {
86    /// Start building a new filter for this [EventType]
87    pub fn new(event_type: EventType) -> Self {
88        EventFilter {
89            event_type,
90            contract_ids: Vec::new(),
91            topics: Vec::new(),
92        }
93    }
94
95    /// Include this `contract_id` in the filter. If omitted, return events for all contracts.
96    /// Maximum 5 contract IDs are allowed per request.
97    pub fn contract(self, contract_id: &str) -> Self {
98        let mut contract_ids = self.contract_ids.to_vec();
99        contract_ids.push(contract_id.to_string());
100        EventFilter {
101            contract_ids,
102            ..self
103        }
104    }
105
106    /// List of topic filters. If omitted, query for all events. If multiple filters are specified,
107    /// events will be included if they match any of the filters. Maximum 5 filters are allowed
108    /// per request.
109    pub fn topic(self, filer: Vec<Topic>) -> Self {
110        let mut topics = self.topics.to_vec();
111        topics.push(filer);
112        EventFilter { topics, ..self }
113    }
114
115    fn event_type(&self) -> Option<String> {
116        match self.event_type {
117            EventType::Contract => Some("contract".to_string()),
118            EventType::System => Some("system".to_string()),
119            EventType::Diagnostic => Some("diagnostic".to_string()),
120            EventType::All => None,
121        }
122    }
123
124    fn contracts(&self) -> Vec<String> {
125        self.contract_ids.to_vec()
126    }
127
128    fn topics(&self) -> Vec<Vec<String>> {
129        self.topics
130            .iter()
131            .map(|v| {
132                v.iter()
133                    .map(|vv| match vv {
134                        Topic::Val(sc_val) => sc_val
135                            .to_xdr_base64(Limits::none())
136                            .expect("ScVal cannot be converted to base64"),
137                        Topic::Any => "*".to_string(),
138                        Topic::Greedy => "**".to_string(),
139                    })
140                    .collect()
141            })
142            .collect()
143    }
144}
145
146/// Contains configuration for how resources will be calculated when simulating transactions.
147#[derive(Debug, Clone, Default)]
148pub struct SimulationOptions {
149    /// Allow this many extra instructions when budgeting resources.
150    pub cpu_instructions: u64,
151    /// The auth mode to apply to the simulation, if None enforce if auth entries are present, record otherwise
152    pub auth_mode: Option<AuthMode>,
153}
154
155/// Select the auth mode to apply to the simulation
156#[derive(Debug, Clone)]
157pub enum AuthMode {
158    /// Always enforcement mode, even with an empty list of auths
159    Enforce,
160    /// Always recording mode, failing if any auth exists
161    Record,
162    /// Like [AuthMode::Record] but allowing non-root authorization
163    RecordAllowNonRoot,
164}
165
166impl FromStr for AuthMode {
167    type Err = error::AuthModeError;
168    fn from_str(s: &str) -> Result<Self, Self::Err> {
169        match s {
170            "enforce" => Ok(Self::Enforce),
171            "record" => Ok(Self::Record),
172            "record_allow_nonroot" => Ok(Self::RecordAllowNonRoot),
173
174            e => Err(AuthModeError::Invalid(e.to_string())),
175        }
176    }
177}
178
179impl From<AuthMode> for &str {
180    fn from(val: AuthMode) -> Self {
181        match val {
182            AuthMode::Enforce => "enforce",
183            AuthMode::Record => "record",
184            AuthMode::RecordAllowNonRoot => "record_allow_nonroot",
185        }
186    }
187}
188
189/// Additionnal options
190#[derive(Debug)]
191pub struct Options {
192    /// If true, using a non HTTPS RPC will not throw an error
193    pub allow_http: bool,
194    /// Timeout in seconds (default: 10)
195    pub timeout: u64,
196    /// Additionnal headers to use while requesting the RPC
197    pub headers: HashMap<String, String>,
198    /// Optional friendbot URL
199    pub friendbot_url: Option<String>,
200}
201
202impl Default for Options {
203    fn default() -> Self {
204        Self {
205            allow_http: false,
206            timeout: 10,
207            headers: Default::default(),
208            friendbot_url: None,
209        }
210    }
211}
212
213/// The main struct to use to interact with the stellar RPC
214#[derive(Debug)]
215pub struct Server {
216    client: JsonRpc,
217    friendbot_url: Option<String>,
218}
219
220impl Server {
221    /// # Instantiate a new [Server]
222    ///
223    /// ```rust
224    /// use soroban_client::*;
225    /// let rpc = Server::new("https://soroban-testnet.stellar.org", Options::default());
226    /// ```
227    pub fn new(server_url: &str, opts: Options) -> Result<Self, Error> {
228        let server_url = reqwest::Url::from_str(server_url)
229            .map_err(|_e| Error::InvalidRpc(InvalidRpcUrl::InvalidUri))?;
230        let allow_http = opts.allow_http;
231        match server_url.scheme() {
232            "https" => {
233                // good
234            }
235            "http" if allow_http => {
236                // good
237            }
238            "http" if !allow_http => {
239                return Err(Error::InvalidRpc(InvalidRpcUrl::UnsecureHttpNotAllowed));
240            }
241            _ => {
242                return Err(Error::InvalidRpc(InvalidRpcUrl::NotHttpScheme));
243            }
244        };
245
246        Ok(Server {
247            client: JsonRpc::new(server_url, opts.timeout, opts.headers),
248            friendbot_url: opts.friendbot_url,
249        })
250    }
251
252    // RPC method implementations -------------------------------
253
254    /// # Call to RPC method [getEvents]
255    ///
256    /// Clients can request a filtered list of events emitted by a given ledger range.
257    ///
258    /// Stellar-RPC will support querying within a maximum 7 days of recent ledgers.
259    ///
260    /// Note, this could be used by the client to only prompt a refresh when there is a new ledger
261    /// with relevant events. It should also be used by backend Dapp components to "ingest" events
262    /// into their own database for querying and serving.
263    ///
264    /// If making multiple requests, clients should deduplicate any events received, based on the
265    /// event's unique id field. This prevents double-processing in the case of duplicate events
266    /// being received.
267    ///
268    /// By default stellar-rpc retains the most recent 24 hours of events.
269    ///
270    /// # Example
271    /// ```rust
272    /// // Fetch 12 events from ledger 67000 for contract "CAA..."
273    /// # use soroban_client::soroban_rpc::*;
274    /// # use soroban_client::*;
275    /// # use soroban_client::error::Error;
276    /// # async fn events() -> Result<(), Error> {
277    /// # let server = Server::new("https://rpc.server", Options::default())?;
278    /// let events = server.get_events(
279    ///     Pagination::From(67000),
280    ///     vec![
281    ///         EventFilter::new(EventType::All).contract("CAA...")
282    ///     ],
283    ///     12
284    /// ).await?;
285    /// # return Ok(()); }
286    ///
287    /// ```
288    ///
289    /// [getEvents]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getEvents
290    ///
291    pub async fn get_events(
292        &self,
293        ledger: Pagination,
294        filters: Vec<EventFilter>,
295        limit: impl Into<Option<u32>>,
296    ) -> Result<GetEventsResponse, Error> {
297        let (start_ledger, end_ledger, cursor) = match ledger {
298            Pagination::From(s) => (Some(s), None, None),
299            Pagination::FromTo(s, e) => (Some(s), Some(e), None),
300            Pagination::Cursor(c) => (None, None, Some(c)),
301        };
302        let filters = filters
303            .into_iter()
304            .map(|v| {
305                //
306                json!({
307                    "type": v.event_type(),
308                    "contractIds": v.contracts(),
309                    "topics": v.topics(),
310                })
311            })
312            .collect::<Vec<serde_json::Value>>();
313
314        let params = json!(
315        {
316            "startLedger": start_ledger,
317            "endLedger": end_ledger,
318            "filters": filters,
319            "pagination": {
320                "cursor": cursor,
321                "limit": limit.into()
322            }
323        }
324        );
325
326        let response = self.client.post("getEvents", params).await?;
327        handle_response(response)
328    }
329
330    /// # Call to RPC method [getFeeStats]
331    ///
332    /// Statistics for charged inclusion fees. The inclusion fee statistics are calculated from
333    /// the inclusion fees that were paid for the transactions to be included onto the ledger. For
334    /// Soroban transactions and Stellar transactions, they each have their own inclusion fees and
335    /// own surge pricing. Inclusion fees are used to prevent spam and prioritize transactions
336    /// during network traffic surge.
337    ///
338    /// [getFeeStats]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getFeeStats
339    ///
340    pub async fn get_fee_stats(&self) -> Result<GetFeeStatsResponse, Error> {
341        let response = self
342            .client
343            .post("getFeeStats", serde_json::Value::Null)
344            .await?;
345        handle_response(response)
346    }
347
348    /// # Call to RPC method [getHealth]
349    ///
350    /// General node health check.
351    ///
352    /// [getHealth]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getHealth
353    ///
354    pub async fn get_health(&self) -> Result<GetHealthResponse, Error> {
355        let response = self
356            .client
357            .post("getHealth", serde_json::Value::Null)
358            .await?;
359        handle_response(response)
360    }
361
362    /// # Call to RPC method [getLatestLedger]
363    ///
364    /// For finding out the current latest known ledger of this node. This is a subset of the
365    /// ledger info from Horizon.
366    ///
367    /// [getLatestLedger]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLatestLedger
368    ///
369    pub async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, Error> {
370        let response = self
371            .client
372            .post("getLatestLedger", serde_json::Value::Null)
373            .await?;
374        handle_response(response)
375    }
376
377    /// # Call to RPC method [getLedgerEntries]
378    ///
379    /// For reading the current value of ledger entries directly.
380    ///
381    /// This method enables the retrieval of various ledger states, such as accounts, trustlines,
382    /// offers, data, claimable balances, and liquidity pools. It also provides direct access to
383    /// inspect a contract's current state, its code, or any other ledger entry. This serves as a
384    /// primary method to access your contract data which may not be available via
385    /// [events][Server::get_events] or
386    /// [simulate_transaction][Server::simulate_transaction].
387    ///
388    /// To fetch contract wasm byte-code, use the ContractCode ledger entry key.
389    ///
390    /// [getLedgerEntries]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgerEntries
391    ///
392    pub async fn get_ledger_entries(
393        &self,
394        keys: Vec<LedgerKey>,
395    ) -> Result<GetLedgerEntriesResponse, Error> {
396        let keys: Result<Vec<String>, Error> = keys
397            .into_iter()
398            .map(|k| k.to_xdr_base64(Limits::none()).map_err(|_| Error::XdrError))
399            .collect();
400
401        match keys {
402            Ok(keys) => {
403                let params = json!({"keys": keys});
404                let response: Response<GetLedgerEntriesResponse> =
405                    self.client.post("getLedgerEntries", params).await?;
406
407                handle_response(response)
408            }
409            Err(err) => Err(err),
410        }
411    }
412
413    /// # Call to RPC method [getLedgers]
414    ///
415    /// The getLedgers method returns a detailed list of ledgers starting from the user specified
416    /// starting point that you can paginate as long as the pages fall within the history
417    /// retention of their corresponding RPC provider.
418    ///
419    /// [getLedgers]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgers
420    pub async fn get_ledgers(
421        &self,
422        ledger: Pagination,
423        limit: impl Into<Option<u32>>,
424    ) -> Result<GetLedgersResponse, Error> {
425        let (start_ledger, cursor) = match ledger {
426            Pagination::From(s) => (Some(s), None),
427            Pagination::FromTo(s, _) => (Some(s), None),
428            Pagination::Cursor(c) => (None, Some(c)),
429        };
430        let params = json!(
431        {
432            "startLedger": start_ledger,
433            "pagination": {
434                "cursor": cursor,
435                "limit": limit.into()
436            }
437        }
438        );
439
440        let response = self.client.post("getLedgers", params).await?;
441        handle_response(response)
442    }
443
444    /// # Call to RPC method [getNetwork]
445    ///
446    /// General information about the currently configured network. This response will contain all
447    /// the information needed to successfully submit transactions to the network this node serves.
448    ///
449    /// [getNetwork]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getNetwork
450    pub async fn get_network(&self) -> Result<GetNetworkResponse, Error> {
451        let response = self
452            .client
453            .post("getNetwork", serde_json::Value::Null)
454            .await?;
455        handle_response(response)
456    }
457
458    /// # Call to RPC method [getTransaction]
459    ///
460    /// The getTransaction method provides details about the specified transaction.
461    ///
462    /// Clients are expected to periodically query this method to ascertain when a transaction has
463    /// been successfully recorded on the blockchain. The stellar-rpc system maintains a restricted
464    /// history of recently processed transactions, with the default retention window set at 24
465    /// hours.
466    ///
467    /// For private soroban-rpc instances, it is possible to modify the retention window
468    /// value by adjusting the transaction-retention-window configuration setting, but we do not
469    /// recommend values longer than 7 days. For debugging needs that extend beyond this timeframe,
470    /// it is advisable to index this data yourself, employ a third-party indexer, or query Hubble
471    /// (our public BigQuery data set).
472    ///
473    /// [getTransaction]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getTransaction
474    ///
475    pub async fn get_transaction(&self, hash: &str) -> Result<GetTransactionResponse, Error> {
476        let params = json!({
477                "hash": hash
478        });
479
480        let response = self.client.post("getTransaction", params).await?;
481        handle_response(response)
482    }
483
484    /// # Call to RPC method [getTransactions]
485    ///
486    /// The getTransactions method return a detailed list of transactions starting from the user
487    /// specified starting point that you can paginate as long as the pages fall within the
488    /// history retention of their corresponding RPC provider.
489    ///
490    /// In [Pagination::FromTo(start, end)], the `end` has no effect for `get_transactions`.
491    ///
492    /// [getTransactions]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getTransactions
493    pub async fn get_transactions(
494        &self,
495        ledger: Pagination,
496        limit: impl Into<Option<u32>>,
497    ) -> Result<GetTransactionsResponse, Error> {
498        let (start_ledger, cursor) = match ledger {
499            Pagination::From(s) => (Some(s), None),
500            Pagination::FromTo(s, _) => (Some(s), None),
501            Pagination::Cursor(c) => (None, Some(c)),
502        };
503        let params = json!(
504        {
505            "startLedger": start_ledger,
506            "pagination": {
507                "cursor": cursor,
508                "limit": limit.into()
509            }
510        }
511        );
512
513        let response = self.client.post("getTransactions", params).await?;
514        handle_response(response)
515    }
516
517    /// # Call to RPC method [getVersionInfo]
518    ///
519    /// Version information about the RPC and Captive core. RPC manages its own, pared-down
520    /// version of Stellar Core optimized for its own subset of needs. we'll refer to this as
521    /// a "Captive Core" instance.
522    ///
523    /// [getVersionInfo]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/getVersionInfo
524    pub async fn get_version_info(&self) -> Result<GetVersionInfoResponse, Error> {
525        let response = self
526            .client
527            .post("getVersionInfo", serde_json::Value::Null)
528            .await?;
529        handle_response(response)
530    }
531
532    /// # Call to RPC method [sendTransaction]
533    ///
534    /// Submit a real transaction to the Stellar network. This is the only way to make changes
535    /// on-chain.
536    ///
537    /// Unlike Horizon, this does not wait for transaction completion. It simply validates and
538    /// enqueues the transaction. Clients should call getTransaction to learn about transaction
539    /// success/failure.
540    ///
541    /// This supports all transactions, not only smart contract-related transactions.
542    ///
543    /// [sendTransaction]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/sendTransaction
544    ///
545    pub async fn send_transaction(
546        &self,
547        transaction: Transaction,
548    ) -> Result<SendTransactionResponse, Error> {
549        let transaction_xdr = transaction
550            .to_envelope()
551            .map_err(|_| Error::TransactionError)?
552            .to_xdr_base64(Limits::none())
553            .map_err(|_| Error::XdrError)?;
554
555        let params = json!({
556                "transaction": transaction_xdr
557            }
558        );
559        let response = self.client.post("sendTransaction", params).await?;
560        handle_response(response)
561    }
562
563    /// # Call to RPC method [simulateTransaction]
564    ///
565    /// Submit a trial contract invocation to simulate how it would be executed by the network.
566    /// This endpoint calculates the effective transaction data, required authorizations, and
567    /// minimal resource fee. It provides a way to test and analyze the potential outcomes of a
568    /// transaction without actually submitting it to the network.
569    ///
570    /// [simulateTransaction]: https://developers.stellar.org/docs/data/rpc/api-reference/methods/simulateTransaction
571    pub async fn simulate_transaction(
572        &self,
573        transaction: &Transaction,
574        options: Option<SimulationOptions>,
575    ) -> Result<SimulateTransactionResponse, Error> {
576        let transaction_xdr = transaction
577            .to_envelope()
578            .map_err(|_| Error::TransactionError)?
579            .to_xdr_base64(Limits::none())
580            .map_err(|_| Error::XdrError)?;
581
582        // Add resource config if provided
583        let params = if let Some(resources) = options {
584            json!({
585                "transaction": transaction_xdr,
586                "resourceConfig": {
587                    "instructionLeeway": resources.cpu_instructions
588                },
589                "authMode": resources.auth_mode.map(|a| {let mode: &str = a.into(); mode}),
590            })
591        } else {
592            json!({
593                "transaction": transaction_xdr
594            })
595        };
596
597        let response = self.client.post("simulateTransaction", params).await?;
598        handle_response(response)
599    }
600
601    // Non-RPC method implementations -------------------------------
602
603    /// # Fetch an [Account] to be used to build a transaction
604    ///
605    /// It uses [Server::get_ledger_entries] to fetch the [LedgerKey::Account]
606    ///
607    pub async fn get_account(&self, address: &str) -> Result<Account, Error> {
608        let account_id = stellar_baselib::keypair::Keypair::from_public_key(address)
609            .map_err(|_| Error::AccountNotFound)?
610            .xdr_account_id();
611        let ledger_key = LedgerKey::Account(LedgerKeyAccount { account_id });
612
613        let resp = self.get_ledger_entries(vec![ledger_key]).await?;
614        let entries = resp.entries.unwrap_or_default();
615        if entries.is_empty() {
616            return Err(Error::AccountNotFound);
617        }
618
619        if let LedgerEntryData::Account(account_entry) = entries[0].to_data() {
620            Ok(Account::new(address, &account_entry.seq_num.0.to_string()).unwrap())
621        } else {
622            Err(Error::AccountNotFound)
623        }
624    }
625
626    /// # Fech the ledger entry specified by the key of the contract
627    ///
628    /// This can be used to inspect the contract state without using [Server::simulate_transaction]
629    /// or to fetch data not available otherwise.
630    ///
631    pub async fn get_contract_data(
632        &self,
633        contract: &str,
634        key: ScVal,
635        durability: Durability,
636    ) -> Result<LedgerEntryResult, Error> {
637        let sc_address = Address::new(contract)
638            .map_err(|_| Error::ContractDataNotFound)?
639            .to_sc_address()
640            .map_err(|_| Error::ContractDataNotFound)?;
641
642        let contract_key = LedgerKey::ContractData(LedgerKeyContractData {
643            key: key.clone(),
644            contract: sc_address,
645            durability: durability.to_xdr(),
646        });
647
648        let val = vec![contract_key];
649
650        let response = self.get_ledger_entries(val).await?;
651
652        if let Some(entries) = response.entries {
653            if let Some(entry) = entries.first() {
654                Ok(entry.clone())
655            } else {
656                Err(Error::ContractDataNotFound)
657            }
658        } else {
659            Err(Error::ContractDataNotFound)
660        }
661    }
662
663    /// # Prepare a transaction to be submited to the network.
664    ///
665    /// If the transaction simulation is successful, a new transaction is built using the returned
666    /// footprint and authorizations.
667    ///
668    /// The fees are adapted based on the initial fees and the contract resource fees estimated
669    /// from the simulation.
670    ///
671    /// If the simulation returns a restore preamble, this method will return a [Error::RestorationRequired].
672    /// This error should be used to build a
673    /// [stellar_baselib::xdr::OperationBody::RestoreFootprint]
674    ///
675    pub async fn prepare_transaction(
676        &self,
677        transaction: &Transaction,
678    ) -> Result<Transaction, Error> {
679        let sim_response = self.simulate_transaction(transaction, None).await?;
680
681        assemble_transaction(transaction, sim_response)
682    }
683
684    /// # Fund the account using the network's [friendbot] faucet (testnet)
685    ///
686    /// The friendbot URL is retrieved first from the [Options::friendbot_url] if provided
687    /// or from the [Server::get_network] method. There is no friendbot faucet on mainnet.
688    ///
689    /// [friendbot]: https://developers.stellar.org/docs/learn/fundamentals/networks#friendbot
690    pub async fn request_airdrop(&self, account_id: &str) -> Result<Account, Error> {
691        let friendbot_url = if let Some(url) = self.friendbot_url.clone() {
692            url
693        } else {
694            let network = self.get_network().await?;
695            if let Some(url) = network.friendbot_url {
696                url
697            } else {
698                return Err(Error::NoFriendbot);
699            }
700        };
701
702        let client = reqwest::ClientBuilder::new()
703            .build()
704            .map_err(Error::NetworkError)?;
705
706        let response = client
707            .get(friendbot_url + "?addr=" + account_id)
708            .send()
709            .map_err(Error::NetworkError)
710            .await?;
711
712        let data: friendbot::FriendbotResponse =
713            response.json().map_err(Error::NetworkError).await?;
714
715        if let Some(success) = data.successful {
716            if success {
717                self.get_account(account_id).await
718            } else {
719                Err(Error::AccountNotFound)
720            }
721        } else {
722            // If we don't get a success, it can be already funded
723            self.get_account(account_id).await
724        }
725    }
726
727    /// # Wait for a transaction to become either Success or Failed
728    ///
729    /// Wait for the transaction referenced by the given `hash` for at most `max_wait` duration.
730    /// The function will loop with an exponential delay between each call to
731    /// [Server::get_transaction] method.
732    ///
733    /// If an error occurs you can get the last result of [Server::get_transaction] with the
734    /// [Error].
735    pub async fn wait_transaction(
736        &self,
737        hash: &str,
738        max_wait: Duration,
739    ) -> Result<GetTransactionResponse, (Error, Option<GetTransactionResponse>)> {
740        let mut delay = Duration::from_secs(1);
741        let start = Instant::now();
742        let mut last_response: Option<GetTransactionResponse> = None;
743
744        while start.elapsed() < max_wait {
745            match self.get_transaction(hash).await {
746                Ok(tx) => match tx.status {
747                    TransactionStatus::Success | TransactionStatus::Failed => {
748                        return Ok(tx);
749                    }
750                    TransactionStatus::NotFound => {
751                        last_response = Some(tx);
752                        sleep(delay).await;
753                        delay = std::cmp::min(delay * 2, Duration::from_secs(60));
754                    }
755                },
756                Err(e) => {
757                    return Err((e, last_response));
758                }
759            }
760        }
761        Err((
762            Error::WaitTransactionTimeout(max_wait.as_secs(), start.elapsed().as_secs()),
763            last_response,
764        ))
765    }
766}
767
768fn handle_response<T>(response: Response<T>) -> Result<T, Error> {
769    if let Some(result) = response.result {
770        Ok(result)
771    } else if let Some(error) = response.error {
772        Err(Error::RPCError {
773            code: error.code,
774            message: error.message.unwrap_or_default(),
775        })
776    } else {
777        Err(Error::UnexpectedError)
778    }
779}
780
781#[cfg(test)]
782mod test {}