1use http::{uri::Authority, Uri};
2use itertools::Itertools;
3use jsonrpsee_core::params::ObjectParams;
4use jsonrpsee_core::{self, client::ClientT};
5use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder};
6use serde_aux::prelude::{
7 deserialize_default_from_null, deserialize_number_from_string,
8 deserialize_option_number_from_string,
9};
10use serde_with::{serde_as, DisplayFromStr};
11use stellar_xdr::curr::{
12 self as xdr, AccountEntry, AccountId, ContractDataEntry, ContractEventType, DiagnosticEvent,
13 Error as XdrError, Hash, LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount,
14 Limited, Limits, PublicKey, ReadXdr, ScContractInstance, SorobanAuthorizationEntry,
15 SorobanResources, SorobanTransactionData, TransactionEnvelope, TransactionMeta,
16 TransactionMetaV3, TransactionResult, Uint256, VecM, WriteXdr,
17};
18
19use std::{
20 f64::consts::E,
21 fmt::Display,
22 str::FromStr,
23 sync::Arc,
24 time::{Duration, Instant},
25};
26
27use termcolor::{Color, ColorChoice, StandardStream, WriteColor};
28use termcolor_output::colored;
29use tokio::time::sleep;
30
31const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
32
33pub type LogEvents = fn(
34 footprint: &LedgerFootprint,
35 auth: &[VecM<SorobanAuthorizationEntry>],
36 events: &[DiagnosticEvent],
37) -> ();
38
39pub type LogResources = fn(resources: &SorobanResources) -> ();
40
41#[derive(thiserror::Error, Debug)]
42#[allow(deprecated)] pub enum Error {
44 #[error(transparent)]
45 InvalidAddress(#[from] stellar_strkey::DecodeError),
46 #[error("invalid response from server")]
47 InvalidResponse,
48 #[error("provided network passphrase {expected:?} does not match the server: {server:?}")]
49 InvalidNetworkPassphrase { expected: String, server: String },
50 #[error("xdr processing error: {0}")]
51 Xdr(#[from] XdrError),
52 #[error("invalid rpc url: {0}")]
53 InvalidRpcUrl(http::uri::InvalidUri),
54 #[error("invalid rpc url: {0}")]
55 InvalidRpcUrlFromUriParts(http::uri::InvalidUriParts),
56 #[error("invalid friendbot url: {0}")]
57 InvalidUrl(String),
58 #[error(transparent)]
59 JsonRpc(#[from] jsonrpsee_core::Error),
60 #[error("json decoding error: {0}")]
61 Serde(#[from] serde_json::Error),
62 #[error("transaction failed: {0}")]
63 TransactionFailed(String),
64 #[error("transaction submission failed: {0}")]
65 TransactionSubmissionFailed(String),
66 #[error("expected transaction status: {0}")]
67 UnexpectedTransactionStatus(String),
68 #[error("transaction submission timeout")]
69 TransactionSubmissionTimeout,
70 #[error("transaction simulation failed: {0}")]
71 TransactionSimulationFailed(String),
72 #[error("{0} not found: {1}")]
73 NotFound(String, String),
74 #[error("Missing result in successful response")]
75 MissingResult,
76 #[error("Failed to read Error response from server")]
77 MissingError,
78 #[error("Missing signing key for account {address}")]
79 MissingSignerForAddress { address: String },
80 #[error("cursor is not valid")]
81 InvalidCursor,
82 #[error("unexpected ({length}) simulate transaction result length")]
83 UnexpectedSimulateTransactionResultSize { length: usize },
84 #[error("unexpected ({count}) number of operations")]
85 UnexpectedOperationCount { count: usize },
86 #[error("Transaction contains unsupported operation type")]
87 UnsupportedOperationType,
88 #[error("unexpected contract code data type: {0:?}")]
89 UnexpectedContractCodeDataType(LedgerEntryData),
90 #[error("unexpected contract instance type: {0:?}")]
91 UnexpectedContractInstance(xdr::ScVal),
92 #[error("unexpected contract code got token {0:?}")]
93 #[deprecated(note = "To be removed in future versions")]
94 UnexpectedToken(ContractDataEntry),
95 #[error("Fee was too large {0}")]
96 LargeFee(u64),
97 #[error("Cannot authorize raw transactions")]
98 CannotAuthorizeRawTransaction,
99 #[error("Missing result for tnx")]
100 MissingOp,
101}
102
103#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
104pub struct SendTransactionResponse {
105 pub hash: String,
106 pub status: String,
107 #[serde(
108 rename = "errorResultXdr",
109 skip_serializing_if = "Option::is_none",
110 default
111 )]
112 pub error_result_xdr: Option<String>,
113 #[serde(rename = "latestLedger")]
114 pub latest_ledger: u32,
115 #[serde(
116 rename = "latestLedgerCloseTime",
117 deserialize_with = "deserialize_number_from_string"
118 )]
119 pub latest_ledger_close_time: u32,
120}
121
122#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
123pub struct GetTransactionResponseRaw {
124 pub status: String,
125 #[serde(
126 rename = "envelopeXdr",
127 skip_serializing_if = "Option::is_none",
128 default
129 )]
130 pub envelope_xdr: Option<String>,
131 #[serde(rename = "resultXdr", skip_serializing_if = "Option::is_none", default)]
132 pub result_xdr: Option<String>,
133 #[serde(
134 rename = "resultMetaXdr",
135 skip_serializing_if = "Option::is_none",
136 default
137 )]
138 pub result_meta_xdr: Option<String>,
139 }
141
142#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
143pub struct GetTransactionResponse {
144 pub status: String,
145 pub envelope: Option<xdr::TransactionEnvelope>,
146 pub result: Option<xdr::TransactionResult>,
147 pub result_meta: Option<xdr::TransactionMeta>,
148}
149
150impl TryInto<GetTransactionResponse> for GetTransactionResponseRaw {
151 type Error = xdr::Error;
152
153 fn try_into(self) -> Result<GetTransactionResponse, Self::Error> {
154 Ok(GetTransactionResponse {
155 status: self.status,
156 envelope: self
157 .envelope_xdr
158 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
159 .transpose()?,
160 result: self
161 .result_xdr
162 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
163 .transpose()?,
164 result_meta: self
165 .result_meta_xdr
166 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
167 .transpose()?,
168 })
169 }
170}
171
172impl GetTransactionResponse {
173 pub fn return_value(&self) -> Result<xdr::ScVal, Error> {
176 if let Some(xdr::TransactionMeta::V3(xdr::TransactionMetaV3 {
177 soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }),
178 ..
179 })) = &self.result_meta
180 {
181 Ok(return_value.clone())
182 } else {
183 Err(Error::MissingOp)
184 }
185 }
186
187 pub fn events(&self) -> Result<Vec<DiagnosticEvent>, Error> {
190 self.result_meta
191 .as_ref()
192 .map(extract_events)
193 .ok_or(Error::MissingOp)
194 }
195
196 pub fn contract_events(&self) -> Result<Vec<DiagnosticEvent>, Error> {
199 Ok(self
200 .events()?
201 .into_iter()
202 .filter(|e| matches!(e.event.type_, ContractEventType::Contract))
203 .collect::<Vec<_>>())
204 }
205}
206
207#[serde_as]
208#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
209pub struct GetTransactionsResponseRaw {
210 pub transactions: Vec<GetTransactionResponseRaw>,
211 #[serde(rename = "latestLedger")]
212 pub latest_ledger: u32,
213 #[serde(rename = "latestLedgerCloseTimestamp")]
214 pub latest_ledger_close_time: i64,
215 #[serde(rename = "oldestLedger")]
216 pub oldest_ledger: u32,
217 #[serde(rename = "oldestLedgerCloseTimestamp")]
218 pub oldest_ledger_close_time: i64,
219 #[serde_as(as = "DisplayFromStr")]
220 pub cursor: u64,
221}
222#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
223pub struct GetTransactionsResponse {
224 pub transactions: Vec<GetTransactionResponse>,
225 pub latest_ledger: u32,
226 pub latest_ledger_close_time: i64,
227 pub oldest_ledger: u32,
228 pub oldest_ledger_close_time: i64,
229 pub cursor: u64,
230}
231impl TryInto<GetTransactionsResponse> for GetTransactionsResponseRaw {
232 type Error = xdr::Error; fn try_into(self) -> Result<GetTransactionsResponse, Self::Error> {
235 Ok(GetTransactionsResponse {
236 transactions: self
237 .transactions
238 .into_iter()
239 .map(TryInto::try_into)
240 .collect::<Result<Vec<_>, xdr::Error>>()?,
241 latest_ledger: self.latest_ledger,
242 latest_ledger_close_time: self.latest_ledger_close_time,
243 oldest_ledger: self.oldest_ledger,
244 oldest_ledger_close_time: self.oldest_ledger_close_time,
245 cursor: self.cursor,
246 })
247 }
248}
249
250#[serde_as]
251#[derive(serde::Serialize, Debug, Clone)]
252pub struct TransactionsPaginationOptions {
253 #[serde_as(as = "Option<DisplayFromStr>")]
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub cursor: Option<u64>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub limit: Option<u32>,
258}
259
260#[derive(serde::Serialize, Debug, Clone)]
261pub struct GetTransactionsRequest {
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub start_ledger: Option<u32>,
264 pub pagination: Option<TransactionsPaginationOptions>,
265}
266
267#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
268pub struct LedgerEntryResult {
269 pub key: String,
270 pub xdr: String,
271 #[serde(rename = "lastModifiedLedgerSeq")]
272 pub last_modified_ledger: u32,
273 #[serde(
274 rename = "liveUntilLedgerSeq",
275 skip_serializing_if = "Option::is_none",
276 deserialize_with = "deserialize_option_number_from_string",
277 default
278 )]
279 pub live_until_ledger_seq_ledger_seq: Option<u32>,
280}
281
282#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
283pub struct GetLedgerEntriesResponse {
284 pub entries: Option<Vec<LedgerEntryResult>>,
285 #[serde(rename = "latestLedger")]
286 pub latest_ledger: i64,
287}
288
289#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
290pub struct GetNetworkResponse {
291 #[serde(
292 rename = "friendbotUrl",
293 skip_serializing_if = "Option::is_none",
294 default
295 )]
296 pub friendbot_url: Option<String>,
297 pub passphrase: String,
298 #[serde(rename = "protocolVersion")]
299 pub protocol_version: u32,
300}
301
302#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
303pub struct GetLatestLedgerResponse {
304 pub id: String,
305 #[serde(rename = "protocolVersion")]
306 pub protocol_version: u32,
307 pub sequence: u32,
308}
309
310#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
311pub struct Cost {
312 #[serde(
313 rename = "cpuInsns",
314 deserialize_with = "deserialize_number_from_string"
315 )]
316 pub cpu_insns: u64,
317 #[serde(
318 rename = "memBytes",
319 deserialize_with = "deserialize_number_from_string"
320 )]
321 pub mem_bytes: u64,
322}
323
324#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
325pub struct SimulateHostFunctionResultRaw {
326 #[serde(deserialize_with = "deserialize_default_from_null")]
327 pub auth: Vec<String>,
328 pub xdr: String,
329}
330
331#[derive(Debug, Clone)]
332pub struct SimulateHostFunctionResult {
333 pub auth: Vec<SorobanAuthorizationEntry>,
334 pub xdr: xdr::ScVal,
335}
336
337#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
338#[serde(tag = "type")]
339pub enum LedgerEntryChange {
340 #[serde(rename = "created")]
341 Created { key: String, after: String },
342 #[serde(rename = "deleted")]
343 Deleted { key: String, before: String },
344 #[serde(rename = "updated")]
345 Updated {
346 key: String,
347 before: String,
348 after: String,
349 },
350}
351
352#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
353pub struct SimulateTransactionResponse {
354 #[serde(
355 rename = "minResourceFee",
356 deserialize_with = "deserialize_number_from_string",
357 default
358 )]
359 pub min_resource_fee: u64,
360 #[serde(default)]
361 pub cost: Cost,
362 #[serde(skip_serializing_if = "Vec::is_empty", default)]
363 pub results: Vec<SimulateHostFunctionResultRaw>,
364 #[serde(rename = "transactionData", default)]
365 pub transaction_data: String,
366 #[serde(
367 deserialize_with = "deserialize_default_from_null",
368 skip_serializing_if = "Vec::is_empty",
369 default
370 )]
371 pub events: Vec<String>,
372 #[serde(
373 rename = "restorePreamble",
374 skip_serializing_if = "Option::is_none",
375 default
376 )]
377 pub restore_preamble: Option<RestorePreamble>,
378 #[serde(
379 rename = "stateChanges",
380 skip_serializing_if = "Option::is_none",
381 default
382 )]
383 pub state_changes: Option<Vec<LedgerEntryChange>>,
384 #[serde(rename = "latestLedger")]
385 pub latest_ledger: u32,
386 #[serde(skip_serializing_if = "Option::is_none", default)]
387 pub error: Option<String>,
388}
389
390impl SimulateTransactionResponse {
391 pub fn results(&self) -> Result<Vec<SimulateHostFunctionResult>, Error> {
394 self.results
395 .iter()
396 .map(|r| {
397 Ok(SimulateHostFunctionResult {
398 auth: r
399 .auth
400 .iter()
401 .map(|a| {
402 Ok(SorobanAuthorizationEntry::from_xdr_base64(
403 a,
404 Limits::none(),
405 )?)
406 })
407 .collect::<Result<_, Error>>()?,
408 xdr: xdr::ScVal::from_xdr_base64(&r.xdr, Limits::none())?,
409 })
410 })
411 .collect()
412 }
413
414 pub fn events(&self) -> Result<Vec<DiagnosticEvent>, Error> {
417 self.events
418 .iter()
419 .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e, Limits::none())?))
420 .collect()
421 }
422
423 pub fn transaction_data(&self) -> Result<SorobanTransactionData, Error> {
426 Ok(SorobanTransactionData::from_xdr_base64(
427 &self.transaction_data,
428 Limits::none(),
429 )?)
430 }
431}
432
433#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
434pub struct RestorePreamble {
435 #[serde(rename = "transactionData")]
436 pub transaction_data: String,
437 #[serde(
438 rename = "minResourceFee",
439 deserialize_with = "deserialize_number_from_string"
440 )]
441 pub min_resource_fee: u64,
442}
443
444#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
445pub struct GetEventsResponse {
446 #[serde(deserialize_with = "deserialize_default_from_null")]
447 pub events: Vec<Event>,
448 #[serde(rename = "latestLedger")]
449 pub latest_ledger: u32,
450}
451
452#[must_use]
468pub fn does_topic_match(topic: &[String], filter: &[String]) -> bool {
469 filter.len() == topic.len()
470 && filter
471 .iter()
472 .enumerate()
473 .all(|(i, s)| *s == "*" || topic[i] == *s)
474}
475
476#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
477pub struct Event {
478 #[serde(rename = "type")]
479 pub event_type: String,
480
481 pub ledger: u32,
482 #[serde(rename = "ledgerClosedAt")]
483 pub ledger_closed_at: String,
484
485 pub id: String,
486 #[serde(rename = "pagingToken")]
487 pub paging_token: String,
488
489 #[serde(rename = "contractId")]
490 pub contract_id: String,
491 pub topic: Vec<String>,
492 pub value: String,
493}
494
495impl Display for Event {
496 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497 writeln!(
498 f,
499 "Event {} [{}]:",
500 self.paging_token,
501 self.event_type.to_ascii_uppercase()
502 )?;
503 writeln!(
504 f,
505 " Ledger: {} (closed at {})",
506 self.ledger, self.ledger_closed_at
507 )?;
508 writeln!(f, " Contract: {}", self.contract_id)?;
509 writeln!(f, " Topics:")?;
510 for topic in &self.topic {
511 let scval =
512 xdr::ScVal::from_xdr_base64(topic, Limits::none()).map_err(|_| std::fmt::Error)?;
513 writeln!(f, " {scval:?}")?;
514 }
515 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())
516 .map_err(|_| std::fmt::Error)?;
517 writeln!(f, " Value: {scval:?}")
518 }
519}
520
521impl Event {
522 pub fn parse_cursor(&self) -> Result<(u64, i32), Error> {
525 parse_cursor(&self.id)
526 }
527 pub fn pretty_print(&self) -> Result<(), Box<dyn std::error::Error>> {
530 let mut stdout = StandardStream::stdout(ColorChoice::Auto);
531 if !stdout.supports_color() {
532 println!("{self}");
533 return Ok(());
534 }
535
536 let color = match self.event_type.as_str() {
537 "system" => Color::Yellow,
538 _ => Color::Blue,
539 };
540 colored!(
541 stdout,
542 "{}Event{} {}{}{} [{}{}{}{}]:\n",
543 bold!(true),
544 bold!(false),
545 fg!(Some(Color::Green)),
546 self.paging_token,
547 reset!(),
548 bold!(true),
549 fg!(Some(color)),
550 self.event_type.to_ascii_uppercase(),
551 reset!(),
552 )?;
553
554 colored!(
555 stdout,
556 " Ledger: {}{}{} (closed at {}{}{})\n",
557 fg!(Some(Color::Green)),
558 self.ledger,
559 reset!(),
560 fg!(Some(Color::Green)),
561 self.ledger_closed_at,
562 reset!(),
563 )?;
564
565 colored!(
566 stdout,
567 " Contract: {}{}{}\n",
568 fg!(Some(Color::Green)),
569 self.contract_id,
570 reset!(),
571 )?;
572
573 colored!(stdout, " Topics:\n")?;
574 for topic in &self.topic {
575 let scval = xdr::ScVal::from_xdr_base64(topic, Limits::none())?;
576 colored!(
577 stdout,
578 " {}{:?}{}\n",
579 fg!(Some(Color::Green)),
580 scval,
581 reset!(),
582 )?;
583 }
584
585 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())?;
586 colored!(
587 stdout,
588 " Value: {}{:?}{}\n",
589 fg!(Some(Color::Green)),
590 scval,
591 reset!(),
592 )?;
593
594 Ok(())
595 }
596}
597
598#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
599pub enum EventType {
600 All,
601 Contract,
602 System,
603}
604
605#[derive(Clone, Debug, Eq, Hash, PartialEq)]
606pub enum EventStart {
607 Ledger(u32),
608 Cursor(String),
609}
610
611#[derive(Debug, Clone)]
612pub struct FullLedgerEntry {
613 pub key: LedgerKey,
614 pub val: LedgerEntryData,
615 pub last_modified_ledger: u32,
616 pub live_until_ledger_seq: u32,
617}
618
619#[derive(Debug, Clone)]
620pub struct FullLedgerEntries {
621 pub entries: Vec<FullLedgerEntry>,
622 pub latest_ledger: i64,
623}
624
625#[derive(Debug, Clone)]
626pub struct Client {
627 base_url: Arc<str>,
628 timeout_in_secs: u64,
629 http_client: Arc<HttpClient>,
630}
631
632#[allow(deprecated)] impl Client {
634 pub fn new(base_url: &str) -> Result<Self, Error> {
637 let uri = base_url.parse::<Uri>().map_err(Error::InvalidRpcUrl)?;
642 let mut parts = uri.into_parts();
643 if let (Some(scheme), Some(authority)) = (&parts.scheme, &parts.authority) {
644 if authority.port().is_none() {
645 let port = match scheme.as_str() {
646 "http" => Some(80),
647 "https" => Some(443),
648 _ => None,
649 };
650 if let Some(port) = port {
651 let host = authority.host();
652 parts.authority = Some(
653 Authority::from_str(&format!("{host}:{port}"))
654 .map_err(Error::InvalidRpcUrl)?,
655 );
656 }
657 }
658 }
659 let uri = Uri::from_parts(parts).map_err(Error::InvalidRpcUrlFromUriParts)?;
660 let base_url = Arc::from(uri.to_string());
661 tracing::trace!(?uri);
662 let headers = Self::default_http_headers();
663 let http_client = Arc::new(
664 HttpClientBuilder::default()
665 .set_headers(headers)
666 .build(&base_url)?,
667 );
668 Ok(Self {
669 base_url,
670 timeout_in_secs: 30,
671 http_client,
672 })
673 }
674
675 pub fn new_with_timeout(base_url: &str, timeout: u64) -> Result<Self, Error> {
678 let mut client = Self::new(base_url)?;
679 client.timeout_in_secs = timeout;
680 Ok(client)
681 }
682
683 pub fn new_with_headers(base_url: &str, additional_headers: HeaderMap) -> Result<Self, Error> {
686 let mut client = Self::new(base_url)?;
687 let mut headers = Self::default_http_headers();
688
689 for (key, value) in additional_headers {
690 headers.insert(key.ok_or(Error::InvalidResponse)?, value);
691 }
692 let http_client = Arc::new(
693 HttpClientBuilder::default()
694 .set_headers(headers)
695 .build(base_url)?,
696 );
697
698 client.http_client = http_client;
699 Ok(client)
700 }
701
702 fn default_http_headers() -> HeaderMap {
703 let mut headers = HeaderMap::new();
704 headers.insert("X-Client-Name", unsafe {
705 "rs-stellar-rpc-client".parse().unwrap_unchecked()
706 });
707 let version = VERSION.unwrap_or("devel");
708 headers.insert("X-Client-Version", unsafe {
709 version.parse().unwrap_unchecked()
710 });
711 headers
712 }
713
714 #[must_use]
715 pub fn base_url(&self) -> &str {
716 &self.base_url
717 }
718
719 #[must_use]
720 pub fn client(&self) -> &HttpClient {
721 &self.http_client
722 }
723
724 pub async fn friendbot_url(&self) -> Result<String, Error> {
727 let network = self.get_network().await?;
728 tracing::trace!("{network:#?}");
729 network.friendbot_url.ok_or_else(|| {
730 Error::NotFound(
731 "Friendbot".to_string(),
732 "Friendbot is not available on this network".to_string(),
733 )
734 })
735 }
736 pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result<String, Error> {
739 let server = self.get_network().await?.passphrase;
740 if let Some(expected) = expected {
741 if expected != server {
742 return Err(Error::InvalidNetworkPassphrase {
743 expected: expected.to_string(),
744 server,
745 });
746 }
747 }
748 Ok(server)
749 }
750
751 pub async fn get_network(&self) -> Result<GetNetworkResponse, Error> {
754 tracing::trace!("Getting network");
755 Ok(self
756 .client()
757 .request("getNetwork", ObjectParams::new())
758 .await?)
759 }
760
761 pub async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, Error> {
764 tracing::trace!("Getting latest ledger");
765 Ok(self
766 .client()
767 .request("getLatestLedger", ObjectParams::new())
768 .await?)
769 }
770
771 pub async fn get_account(&self, address: &str) -> Result<AccountEntry, Error> {
774 tracing::trace!("Getting address {}", address);
775 let key = LedgerKey::Account(LedgerKeyAccount {
776 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
777 stellar_strkey::ed25519::PublicKey::from_string(address)?.0,
778 ))),
779 });
780 let keys = Vec::from([key]);
781 let response = self.get_ledger_entries(&keys).await?;
782 let entries = response.entries.unwrap_or_default();
783 if entries.is_empty() {
784 return Err(Error::NotFound("Account".to_string(), address.to_owned()));
785 }
786 let ledger_entry = &entries[0];
787 let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none());
788 if let LedgerEntryData::Account(entry) = LedgerEntryData::read_xdr_base64(&mut read)? {
789 tracing::trace!(account=?entry);
790 Ok(entry)
791 } else {
792 Err(Error::InvalidResponse)
793 }
794 }
795
796 pub async fn send_transaction(&self, tx: &TransactionEnvelope) -> Result<Hash, Error> {
799 tracing::trace!("Sending:\n{tx:#?}");
800 let mut oparams = ObjectParams::new();
801 oparams.insert("transaction", tx.to_xdr_base64(Limits::none())?)?;
802 let SendTransactionResponse {
803 hash,
804 error_result_xdr,
805 status,
806 ..
807 } = self
808 .client()
809 .request("sendTransaction", oparams)
810 .await
811 .map_err(|err| {
812 Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}"))
813 })?;
814
815 if status == "ERROR" {
816 let error = error_result_xdr
817 .ok_or(Error::MissingError)
818 .and_then(|x| {
819 TransactionResult::read_xdr_base64(&mut Limited::new(
820 x.as_bytes(),
821 Limits::none(),
822 ))
823 .map_err(|_| Error::InvalidResponse)
824 })
825 .map(|r| r.result);
826 tracing::error!("TXN {hash} failed:\n {error:#?}");
827 return Err(Error::TransactionSubmissionFailed(format!("{:#?}", error?)));
828 }
829 Ok(Hash::from_str(&hash)?)
830 }
831
832 pub async fn send_transaction_polling(
835 &self,
836 tx: &TransactionEnvelope,
837 ) -> Result<GetTransactionResponse, Error> {
838 let hash = self.send_transaction(tx).await?;
839 self.get_transaction_polling(&hash, None).await
840 }
841
842 pub async fn simulate_transaction_envelope(
845 &self,
846 tx: &TransactionEnvelope,
847 ) -> Result<SimulateTransactionResponse, Error> {
848 tracing::trace!("Simulating:\n{tx:#?}");
849 let base64_tx = tx.to_xdr_base64(Limits::none())?;
850 let mut oparams = ObjectParams::new();
851 oparams.insert("transaction", base64_tx)?;
852 let sim_res = self
853 .client()
854 .request("simulateTransaction", oparams)
855 .await?;
856 tracing::trace!("Simulation response:\n {sim_res:#?}");
857 Ok(sim_res)
858 }
859
860 pub async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, Error> {
863 let mut oparams = ObjectParams::new();
864 oparams.insert("hash", tx_id)?;
865 let resp: GetTransactionResponseRaw =
866 self.client().request("getTransaction", oparams).await?;
867 Ok(resp.try_into()?)
868 }
869
870 pub async fn get_transactions(
873 &self,
874 request: GetTransactionsRequest,
875 ) -> Result<GetTransactionsResponse, Error> {
876 let mut oparams = ObjectParams::new();
877 if let Some(start_ledger) = request.start_ledger {
878 oparams.insert("startLedger", start_ledger)?;
879 }
880 if let Some(pagination_params) = request.pagination {
881 let pagination = serde_json::json!(pagination_params);
882 oparams.insert("pagination", pagination)?;
883 }
884 let resp: GetTransactionsResponseRaw =
885 self.client().request("getTransactions", oparams).await?;
886 Ok(resp.try_into()?)
887 }
888
889 pub async fn get_transaction_polling(
899 &self,
900 tx_id: &Hash,
901 timeout_s: Option<Duration>,
902 ) -> Result<GetTransactionResponse, Error> {
903 let start = Instant::now();
905 let timeout = timeout_s.unwrap_or(Duration::from_secs(self.timeout_in_secs));
906 let exponential_backoff: f64 = 1.0 / (1.0 - E.powf(-1.0));
909 let mut sleep_time = Duration::from_secs(1);
910 loop {
911 let response = self.get_transaction(tx_id).await?;
912 match response.status.as_str() {
913 "SUCCESS" => {
914 tracing::trace!("{response:#?}");
916 return Ok(response);
917 }
918 "FAILED" => {
919 tracing::error!("{response:#?}");
920 return Err(Error::TransactionSubmissionFailed(format!(
922 "{:#?}",
923 response.result
924 )));
925 }
926 "NOT_FOUND" => (),
927 _ => {
928 return Err(Error::UnexpectedTransactionStatus(response.status));
929 }
930 };
931 if start.elapsed() > timeout {
932 return Err(Error::TransactionSubmissionTimeout);
933 }
934 sleep(sleep_time).await;
935 sleep_time = Duration::from_secs_f64(sleep_time.as_secs_f64() * exponential_backoff);
936 }
937 }
938
939 pub async fn get_ledger_entries(
942 &self,
943 keys: &[LedgerKey],
944 ) -> Result<GetLedgerEntriesResponse, Error> {
945 let mut base64_keys: Vec<String> = vec![];
946 for k in keys {
947 let base64_result = k.to_xdr_base64(Limits::none());
948 if base64_result.is_err() {
949 return Err(Error::Xdr(XdrError::Invalid));
950 }
951 base64_keys.push(k.to_xdr_base64(Limits::none())?);
952 }
953 let mut oparams = ObjectParams::new();
954 oparams.insert("keys", base64_keys)?;
955 Ok(self.client().request("getLedgerEntries", oparams).await?)
956 }
957
958 pub async fn get_full_ledger_entries(
961 &self,
962 ledger_keys: &[LedgerKey],
963 ) -> Result<FullLedgerEntries, Error> {
964 let keys = ledger_keys
965 .iter()
966 .filter(|key| !matches!(key, LedgerKey::Ttl(_)))
967 .map(Clone::clone)
968 .collect::<Vec<_>>();
969 tracing::trace!("keys: {keys:#?}");
970 let GetLedgerEntriesResponse {
971 entries,
972 latest_ledger,
973 } = self.get_ledger_entries(&keys).await?;
974 tracing::trace!("raw: {entries:#?}");
975 let entries = entries
976 .unwrap_or_default()
977 .iter()
978 .map(
979 |LedgerEntryResult {
980 key,
981 xdr,
982 last_modified_ledger,
983 live_until_ledger_seq_ledger_seq,
984 }| {
985 Ok(FullLedgerEntry {
986 key: LedgerKey::from_xdr_base64(key, Limits::none())?,
987 val: LedgerEntryData::from_xdr_base64(xdr, Limits::none())?,
988 live_until_ledger_seq: live_until_ledger_seq_ledger_seq.unwrap_or_default(),
989 last_modified_ledger: *last_modified_ledger,
990 })
991 },
992 )
993 .collect::<Result<Vec<_>, Error>>()?;
994 tracing::trace!("parsed: {entries:#?}");
995 Ok(FullLedgerEntries {
996 entries,
997 latest_ledger,
998 })
999 }
1000 pub async fn get_events(
1003 &self,
1004 start: EventStart,
1005 event_type: Option<EventType>,
1006 contract_ids: &[String],
1007 topics: &[String],
1008 limit: Option<usize>,
1009 ) -> Result<GetEventsResponse, Error> {
1010 let mut filters = serde_json::Map::new();
1011
1012 event_type
1013 .and_then(|t| match t {
1014 EventType::All => None, EventType::Contract => Some("contract"),
1016 EventType::System => Some("system"),
1017 })
1018 .map(|t| filters.insert("type".to_string(), t.into()));
1019
1020 filters.insert("topics".to_string(), topics.into());
1021 filters.insert("contractIds".to_string(), contract_ids.into());
1022
1023 let mut pagination = serde_json::Map::new();
1024 if let Some(limit) = limit {
1025 pagination.insert("limit".to_string(), limit.into());
1026 }
1027
1028 let mut oparams = ObjectParams::new();
1029 match start {
1030 EventStart::Ledger(l) => oparams.insert("startLedger", l)?,
1031 EventStart::Cursor(c) => {
1032 pagination.insert("cursor".to_string(), c.into());
1033 }
1034 };
1035 oparams.insert("filters", vec![filters])?;
1036 oparams.insert("pagination", pagination)?;
1037
1038 Ok(self.client().request("getEvents", oparams).await?)
1039 }
1040
1041 pub async fn get_contract_data(
1044 &self,
1045 contract_id: &[u8; 32],
1046 ) -> Result<ContractDataEntry, Error> {
1047 let contract_key = LedgerKey::ContractData(xdr::LedgerKeyContractData {
1049 contract: xdr::ScAddress::Contract(xdr::Hash(*contract_id)),
1050 key: xdr::ScVal::LedgerKeyContractInstance,
1051 durability: xdr::ContractDataDurability::Persistent,
1052 });
1053 let contract_ref = self.get_ledger_entries(&[contract_key]).await?;
1054 let entries = contract_ref.entries.unwrap_or_default();
1055 if entries.is_empty() {
1056 let contract_address = stellar_strkey::Contract(*contract_id).to_string();
1057 return Err(Error::NotFound("Contract".to_string(), contract_address));
1058 }
1059 let contract_ref_entry = &entries[0];
1060 match LedgerEntryData::from_xdr_base64(&contract_ref_entry.xdr, Limits::none())? {
1061 LedgerEntryData::ContractData(contract_data) => Ok(contract_data),
1062 scval => Err(Error::UnexpectedContractCodeDataType(scval)),
1063 }
1064 }
1065
1066 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1069 pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result<Vec<u8>, Error> {
1070 match self.get_contract_data(contract_id).await? {
1071 xdr::ContractDataEntry {
1072 val:
1073 xdr::ScVal::ContractInstance(xdr::ScContractInstance {
1074 executable: xdr::ContractExecutable::Wasm(hash),
1075 ..
1076 }),
1077 ..
1078 } => self.get_remote_wasm_from_hash(hash).await,
1079 scval => Err(Error::UnexpectedToken(scval)),
1080 }
1081 }
1082
1083 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1086 pub async fn get_remote_wasm_from_hash(&self, hash: Hash) -> Result<Vec<u8>, Error> {
1087 let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
1088 let contract_data = self.get_ledger_entries(&[code_key]).await?;
1089 let entries = contract_data.entries.unwrap_or_default();
1090 if entries.is_empty() {
1091 return Err(Error::NotFound(
1092 "Contract Code".to_string(),
1093 hex::encode(hash),
1094 ));
1095 }
1096 let contract_data_entry = &entries[0];
1097 match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
1098 LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
1099 scval => Err(Error::UnexpectedContractCodeDataType(scval)),
1100 }
1101 }
1102
1103 pub async fn get_contract_instance(
1108 &self,
1109 contract_id: &[u8; 32],
1110 ) -> Result<ScContractInstance, Error> {
1111 let contract_data = self.get_contract_data(contract_id).await?;
1112 match contract_data.val {
1113 xdr::ScVal::ContractInstance(instance) => Ok(instance),
1114 scval => Err(Error::UnexpectedContractInstance(scval)),
1115 }
1116 }
1117}
1118
1119fn extract_events(tx_meta: &TransactionMeta) -> Vec<DiagnosticEvent> {
1120 match tx_meta {
1121 TransactionMeta::V3(TransactionMetaV3 {
1122 soroban_meta: Some(meta),
1123 ..
1124 }) => {
1125 if meta.diagnostic_events.len() == 1 {
1127 meta.diagnostic_events.clone().into()
1128 } else if meta.events.len() == 1 {
1129 meta.events
1130 .iter()
1131 .map(|e| DiagnosticEvent {
1132 in_successful_contract_call: true,
1133 event: e.clone(),
1134 })
1135 .collect()
1136 } else {
1137 Vec::new()
1138 }
1139 }
1140 _ => Vec::new(),
1141 }
1142}
1143
1144pub(crate) fn parse_cursor(c: &str) -> Result<(u64, i32), Error> {
1145 let (toid_part, event_index) = c.split('-').collect_tuple().ok_or(Error::InvalidCursor)?;
1146 let toid_part: u64 = toid_part.parse().map_err(|_| Error::InvalidCursor)?;
1147 let start_index: i32 = event_index.parse().map_err(|_| Error::InvalidCursor)?;
1148 Ok((toid_part, start_index))
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use super::*;
1154 use std::env;
1155 use std::fs;
1156 use std::path::PathBuf;
1157
1158 #[test]
1159 fn simulation_transaction_response_parsing() {
1160 let s = r#"{
1161 "minResourceFee": "100000000",
1162 "cost": { "cpuInsns": "1000", "memBytes": "1000" },
1163 "transactionData": "",
1164 "latestLedger": 1234,
1165 "stateChanges": [{
1166 "type": "created",
1167 "key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==",
1168 "before": null,
1169 "after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
1170 }]
1171 }"#;
1172
1173 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1174 assert_eq!(
1175 resp.state_changes.unwrap()[0],
1176 LedgerEntryChange::Created { key: "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==".to_string(), after: "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string() },
1177 );
1178 assert_eq!(resp.min_resource_fee, 100_000_000);
1179 }
1180
1181 #[test]
1182 fn simulation_transaction_response_parsing_mostly_empty() {
1183 let s = r#"{
1184 "latestLedger": 1234
1185 }"#;
1186
1187 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1188 assert_eq!(resp.latest_ledger, 1_234);
1189 }
1190
1191 fn get_repo_root() -> PathBuf {
1192 let mut path = env::current_exe().expect("Failed to get current executable path");
1193 while path.pop() {
1195 if path.join("Cargo.toml").exists() {
1196 return path;
1197 }
1198 }
1199 panic!("Could not find repository root");
1200 }
1201
1202 #[test]
1203 fn test_parse_get_transactions_response() {
1204 let repo_root = get_repo_root();
1205 let fixture_path = repo_root
1206 .join("src")
1207 .join("fixtures")
1208 .join("transactions_response.json");
1209 let response_content =
1210 fs::read_to_string(fixture_path).expect("Failed to read transactions_response.json");
1211
1212 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1214 .expect("Failed to parse JSON from transactions_response.json");
1215
1216 let result = full_response["result"].clone();
1218 let raw_response: GetTransactionsResponseRaw = serde_json::from_value(result)
1220 .expect("Failed to parse 'result' into GetTransactionsResponseRaw");
1221
1222 let response: GetTransactionsResponse = raw_response
1224 .try_into()
1225 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1226
1227 assert_eq!(response.transactions.len(), 5);
1229 assert_eq!(response.latest_ledger, 556_962);
1230 assert_eq!(response.cursor, 2_379_420_471_922_689);
1231
1232 assert_eq!(response.transactions[0].status, "SUCCESS");
1234 }
1237
1238 #[test]
1239 fn test_rpc_url_default_ports() {
1240 let client = Client::new("http://example.com").unwrap();
1242 assert_eq!(client.base_url(), "http://example.com:80/");
1243 let client = Client::new("https://example.com").unwrap();
1244 assert_eq!(client.base_url(), "https://example.com:443/");
1245
1246 let client = Client::new("http://example.com:8080").unwrap();
1248 assert_eq!(client.base_url(), "http://example.com:8080/");
1249 let client = Client::new("https://example.com:8080").unwrap();
1250 assert_eq!(client.base_url(), "https://example.com:8080/");
1251
1252 let client = Client::new("http://example.com/a/b/c").unwrap();
1254 assert_eq!(client.base_url(), "http://example.com:80/a/b/c");
1255 let client = Client::new("https://example.com/a/b/c").unwrap();
1256 assert_eq!(client.base_url(), "https://example.com:443/a/b/c");
1257 let client = Client::new("http://example.com/a/b/c/").unwrap();
1258 assert_eq!(client.base_url(), "http://example.com:80/a/b/c/");
1259 let client = Client::new("https://example.com/a/b/c/").unwrap();
1260 assert_eq!(client.base_url(), "https://example.com:443/a/b/c/");
1261 let client = Client::new("http://example.com/a/b:80/c/").unwrap();
1262 assert_eq!(client.base_url(), "http://example.com:80/a/b:80/c/");
1263 let client = Client::new("https://example.com/a/b:80/c/").unwrap();
1264 assert_eq!(client.base_url(), "https://example.com:443/a/b:80/c/");
1265 }
1266
1267 #[test]
1268 fn test_does_topic_match() {
1271 struct TestCase<'a> {
1272 name: &'a str,
1273 filter: Vec<&'a str>,
1274 includes: Vec<Vec<&'a str>>,
1275 excludes: Vec<Vec<&'a str>>,
1276 }
1277
1278 let xfer = "AAAABQAAAAh0cmFuc2Zlcg==";
1279 let number = "AAAAAQB6Mcc=";
1280 let star = "*";
1281
1282 for tc in vec![
1283 TestCase {
1285 name: "<empty>",
1286 filter: vec![],
1287 includes: vec![],
1288 excludes: vec![vec![xfer]],
1289 },
1290 TestCase {
1294 name: "*",
1295 filter: vec![star],
1296 includes: vec![vec![xfer]],
1297 excludes: vec![vec![xfer, xfer], vec![xfer, number]],
1298 },
1299 TestCase {
1302 name: "*/transfer",
1303 filter: vec![star, xfer],
1304 includes: vec![vec![number, xfer], vec![xfer, xfer]],
1305 excludes: vec![
1306 vec![number],
1307 vec![number, number],
1308 vec![number, xfer, number],
1309 vec![xfer],
1310 vec![xfer, number],
1311 vec![xfer, xfer, xfer],
1312 ],
1313 },
1314 TestCase {
1318 name: "transfer/*",
1319 filter: vec![xfer, star],
1320 includes: vec![vec![xfer, number], vec![xfer, xfer]],
1321 excludes: vec![
1322 vec![number],
1323 vec![number, number],
1324 vec![number, xfer, number],
1325 vec![xfer],
1326 vec![number, xfer],
1327 vec![xfer, xfer, xfer],
1328 ],
1329 },
1330 TestCase {
1332 name: "transfer/*/*",
1333 filter: vec![xfer, star, star],
1334 includes: vec![vec![xfer, number, number], vec![xfer, xfer, xfer]],
1335 excludes: vec![
1336 vec![number],
1337 vec![number, number],
1338 vec![number, xfer],
1339 vec![number, xfer, number, number],
1340 vec![xfer],
1341 vec![xfer, xfer, xfer, xfer],
1342 ],
1343 },
1344 TestCase {
1348 name: "transfer/*/number",
1349 filter: vec![xfer, star, number],
1350 includes: vec![vec![xfer, number, number], vec![xfer, xfer, number]],
1351 excludes: vec![
1352 vec![number],
1353 vec![number, number],
1354 vec![number, number, number],
1355 vec![number, xfer, number],
1356 vec![xfer],
1357 vec![number, xfer],
1358 vec![xfer, xfer, xfer],
1359 vec![xfer, number, xfer],
1360 ],
1361 },
1362 ] {
1363 for topic in tc.includes {
1364 assert!(
1365 does_topic_match(
1366 &topic
1367 .iter()
1368 .map(std::string::ToString::to_string)
1369 .collect::<Vec<String>>(),
1370 &tc.filter
1371 .iter()
1372 .map(std::string::ToString::to_string)
1373 .collect::<Vec<String>>()
1374 ),
1375 "test: {}, topic ({:?}) should be matched by filter ({:?})",
1376 tc.name,
1377 topic,
1378 tc.filter
1379 );
1380 }
1381
1382 for topic in tc.excludes {
1383 assert!(
1384 !does_topic_match(
1385 &topic
1387 .iter()
1388 .map(std::string::ToString::to_string)
1389 .collect::<Vec<String>>(),
1390 &tc.filter
1391 .iter()
1392 .map(std::string::ToString::to_string)
1393 .collect::<Vec<String>>()
1394 ),
1395 "test: {}, topic ({:?}) should NOT be matched by filter ({:?})",
1396 tc.name,
1397 topic,
1398 tc.filter
1399 );
1400 }
1401 }
1402 }
1403}