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 GetHealthResponse {
304 pub status: String,
305 #[serde(rename = "latestLedger")]
306 pub latest_ledger: u32,
307 #[serde(rename = "oldestLedger")]
308 pub oldest_ledger: u32,
309 #[serde(rename = "ledgerRetentionWindow")]
310 pub ledger_retention_window: u32,
311}
312
313#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
314pub struct GetLatestLedgerResponse {
315 pub id: String,
316 #[serde(rename = "protocolVersion")]
317 pub protocol_version: u32,
318 pub sequence: u32,
319}
320
321#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
322pub struct Cost {
323 #[serde(
324 rename = "cpuInsns",
325 deserialize_with = "deserialize_number_from_string"
326 )]
327 pub cpu_insns: u64,
328 #[serde(
329 rename = "memBytes",
330 deserialize_with = "deserialize_number_from_string"
331 )]
332 pub mem_bytes: u64,
333}
334
335#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
336pub struct SimulateHostFunctionResultRaw {
337 #[serde(deserialize_with = "deserialize_default_from_null")]
338 pub auth: Vec<String>,
339 pub xdr: String,
340}
341
342#[derive(Debug, Clone)]
343pub struct SimulateHostFunctionResult {
344 pub auth: Vec<SorobanAuthorizationEntry>,
345 pub xdr: xdr::ScVal,
346}
347
348#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
349#[serde(tag = "type")]
350pub enum LedgerEntryChange {
351 #[serde(rename = "created")]
352 Created { key: String, after: String },
353 #[serde(rename = "deleted")]
354 Deleted { key: String, before: String },
355 #[serde(rename = "updated")]
356 Updated {
357 key: String,
358 before: String,
359 after: String,
360 },
361}
362
363#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
364pub struct SimulateTransactionResponse {
365 #[serde(
366 rename = "minResourceFee",
367 deserialize_with = "deserialize_number_from_string",
368 default
369 )]
370 pub min_resource_fee: u64,
371 #[serde(default)]
372 pub cost: Cost,
373 #[serde(skip_serializing_if = "Vec::is_empty", default)]
374 pub results: Vec<SimulateHostFunctionResultRaw>,
375 #[serde(rename = "transactionData", default)]
376 pub transaction_data: String,
377 #[serde(
378 deserialize_with = "deserialize_default_from_null",
379 skip_serializing_if = "Vec::is_empty",
380 default
381 )]
382 pub events: Vec<String>,
383 #[serde(
384 rename = "restorePreamble",
385 skip_serializing_if = "Option::is_none",
386 default
387 )]
388 pub restore_preamble: Option<RestorePreamble>,
389 #[serde(
390 rename = "stateChanges",
391 skip_serializing_if = "Option::is_none",
392 default
393 )]
394 pub state_changes: Option<Vec<LedgerEntryChange>>,
395 #[serde(rename = "latestLedger")]
396 pub latest_ledger: u32,
397 #[serde(skip_serializing_if = "Option::is_none", default)]
398 pub error: Option<String>,
399}
400
401impl SimulateTransactionResponse {
402 pub fn results(&self) -> Result<Vec<SimulateHostFunctionResult>, Error> {
405 self.results
406 .iter()
407 .map(|r| {
408 Ok(SimulateHostFunctionResult {
409 auth: r
410 .auth
411 .iter()
412 .map(|a| {
413 Ok(SorobanAuthorizationEntry::from_xdr_base64(
414 a,
415 Limits::none(),
416 )?)
417 })
418 .collect::<Result<_, Error>>()?,
419 xdr: xdr::ScVal::from_xdr_base64(&r.xdr, Limits::none())?,
420 })
421 })
422 .collect()
423 }
424
425 pub fn events(&self) -> Result<Vec<DiagnosticEvent>, Error> {
428 self.events
429 .iter()
430 .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e, Limits::none())?))
431 .collect()
432 }
433
434 pub fn transaction_data(&self) -> Result<SorobanTransactionData, Error> {
437 Ok(SorobanTransactionData::from_xdr_base64(
438 &self.transaction_data,
439 Limits::none(),
440 )?)
441 }
442}
443
444#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
445pub struct RestorePreamble {
446 #[serde(rename = "transactionData")]
447 pub transaction_data: String,
448 #[serde(
449 rename = "minResourceFee",
450 deserialize_with = "deserialize_number_from_string"
451 )]
452 pub min_resource_fee: u64,
453}
454
455#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
456pub struct GetEventsResponse {
457 #[serde(deserialize_with = "deserialize_default_from_null")]
458 pub events: Vec<Event>,
459 #[serde(rename = "latestLedger")]
460 pub latest_ledger: u32,
461}
462
463#[must_use]
479pub fn does_topic_match(topic: &[String], filter: &[String]) -> bool {
480 filter.len() == topic.len()
481 && filter
482 .iter()
483 .enumerate()
484 .all(|(i, s)| *s == "*" || topic[i] == *s)
485}
486
487#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
488pub struct Event {
489 #[serde(rename = "type")]
490 pub event_type: String,
491
492 pub ledger: u32,
493 #[serde(rename = "ledgerClosedAt")]
494 pub ledger_closed_at: String,
495
496 pub id: String,
497 #[serde(rename = "pagingToken")]
498 pub paging_token: String,
499
500 #[serde(rename = "contractId")]
501 pub contract_id: String,
502 pub topic: Vec<String>,
503 pub value: String,
504}
505
506impl Display for Event {
507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508 writeln!(
509 f,
510 "Event {} [{}]:",
511 self.paging_token,
512 self.event_type.to_ascii_uppercase()
513 )?;
514 writeln!(
515 f,
516 " Ledger: {} (closed at {})",
517 self.ledger, self.ledger_closed_at
518 )?;
519 writeln!(f, " Contract: {}", self.contract_id)?;
520 writeln!(f, " Topics:")?;
521 for topic in &self.topic {
522 let scval =
523 xdr::ScVal::from_xdr_base64(topic, Limits::none()).map_err(|_| std::fmt::Error)?;
524 writeln!(f, " {scval:?}")?;
525 }
526 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())
527 .map_err(|_| std::fmt::Error)?;
528 writeln!(f, " Value: {scval:?}")
529 }
530}
531
532impl Event {
533 pub fn parse_cursor(&self) -> Result<(u64, i32), Error> {
536 parse_cursor(&self.id)
537 }
538 pub fn pretty_print(&self) -> Result<(), Box<dyn std::error::Error>> {
541 let mut stdout = StandardStream::stdout(ColorChoice::Auto);
542 if !stdout.supports_color() {
543 println!("{self}");
544 return Ok(());
545 }
546
547 let color = match self.event_type.as_str() {
548 "system" => Color::Yellow,
549 _ => Color::Blue,
550 };
551 colored!(
552 stdout,
553 "{}Event{} {}{}{} [{}{}{}{}]:\n",
554 bold!(true),
555 bold!(false),
556 fg!(Some(Color::Green)),
557 self.paging_token,
558 reset!(),
559 bold!(true),
560 fg!(Some(color)),
561 self.event_type.to_ascii_uppercase(),
562 reset!(),
563 )?;
564
565 colored!(
566 stdout,
567 " Ledger: {}{}{} (closed at {}{}{})\n",
568 fg!(Some(Color::Green)),
569 self.ledger,
570 reset!(),
571 fg!(Some(Color::Green)),
572 self.ledger_closed_at,
573 reset!(),
574 )?;
575
576 colored!(
577 stdout,
578 " Contract: {}{}{}\n",
579 fg!(Some(Color::Green)),
580 self.contract_id,
581 reset!(),
582 )?;
583
584 colored!(stdout, " Topics:\n")?;
585 for topic in &self.topic {
586 let scval = xdr::ScVal::from_xdr_base64(topic, Limits::none())?;
587 colored!(
588 stdout,
589 " {}{:?}{}\n",
590 fg!(Some(Color::Green)),
591 scval,
592 reset!(),
593 )?;
594 }
595
596 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())?;
597 colored!(
598 stdout,
599 " Value: {}{:?}{}\n",
600 fg!(Some(Color::Green)),
601 scval,
602 reset!(),
603 )?;
604
605 Ok(())
606 }
607}
608
609#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
610pub enum EventType {
611 All,
612 Contract,
613 System,
614}
615
616#[derive(Clone, Debug, Eq, Hash, PartialEq)]
617pub enum EventStart {
618 Ledger(u32),
619 Cursor(String),
620}
621
622#[derive(Debug, Clone)]
623pub struct FullLedgerEntry {
624 pub key: LedgerKey,
625 pub val: LedgerEntryData,
626 pub last_modified_ledger: u32,
627 pub live_until_ledger_seq: u32,
628}
629
630#[derive(Debug, Clone)]
631pub struct FullLedgerEntries {
632 pub entries: Vec<FullLedgerEntry>,
633 pub latest_ledger: i64,
634}
635
636#[derive(Debug, Clone)]
637pub struct Client {
638 base_url: Arc<str>,
639 timeout_in_secs: u64,
640 http_client: Arc<HttpClient>,
641}
642
643#[allow(deprecated)] impl Client {
645 pub fn new(base_url: &str) -> Result<Self, Error> {
648 let uri = base_url.parse::<Uri>().map_err(Error::InvalidRpcUrl)?;
653 let mut parts = uri.into_parts();
654 if let (Some(scheme), Some(authority)) = (&parts.scheme, &parts.authority) {
655 if authority.port().is_none() {
656 let port = match scheme.as_str() {
657 "http" => Some(80),
658 "https" => Some(443),
659 _ => None,
660 };
661 if let Some(port) = port {
662 let host = authority.host();
663 parts.authority = Some(
664 Authority::from_str(&format!("{host}:{port}"))
665 .map_err(Error::InvalidRpcUrl)?,
666 );
667 }
668 }
669 }
670 let uri = Uri::from_parts(parts).map_err(Error::InvalidRpcUrlFromUriParts)?;
671 let base_url = Arc::from(uri.to_string());
672 let headers = Self::default_http_headers();
673 let http_client = Arc::new(
674 HttpClientBuilder::default()
675 .set_headers(headers)
676 .build(&base_url)?,
677 );
678 Ok(Self {
679 base_url,
680 timeout_in_secs: 30,
681 http_client,
682 })
683 }
684
685 #[deprecated(
688 note = "To be marked private in a future major release. Please use `new_with_headers` instead."
689 )]
690 pub fn new_with_timeout(base_url: &str, timeout: u64) -> Result<Self, Error> {
691 let mut client = Self::new(base_url)?;
692 client.timeout_in_secs = timeout;
693 Ok(client)
694 }
695
696 pub fn new_with_headers(base_url: &str, additional_headers: HeaderMap) -> Result<Self, Error> {
699 let mut client = Self::new(base_url)?;
700 let mut headers = Self::default_http_headers();
701
702 for (key, value) in additional_headers {
703 headers.insert(key.ok_or(Error::InvalidResponse)?, value);
704 }
705 let http_client = Arc::new(
706 HttpClientBuilder::default()
707 .set_headers(headers)
708 .build(base_url)?,
709 );
710
711 client.http_client = http_client;
712 Ok(client)
713 }
714
715 fn default_http_headers() -> HeaderMap {
716 let mut headers = HeaderMap::new();
717 headers.insert("X-Client-Name", unsafe {
718 "rs-stellar-rpc-client".parse().unwrap_unchecked()
719 });
720 let version = VERSION.unwrap_or("devel");
721 headers.insert("X-Client-Version", unsafe {
722 version.parse().unwrap_unchecked()
723 });
724 headers
725 }
726
727 #[must_use]
728 pub fn base_url(&self) -> &str {
729 &self.base_url
730 }
731
732 #[must_use]
733 pub fn client(&self) -> &HttpClient {
734 &self.http_client
735 }
736
737 pub async fn friendbot_url(&self) -> Result<String, Error> {
740 let network = self.get_network().await?;
741 network.friendbot_url.ok_or_else(|| {
742 Error::NotFound(
743 "Friendbot".to_string(),
744 "Friendbot is not available on this network".to_string(),
745 )
746 })
747 }
748 pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result<String, Error> {
751 let server = self.get_network().await?.passphrase;
752 if let Some(expected) = expected {
753 if expected != server {
754 return Err(Error::InvalidNetworkPassphrase {
755 expected: expected.to_string(),
756 server,
757 });
758 }
759 }
760 Ok(server)
761 }
762
763 pub async fn get_network(&self) -> Result<GetNetworkResponse, Error> {
766 Ok(self
767 .client()
768 .request("getNetwork", ObjectParams::new())
769 .await?)
770 }
771
772 pub async fn get_health(&self) -> Result<GetHealthResponse, Error> {
775 Ok(self
776 .client()
777 .request("getHealth", ObjectParams::new())
778 .await?)
779 }
780
781 pub async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, Error> {
784 Ok(self
785 .client()
786 .request("getLatestLedger", ObjectParams::new())
787 .await?)
788 }
789
790 pub async fn get_account(&self, address: &str) -> Result<AccountEntry, Error> {
793 let key = LedgerKey::Account(LedgerKeyAccount {
794 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
795 stellar_strkey::ed25519::PublicKey::from_string(address)?.0,
796 ))),
797 });
798 let keys = Vec::from([key]);
799 let response = self.get_ledger_entries(&keys).await?;
800 let entries = response.entries.unwrap_or_default();
801 if entries.is_empty() {
802 return Err(Error::NotFound("Account".to_string(), address.to_owned()));
803 }
804 let ledger_entry = &entries[0];
805 let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none());
806 if let LedgerEntryData::Account(entry) = LedgerEntryData::read_xdr_base64(&mut read)? {
807 Ok(entry)
808 } else {
809 Err(Error::InvalidResponse)
810 }
811 }
812
813 pub async fn send_transaction(&self, tx: &TransactionEnvelope) -> Result<Hash, Error> {
816 let mut oparams = ObjectParams::new();
817 oparams.insert("transaction", tx.to_xdr_base64(Limits::none())?)?;
818 let SendTransactionResponse {
819 hash,
820 error_result_xdr,
821 status,
822 ..
823 } = self
824 .client()
825 .request("sendTransaction", oparams)
826 .await
827 .map_err(|err| {
828 Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}"))
829 })?;
830
831 if status == "ERROR" {
832 let error = error_result_xdr
833 .ok_or(Error::MissingError)
834 .and_then(|x| {
835 TransactionResult::read_xdr_base64(&mut Limited::new(
836 x.as_bytes(),
837 Limits::none(),
838 ))
839 .map_err(|_| Error::InvalidResponse)
840 })
841 .map(|r| r.result)?;
842 return Err(Error::TransactionSubmissionFailed(format!("{error:#?}")));
843 }
844 Ok(Hash::from_str(&hash)?)
845 }
846
847 pub async fn send_transaction_polling(
850 &self,
851 tx: &TransactionEnvelope,
852 ) -> Result<GetTransactionResponse, Error> {
853 let hash = self.send_transaction(tx).await?;
854 self.get_transaction_polling(&hash, None).await
855 }
856
857 pub async fn simulate_transaction_envelope(
860 &self,
861 tx: &TransactionEnvelope,
862 ) -> Result<SimulateTransactionResponse, Error> {
863 let base64_tx = tx.to_xdr_base64(Limits::none())?;
864 let mut oparams = ObjectParams::new();
865 oparams.insert("transaction", base64_tx)?;
866 let sim_res = self
867 .client()
868 .request("simulateTransaction", oparams)
869 .await?;
870 Ok(sim_res)
871 }
872
873 pub async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, Error> {
876 let mut oparams = ObjectParams::new();
877 oparams.insert("hash", tx_id)?;
878 let resp: GetTransactionResponseRaw =
879 self.client().request("getTransaction", oparams).await?;
880 Ok(resp.try_into()?)
881 }
882
883 pub async fn get_transactions(
886 &self,
887 request: GetTransactionsRequest,
888 ) -> Result<GetTransactionsResponse, Error> {
889 let mut oparams = ObjectParams::new();
890 if let Some(start_ledger) = request.start_ledger {
891 oparams.insert("startLedger", start_ledger)?;
892 }
893 if let Some(pagination_params) = request.pagination {
894 let pagination = serde_json::json!(pagination_params);
895 oparams.insert("pagination", pagination)?;
896 }
897 let resp: GetTransactionsResponseRaw =
898 self.client().request("getTransactions", oparams).await?;
899 Ok(resp.try_into()?)
900 }
901
902 pub async fn get_transaction_polling(
912 &self,
913 tx_id: &Hash,
914 timeout_s: Option<Duration>,
915 ) -> Result<GetTransactionResponse, Error> {
916 let start = Instant::now();
918 let timeout = timeout_s.unwrap_or(Duration::from_secs(self.timeout_in_secs));
919 let exponential_backoff: f64 = 1.0 / (1.0 - E.powf(-1.0));
922 let mut sleep_time = Duration::from_secs(1);
923 loop {
924 let response = self.get_transaction(tx_id).await?;
925 match response.status.as_str() {
926 "SUCCESS" => return Ok(response),
927
928 "FAILED" => {
929 return Err(Error::TransactionSubmissionFailed(format!(
930 "{:#?}",
931 response.result
932 )))
933 }
934
935 "NOT_FOUND" => (),
936 _ => {
937 return Err(Error::UnexpectedTransactionStatus(response.status));
938 }
939 };
940 if start.elapsed() > timeout {
941 return Err(Error::TransactionSubmissionTimeout);
942 }
943 sleep(sleep_time).await;
944 sleep_time = Duration::from_secs_f64(sleep_time.as_secs_f64() * exponential_backoff);
945 }
946 }
947
948 pub async fn get_ledger_entries(
951 &self,
952 keys: &[LedgerKey],
953 ) -> Result<GetLedgerEntriesResponse, Error> {
954 let mut base64_keys: Vec<String> = vec![];
955 for k in keys {
956 let base64_result = k.to_xdr_base64(Limits::none());
957 if base64_result.is_err() {
958 return Err(Error::Xdr(XdrError::Invalid));
959 }
960 base64_keys.push(k.to_xdr_base64(Limits::none())?);
961 }
962 let mut oparams = ObjectParams::new();
963 oparams.insert("keys", base64_keys)?;
964 Ok(self.client().request("getLedgerEntries", oparams).await?)
965 }
966
967 pub async fn get_full_ledger_entries(
970 &self,
971 ledger_keys: &[LedgerKey],
972 ) -> Result<FullLedgerEntries, Error> {
973 let keys = ledger_keys
974 .iter()
975 .filter(|key| !matches!(key, LedgerKey::Ttl(_)))
976 .map(Clone::clone)
977 .collect::<Vec<_>>();
978 let GetLedgerEntriesResponse {
979 entries,
980 latest_ledger,
981 } = self.get_ledger_entries(&keys).await?;
982 let entries = entries
983 .unwrap_or_default()
984 .iter()
985 .map(
986 |LedgerEntryResult {
987 key,
988 xdr,
989 last_modified_ledger,
990 live_until_ledger_seq_ledger_seq,
991 }| {
992 Ok(FullLedgerEntry {
993 key: LedgerKey::from_xdr_base64(key, Limits::none())?,
994 val: LedgerEntryData::from_xdr_base64(xdr, Limits::none())?,
995 live_until_ledger_seq: live_until_ledger_seq_ledger_seq.unwrap_or_default(),
996 last_modified_ledger: *last_modified_ledger,
997 })
998 },
999 )
1000 .collect::<Result<Vec<_>, Error>>()?;
1001 Ok(FullLedgerEntries {
1002 entries,
1003 latest_ledger,
1004 })
1005 }
1006 pub async fn get_events(
1009 &self,
1010 start: EventStart,
1011 event_type: Option<EventType>,
1012 contract_ids: &[String],
1013 topics: &[String],
1014 limit: Option<usize>,
1015 ) -> Result<GetEventsResponse, Error> {
1016 let mut filters = serde_json::Map::new();
1017
1018 event_type
1019 .and_then(|t| match t {
1020 EventType::All => None, EventType::Contract => Some("contract"),
1022 EventType::System => Some("system"),
1023 })
1024 .map(|t| filters.insert("type".to_string(), t.into()));
1025
1026 filters.insert("topics".to_string(), topics.into());
1027 filters.insert("contractIds".to_string(), contract_ids.into());
1028
1029 let mut pagination = serde_json::Map::new();
1030 if let Some(limit) = limit {
1031 pagination.insert("limit".to_string(), limit.into());
1032 }
1033
1034 let mut oparams = ObjectParams::new();
1035 match start {
1036 EventStart::Ledger(l) => oparams.insert("startLedger", l)?,
1037 EventStart::Cursor(c) => {
1038 pagination.insert("cursor".to_string(), c.into());
1039 }
1040 };
1041 oparams.insert("filters", vec![filters])?;
1042 oparams.insert("pagination", pagination)?;
1043
1044 Ok(self.client().request("getEvents", oparams).await?)
1045 }
1046
1047 pub async fn get_contract_data(
1050 &self,
1051 contract_id: &[u8; 32],
1052 ) -> Result<ContractDataEntry, Error> {
1053 let contract_key = LedgerKey::ContractData(xdr::LedgerKeyContractData {
1055 contract: xdr::ScAddress::Contract(xdr::Hash(*contract_id)),
1056 key: xdr::ScVal::LedgerKeyContractInstance,
1057 durability: xdr::ContractDataDurability::Persistent,
1058 });
1059 let contract_ref = self.get_ledger_entries(&[contract_key]).await?;
1060 let entries = contract_ref.entries.unwrap_or_default();
1061 if entries.is_empty() {
1062 let contract_address = stellar_strkey::Contract(*contract_id).to_string();
1063 return Err(Error::NotFound("Contract".to_string(), contract_address));
1064 }
1065 let contract_ref_entry = &entries[0];
1066 match LedgerEntryData::from_xdr_base64(&contract_ref_entry.xdr, Limits::none())? {
1067 LedgerEntryData::ContractData(contract_data) => Ok(contract_data),
1068 scval => Err(Error::UnexpectedContractCodeDataType(scval)),
1069 }
1070 }
1071
1072 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1075 pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result<Vec<u8>, Error> {
1076 match self.get_contract_data(contract_id).await? {
1077 xdr::ContractDataEntry {
1078 val:
1079 xdr::ScVal::ContractInstance(xdr::ScContractInstance {
1080 executable: xdr::ContractExecutable::Wasm(hash),
1081 ..
1082 }),
1083 ..
1084 } => self.get_remote_wasm_from_hash(hash).await,
1085 scval => Err(Error::UnexpectedToken(scval)),
1086 }
1087 }
1088
1089 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1092 pub async fn get_remote_wasm_from_hash(&self, hash: Hash) -> Result<Vec<u8>, Error> {
1093 let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
1094 let contract_data = self.get_ledger_entries(&[code_key]).await?;
1095 let entries = contract_data.entries.unwrap_or_default();
1096 if entries.is_empty() {
1097 return Err(Error::NotFound(
1098 "Contract Code".to_string(),
1099 hex::encode(hash),
1100 ));
1101 }
1102 let contract_data_entry = &entries[0];
1103 match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
1104 LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
1105 scval => Err(Error::UnexpectedContractCodeDataType(scval)),
1106 }
1107 }
1108
1109 pub async fn get_contract_instance(
1114 &self,
1115 contract_id: &[u8; 32],
1116 ) -> Result<ScContractInstance, Error> {
1117 let contract_data = self.get_contract_data(contract_id).await?;
1118 match contract_data.val {
1119 xdr::ScVal::ContractInstance(instance) => Ok(instance),
1120 scval => Err(Error::UnexpectedContractInstance(scval)),
1121 }
1122 }
1123}
1124
1125fn extract_events(tx_meta: &TransactionMeta) -> Vec<DiagnosticEvent> {
1126 match tx_meta {
1127 TransactionMeta::V3(TransactionMetaV3 {
1128 soroban_meta: Some(meta),
1129 ..
1130 }) => {
1131 if meta.diagnostic_events.len() == 1 {
1133 meta.diagnostic_events.clone().into()
1134 } else if meta.events.len() == 1 {
1135 meta.events
1136 .iter()
1137 .map(|e| DiagnosticEvent {
1138 in_successful_contract_call: true,
1139 event: e.clone(),
1140 })
1141 .collect()
1142 } else {
1143 Vec::new()
1144 }
1145 }
1146 _ => Vec::new(),
1147 }
1148}
1149
1150pub(crate) fn parse_cursor(c: &str) -> Result<(u64, i32), Error> {
1151 let (toid_part, event_index) = c.split('-').collect_tuple().ok_or(Error::InvalidCursor)?;
1152 let toid_part: u64 = toid_part.parse().map_err(|_| Error::InvalidCursor)?;
1153 let start_index: i32 = event_index.parse().map_err(|_| Error::InvalidCursor)?;
1154 Ok((toid_part, start_index))
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160 use std::env;
1161 use std::fs;
1162 use std::path::PathBuf;
1163
1164 #[test]
1165 fn simulation_transaction_response_parsing() {
1166 let s = r#"{
1167 "minResourceFee": "100000000",
1168 "cost": { "cpuInsns": "1000", "memBytes": "1000" },
1169 "transactionData": "",
1170 "latestLedger": 1234,
1171 "stateChanges": [{
1172 "type": "created",
1173 "key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==",
1174 "before": null,
1175 "after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
1176 }]
1177 }"#;
1178
1179 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1180 assert_eq!(
1181 resp.state_changes.unwrap()[0],
1182 LedgerEntryChange::Created { key: "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==".to_string(), after: "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string() },
1183 );
1184 assert_eq!(resp.min_resource_fee, 100_000_000);
1185 }
1186
1187 #[test]
1188 fn simulation_transaction_response_parsing_mostly_empty() {
1189 let s = r#"{
1190 "latestLedger": 1234
1191 }"#;
1192
1193 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1194 assert_eq!(resp.latest_ledger, 1_234);
1195 }
1196
1197 fn get_repo_root() -> PathBuf {
1198 let mut path = env::current_exe().expect("Failed to get current executable path");
1199 while path.pop() {
1201 if path.join("Cargo.toml").exists() {
1202 return path;
1203 }
1204 }
1205 panic!("Could not find repository root");
1206 }
1207
1208 #[test]
1209 fn test_parse_get_transactions_response() {
1210 let repo_root = get_repo_root();
1211 let fixture_path = repo_root
1212 .join("src")
1213 .join("fixtures")
1214 .join("transactions_response.json");
1215 let response_content =
1216 fs::read_to_string(fixture_path).expect("Failed to read transactions_response.json");
1217
1218 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1220 .expect("Failed to parse JSON from transactions_response.json");
1221
1222 let result = full_response["result"].clone();
1224 let raw_response: GetTransactionsResponseRaw = serde_json::from_value(result)
1226 .expect("Failed to parse 'result' into GetTransactionsResponseRaw");
1227
1228 let response: GetTransactionsResponse = raw_response
1230 .try_into()
1231 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1232
1233 assert_eq!(response.transactions.len(), 5);
1235 assert_eq!(response.latest_ledger, 556_962);
1236 assert_eq!(response.cursor, 2_379_420_471_922_689);
1237
1238 assert_eq!(response.transactions[0].status, "SUCCESS");
1240 }
1243
1244 #[test]
1245 fn test_rpc_url_default_ports() {
1246 let client = Client::new("http://example.com").unwrap();
1248 assert_eq!(client.base_url(), "http://example.com:80/");
1249 let client = Client::new("https://example.com").unwrap();
1250 assert_eq!(client.base_url(), "https://example.com:443/");
1251
1252 let client = Client::new("http://example.com:8080").unwrap();
1254 assert_eq!(client.base_url(), "http://example.com:8080/");
1255 let client = Client::new("https://example.com:8080").unwrap();
1256 assert_eq!(client.base_url(), "https://example.com:8080/");
1257
1258 let client = Client::new("http://example.com/a/b/c").unwrap();
1260 assert_eq!(client.base_url(), "http://example.com:80/a/b/c");
1261 let client = Client::new("https://example.com/a/b/c").unwrap();
1262 assert_eq!(client.base_url(), "https://example.com:443/a/b/c");
1263 let client = Client::new("http://example.com/a/b/c/").unwrap();
1264 assert_eq!(client.base_url(), "http://example.com:80/a/b/c/");
1265 let client = Client::new("https://example.com/a/b/c/").unwrap();
1266 assert_eq!(client.base_url(), "https://example.com:443/a/b/c/");
1267 let client = Client::new("http://example.com/a/b:80/c/").unwrap();
1268 assert_eq!(client.base_url(), "http://example.com:80/a/b:80/c/");
1269 let client = Client::new("https://example.com/a/b:80/c/").unwrap();
1270 assert_eq!(client.base_url(), "https://example.com:443/a/b:80/c/");
1271 }
1272
1273 #[test]
1274 fn test_does_topic_match() {
1277 struct TestCase<'a> {
1278 name: &'a str,
1279 filter: Vec<&'a str>,
1280 includes: Vec<Vec<&'a str>>,
1281 excludes: Vec<Vec<&'a str>>,
1282 }
1283
1284 let xfer = "AAAABQAAAAh0cmFuc2Zlcg==";
1285 let number = "AAAAAQB6Mcc=";
1286 let star = "*";
1287
1288 for tc in vec![
1289 TestCase {
1291 name: "<empty>",
1292 filter: vec![],
1293 includes: vec![],
1294 excludes: vec![vec![xfer]],
1295 },
1296 TestCase {
1300 name: "*",
1301 filter: vec![star],
1302 includes: vec![vec![xfer]],
1303 excludes: vec![vec![xfer, xfer], vec![xfer, number]],
1304 },
1305 TestCase {
1308 name: "*/transfer",
1309 filter: vec![star, xfer],
1310 includes: vec![vec![number, xfer], vec![xfer, xfer]],
1311 excludes: vec![
1312 vec![number],
1313 vec![number, number],
1314 vec![number, xfer, number],
1315 vec![xfer],
1316 vec![xfer, number],
1317 vec![xfer, xfer, xfer],
1318 ],
1319 },
1320 TestCase {
1324 name: "transfer/*",
1325 filter: vec![xfer, star],
1326 includes: vec![vec![xfer, number], vec![xfer, xfer]],
1327 excludes: vec![
1328 vec![number],
1329 vec![number, number],
1330 vec![number, xfer, number],
1331 vec![xfer],
1332 vec![number, xfer],
1333 vec![xfer, xfer, xfer],
1334 ],
1335 },
1336 TestCase {
1338 name: "transfer/*/*",
1339 filter: vec![xfer, star, star],
1340 includes: vec![vec![xfer, number, number], vec![xfer, xfer, xfer]],
1341 excludes: vec![
1342 vec![number],
1343 vec![number, number],
1344 vec![number, xfer],
1345 vec![number, xfer, number, number],
1346 vec![xfer],
1347 vec![xfer, xfer, xfer, xfer],
1348 ],
1349 },
1350 TestCase {
1354 name: "transfer/*/number",
1355 filter: vec![xfer, star, number],
1356 includes: vec![vec![xfer, number, number], vec![xfer, xfer, number]],
1357 excludes: vec![
1358 vec![number],
1359 vec![number, number],
1360 vec![number, number, number],
1361 vec![number, xfer, number],
1362 vec![xfer],
1363 vec![number, xfer],
1364 vec![xfer, xfer, xfer],
1365 vec![xfer, number, xfer],
1366 ],
1367 },
1368 ] {
1369 for topic in tc.includes {
1370 assert!(
1371 does_topic_match(
1372 &topic
1373 .iter()
1374 .map(std::string::ToString::to_string)
1375 .collect::<Vec<String>>(),
1376 &tc.filter
1377 .iter()
1378 .map(std::string::ToString::to_string)
1379 .collect::<Vec<String>>()
1380 ),
1381 "test: {}, topic ({:?}) should be matched by filter ({:?})",
1382 tc.name,
1383 topic,
1384 tc.filter
1385 );
1386 }
1387
1388 for topic in tc.excludes {
1389 assert!(
1390 !does_topic_match(
1391 &topic
1393 .iter()
1394 .map(std::string::ToString::to_string)
1395 .collect::<Vec<String>>(),
1396 &tc.filter
1397 .iter()
1398 .map(std::string::ToString::to_string)
1399 .collect::<Vec<String>>()
1400 ),
1401 "test: {}, topic ({:?}) should NOT be matched by filter ({:?})",
1402 tc.name,
1403 topic,
1404 tc.filter
1405 );
1406 }
1407 }
1408 }
1409}