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 {}