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, ContractEvent, ContractId,
13 DiagnosticEvent, Error as XdrError, Hash, LedgerCloseMeta, LedgerEntryData, LedgerFootprint,
14 LedgerHeaderHistoryEntry, LedgerKey, LedgerKeyAccount, Limited, Limits, PublicKey, ReadXdr,
15 ScContractInstance, SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData,
16 TransactionEnvelope, TransactionEvent, TransactionMetaV3, TransactionResult, Uint256, VecM,
17 WriteXdr,
18};
19
20use std::{
21 f64::consts::E,
22 fmt::Display,
23 str::FromStr,
24 sync::Arc,
25 time::{Duration, Instant},
26};
27
28use termcolor::{Color, ColorChoice, StandardStream, WriteColor};
29use termcolor_output::colored;
30use tokio::time::sleep;
31
32const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
33
34pub type LogEvents = fn(
35 footprint: &LedgerFootprint,
36 auth: &[VecM<SorobanAuthorizationEntry>],
37 events: &[DiagnosticEvent],
38) -> ();
39
40pub type LogResources = fn(resources: &SorobanResources) -> ();
41
42#[derive(thiserror::Error, Debug)]
43#[allow(deprecated)] pub enum Error {
45 #[error(transparent)]
46 InvalidAddress(#[from] stellar_strkey::DecodeError),
47 #[error("invalid response from server")]
48 InvalidResponse,
49 #[error("provided network passphrase {expected:?} does not match the server: {server:?}")]
50 InvalidNetworkPassphrase { expected: String, server: String },
51 #[error("xdr processing error: {0}")]
52 Xdr(#[from] XdrError),
53 #[error("invalid rpc url: {0}")]
54 InvalidRpcUrl(http::uri::InvalidUri),
55 #[error("invalid rpc url: {0}")]
56 InvalidRpcUrlFromUriParts(http::uri::InvalidUriParts),
57 #[error("invalid friendbot url: {0}")]
58 InvalidUrl(String),
59 #[error(transparent)]
60 JsonRpc(#[from] jsonrpsee_core::Error),
61 #[error("json decoding error: {0}")]
62 Serde(#[from] serde_json::Error),
63 #[error("transaction failed: {0}")]
64 TransactionFailed(String),
65 #[error("transaction submission failed: {0}")]
66 TransactionSubmissionFailed(String),
67 #[error("expected transaction status: {0}")]
68 UnexpectedTransactionStatus(String),
69 #[error("transaction submission timeout")]
70 TransactionSubmissionTimeout,
71 #[error("transaction simulation failed: {0}")]
72 TransactionSimulationFailed(String),
73 #[error("{0} not found: {1}")]
74 NotFound(String, String),
75 #[error("Missing result in successful response")]
76 MissingResult,
77 #[error("Failed to read Error response from server")]
78 MissingError,
79 #[error("Missing signing key for account {address}")]
80 MissingSignerForAddress { address: String },
81 #[error("cursor is not valid")]
82 InvalidCursor,
83 #[error("unexpected ({length}) simulate transaction result length")]
84 UnexpectedSimulateTransactionResultSize { length: usize },
85 #[error("unexpected ({count}) number of operations")]
86 UnexpectedOperationCount { count: usize },
87 #[error("Transaction contains unsupported operation type")]
88 UnsupportedOperationType,
89 #[error("unexpected contract code data type: {0:?}")]
90 UnexpectedContractCodeDataType(LedgerEntryData),
91 #[error("unexpected contract instance type: {0:?}")]
92 UnexpectedContractInstance(xdr::ScVal),
93 #[error("unexpected contract code got token {0:?}")]
94 #[deprecated(note = "To be removed in future versions")]
95 UnexpectedToken(ContractDataEntry),
96 #[error("Fee was too large {0}")]
97 LargeFee(u64),
98 #[error("Cannot authorize raw transactions")]
99 CannotAuthorizeRawTransaction,
100 #[error("Missing result for tnx")]
101 MissingOp,
102}
103
104#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
105pub struct SendTransactionResponse {
106 pub hash: String,
107 pub status: String,
108 #[serde(
109 rename = "errorResultXdr",
110 skip_serializing_if = "Option::is_none",
111 default
112 )]
113 pub error_result_xdr: Option<String>,
114 #[serde(rename = "latestLedger")]
115 pub latest_ledger: u32,
116 #[serde(
117 rename = "latestLedgerCloseTime",
118 deserialize_with = "deserialize_number_from_string"
119 )]
120 pub latest_ledger_close_time: u32,
121}
122
123#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
124pub struct GetTransactionResponseRaw {
126 pub status: String,
127
128 #[serde(
129 rename = "envelopeXdr",
130 skip_serializing_if = "Option::is_none",
131 default
132 )]
133 pub envelope_xdr: Option<String>,
134
135 #[serde(rename = "resultXdr", skip_serializing_if = "Option::is_none", default)]
136 pub result_xdr: Option<String>,
137
138 #[serde(
139 rename = "resultMetaXdr",
140 skip_serializing_if = "Option::is_none",
141 default
142 )]
143 pub result_meta_xdr: Option<String>,
144
145 #[serde(rename = "events", skip_serializing_if = "Option::is_none", default)]
146 pub events: Option<GetTransactionEventsRaw>,
147}
148
149#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Default)]
150pub struct GetTransactionEventsRaw {
151 #[serde(
152 rename = "contractEventsXdr",
153 skip_serializing_if = "Option::is_none",
154 default
155 )]
156 pub contract_events_xdr: Option<Vec<Vec<String>>>,
157
158 #[serde(
159 rename = "diagnosticEventsXdr",
160 skip_serializing_if = "Option::is_none",
161 default
162 )]
163 pub diagnostic_events_xdr: Option<Vec<String>>,
164
165 #[serde(
166 rename = "transactionEventsXdr",
167 skip_serializing_if = "Option::is_none",
168 default
169 )]
170 pub transaction_events_xdr: Option<Vec<String>>,
171}
172
173#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
174pub struct GetTransactionEvents {
175 pub contract_events: Vec<Vec<ContractEvent>>,
176 pub diagnostic_events: Vec<DiagnosticEvent>,
177 pub transaction_events: Vec<TransactionEvent>,
178}
179
180#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
181pub struct GetTransactionResponse {
182 pub status: String,
183 pub envelope: Option<xdr::TransactionEnvelope>,
184 pub result: Option<xdr::TransactionResult>,
185 pub result_meta: Option<xdr::TransactionMeta>,
186 pub events: GetTransactionEvents,
187}
188
189impl TryInto<GetTransactionResponse> for GetTransactionResponseRaw {
190 type Error = xdr::Error;
191
192 fn try_into(self) -> Result<GetTransactionResponse, Self::Error> {
193 let events = self.events.unwrap_or_default();
194 let result_meta: Option<xdr::TransactionMeta> = self
195 .result_meta_xdr
196 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
197 .transpose()?;
198
199 let events = match result_meta {
200 Some(xdr::TransactionMeta::V4(_)) => GetTransactionEvents {
201 contract_events: events
202 .contract_events_xdr
203 .unwrap_or_default()
204 .into_iter()
205 .map(|es| {
206 es.into_iter()
207 .filter_map(|e| ContractEvent::from_xdr_base64(e, Limits::none()).ok())
208 .collect::<Vec<_>>()
209 })
210 .collect::<Vec<Vec<ContractEvent>>>(),
211
212 diagnostic_events: events
213 .diagnostic_events_xdr
214 .unwrap_or_default()
215 .iter()
216 .filter_map(|e| DiagnosticEvent::from_xdr_base64(e, Limits::none()).ok())
217 .collect(),
218
219 transaction_events: events
220 .transaction_events_xdr
221 .unwrap_or_default()
222 .iter()
223 .filter_map(|e| TransactionEvent::from_xdr_base64(e, Limits::none()).ok())
224 .collect(),
225 },
226
227 Some(xdr::TransactionMeta::V3(TransactionMetaV3 {
228 soroban_meta: Some(ref meta),
229 ..
230 })) => GetTransactionEvents {
231 contract_events: vec![],
232 transaction_events: vec![],
233 diagnostic_events: meta.diagnostic_events.clone().into(),
234 },
235
236 _ => GetTransactionEvents {
237 contract_events: vec![],
238 transaction_events: vec![],
239 diagnostic_events: vec![],
240 },
241 };
242
243 Ok(GetTransactionResponse {
244 status: self.status,
245 envelope: self
246 .envelope_xdr
247 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
248 .transpose()?,
249 result: self
250 .result_xdr
251 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
252 .transpose()?,
253 result_meta: result_meta,
254 events: events,
255 })
256 }
257}
258
259impl GetTransactionResponse {
260 pub fn return_value(&self) -> Result<xdr::ScVal, Error> {
263 if let Some(xdr::TransactionMeta::V3(xdr::TransactionMetaV3 {
264 soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }),
265 ..
266 })) = &self.result_meta
267 {
268 return Ok(return_value.clone());
269 }
270
271 if let Some(xdr::TransactionMeta::V4(xdr::TransactionMetaV4 {
272 soroban_meta:
273 Some(xdr::SorobanTransactionMetaV2 {
274 return_value: Some(return_value),
275 ..
276 }),
277 ..
278 })) = &self.result_meta
279 {
280 return Ok(return_value.clone());
281 }
282
283 Err(Error::MissingOp)
284 }
285}
286
287#[serde_as]
288#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
289pub struct GetTransactionsResponseRaw {
290 pub transactions: Vec<GetTransactionResponseRaw>,
291 #[serde(rename = "latestLedger")]
292 pub latest_ledger: u32,
293 #[serde(rename = "latestLedgerCloseTimestamp")]
294 pub latest_ledger_close_time: i64,
295 #[serde(rename = "oldestLedger")]
296 pub oldest_ledger: u32,
297 #[serde(rename = "oldestLedgerCloseTimestamp")]
298 pub oldest_ledger_close_time: i64,
299 #[serde_as(as = "DisplayFromStr")]
300 pub cursor: u64,
301}
302#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
303pub struct GetTransactionsResponse {
304 pub transactions: Vec<GetTransactionResponse>,
305 pub latest_ledger: u32,
306 pub latest_ledger_close_time: i64,
307 pub oldest_ledger: u32,
308 pub oldest_ledger_close_time: i64,
309 pub cursor: u64,
310}
311impl TryInto<GetTransactionsResponse> for GetTransactionsResponseRaw {
312 type Error = xdr::Error; fn try_into(self) -> Result<GetTransactionsResponse, Self::Error> {
315 Ok(GetTransactionsResponse {
316 transactions: self
317 .transactions
318 .into_iter()
319 .map(TryInto::try_into)
320 .collect::<Result<Vec<_>, xdr::Error>>()?,
321 latest_ledger: self.latest_ledger,
322 latest_ledger_close_time: self.latest_ledger_close_time,
323 oldest_ledger: self.oldest_ledger,
324 oldest_ledger_close_time: self.oldest_ledger_close_time,
325 cursor: self.cursor,
326 })
327 }
328}
329
330#[serde_as]
331#[derive(serde::Serialize, Debug, Clone)]
332pub struct TransactionsPaginationOptions {
333 #[serde_as(as = "Option<DisplayFromStr>")]
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub cursor: Option<u64>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub limit: Option<u32>,
338}
339
340#[derive(serde::Serialize, Debug, Clone)]
341pub struct GetTransactionsRequest {
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub start_ledger: Option<u32>,
344 pub pagination: Option<TransactionsPaginationOptions>,
345}
346
347#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
348pub struct LedgerEntryResult {
349 pub key: String,
350 pub xdr: String,
351 #[serde(rename = "lastModifiedLedgerSeq")]
352 pub last_modified_ledger: u32,
353 #[serde(
354 rename = "liveUntilLedgerSeq",
355 skip_serializing_if = "Option::is_none",
356 deserialize_with = "deserialize_option_number_from_string",
357 default
358 )]
359 pub live_until_ledger_seq_ledger_seq: Option<u32>,
360}
361
362#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
363pub struct GetLedgerEntriesResponse {
364 pub entries: Option<Vec<LedgerEntryResult>>,
365 #[serde(rename = "latestLedger")]
366 pub latest_ledger: i64,
367}
368
369#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
370pub struct GetNetworkResponse {
371 #[serde(
372 rename = "friendbotUrl",
373 skip_serializing_if = "Option::is_none",
374 default
375 )]
376 pub friendbot_url: Option<String>,
377 pub passphrase: String,
378 #[serde(rename = "protocolVersion")]
379 pub protocol_version: u32,
380}
381
382#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
383pub struct GetHealthResponse {
384 pub status: String,
385 #[serde(rename = "latestLedger")]
386 pub latest_ledger: u32,
387 #[serde(rename = "oldestLedger")]
388 pub oldest_ledger: u32,
389 #[serde(rename = "ledgerRetentionWindow")]
390 pub ledger_retention_window: u32,
391}
392
393#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
394pub struct GetVersionInfoResponse {
395 pub version: String,
396 #[serde(rename = "commitHash")]
397 pub commmit_hash: String,
398 #[serde(rename = "buildTimestamp")]
399 pub build_timestamp: String,
400 #[serde(rename = "captiveCoreVersion")]
401 pub captive_core_version: String,
402 #[serde(rename = "protocolVersion")]
403 pub protocol_version: u32,
404}
405
406#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
407pub struct GetLatestLedgerResponse {
408 pub id: String,
409 #[serde(rename = "protocolVersion")]
410 pub protocol_version: u32,
411 pub sequence: u32,
412}
413
414#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
415pub struct GetFeeStatsResponse {
416 #[serde(rename = "sorobanInclusionFee")]
417 pub soroban_inclusion_fee: FeeStat,
418 #[serde(rename = "inclusionFee")]
419 pub inclusion_fee: FeeStat,
420 #[serde(
421 rename = "latestLedger",
422 deserialize_with = "deserialize_number_from_string"
423 )]
424 pub latest_ledger: u32,
425}
426
427#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
428pub struct FeeStat {
429 pub max: String,
430 pub min: String,
431 pub mode: String,
433 pub p10: String,
435 pub p20: String,
437 pub p30: String,
439 pub p40: String,
441 pub p50: String,
443 pub p60: String,
445 pub p70: String,
447 pub p80: String,
449 pub p90: String,
451 pub p95: String,
453 pub p99: String,
455 #[serde(
457 rename = "transactionCount",
458 deserialize_with = "deserialize_number_from_string"
459 )]
460 pub transaction_count: u32,
461 #[serde(
463 rename = "ledgerCount",
464 deserialize_with = "deserialize_number_from_string"
465 )]
466 pub ledger_count: u32,
467}
468
469#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
470pub struct Cost {
471 #[serde(
472 rename = "cpuInsns",
473 deserialize_with = "deserialize_number_from_string"
474 )]
475 pub cpu_insns: u64,
476 #[serde(
477 rename = "memBytes",
478 deserialize_with = "deserialize_number_from_string"
479 )]
480 pub mem_bytes: u64,
481}
482
483#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
484pub struct SimulateHostFunctionResultRaw {
485 #[serde(deserialize_with = "deserialize_default_from_null")]
486 pub auth: Vec<String>,
487 pub xdr: String,
488}
489
490#[derive(Debug, Clone)]
491pub struct SimulateHostFunctionResult {
492 pub auth: Vec<SorobanAuthorizationEntry>,
493 pub xdr: xdr::ScVal,
494}
495
496#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
497#[serde(tag = "type")]
498pub enum LedgerEntryChange {
499 #[serde(rename = "created")]
500 Created { key: String, after: String },
501 #[serde(rename = "deleted")]
502 Deleted { key: String, before: String },
503 #[serde(rename = "updated")]
504 Updated {
505 key: String,
506 before: String,
507 after: String,
508 },
509}
510
511#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
512pub struct SimulateTransactionResponse {
513 #[serde(
514 rename = "minResourceFee",
515 deserialize_with = "deserialize_number_from_string",
516 default
517 )]
518 pub min_resource_fee: u64,
519 #[serde(default)]
520 pub cost: Cost,
521 #[serde(skip_serializing_if = "Vec::is_empty", default)]
522 pub results: Vec<SimulateHostFunctionResultRaw>,
523 #[serde(rename = "transactionData", default)]
524 pub transaction_data: String,
525 #[serde(
526 deserialize_with = "deserialize_default_from_null",
527 skip_serializing_if = "Vec::is_empty",
528 default
529 )]
530 pub events: Vec<String>,
531 #[serde(
532 rename = "restorePreamble",
533 skip_serializing_if = "Option::is_none",
534 default
535 )]
536 pub restore_preamble: Option<RestorePreamble>,
537 #[serde(
538 rename = "stateChanges",
539 skip_serializing_if = "Option::is_none",
540 default
541 )]
542 pub state_changes: Option<Vec<LedgerEntryChange>>,
543 #[serde(rename = "latestLedger")]
544 pub latest_ledger: u32,
545 #[serde(skip_serializing_if = "Option::is_none", default)]
546 pub error: Option<String>,
547}
548
549impl SimulateTransactionResponse {
550 pub fn results(&self) -> Result<Vec<SimulateHostFunctionResult>, Error> {
553 self.results
554 .iter()
555 .map(|r| {
556 Ok(SimulateHostFunctionResult {
557 auth: r
558 .auth
559 .iter()
560 .map(|a| {
561 Ok(SorobanAuthorizationEntry::from_xdr_base64(
562 a,
563 Limits::none(),
564 )?)
565 })
566 .collect::<Result<_, Error>>()?,
567 xdr: xdr::ScVal::from_xdr_base64(&r.xdr, Limits::none())?,
568 })
569 })
570 .collect()
571 }
572
573 pub fn events(&self) -> Result<Vec<DiagnosticEvent>, Error> {
576 self.events
577 .iter()
578 .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e, Limits::none())?))
579 .collect()
580 }
581
582 pub fn transaction_data(&self) -> Result<SorobanTransactionData, Error> {
585 Ok(SorobanTransactionData::from_xdr_base64(
586 &self.transaction_data,
587 Limits::none(),
588 )?)
589 }
590}
591
592#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
593pub struct RestorePreamble {
594 #[serde(rename = "transactionData")]
595 pub transaction_data: String,
596 #[serde(
597 rename = "minResourceFee",
598 deserialize_with = "deserialize_number_from_string"
599 )]
600 pub min_resource_fee: u64,
601}
602
603#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
604pub struct GetEventsResponse {
605 #[serde(deserialize_with = "deserialize_default_from_null")]
606 pub events: Vec<Event>,
607 #[serde(rename = "latestLedger")]
608 pub latest_ledger: u32,
609 #[serde(rename = "latestLedgerCloseTime")]
610 pub latest_ledger_close_time: String,
611 #[serde(rename = "oldestLedger")]
612 pub oldest_ledger: u32,
613 #[serde(rename = "oldestLedgerCloseTime")]
614 pub oldest_ledger_close_time: String,
615 pub cursor: String,
616}
617
618#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
619pub struct GetLedgersResponse {
620 #[serde(rename = "latestLedger")]
621 pub latest_ledger: u32,
622 #[serde(
623 rename = "latestLedgerCloseTime",
624 deserialize_with = "deserialize_number_from_string"
625 )]
626 pub latest_ledger_close_time: i64,
627 #[serde(rename = "oldestLedger")]
628 pub oldest_ledger: u32,
629 #[serde(rename = "oldestLedgerCloseTime")]
630 pub oldest_ledger_close_time: i64,
631 pub cursor: String,
632 pub ledgers: Vec<Ledger>,
633}
634
635#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
636pub struct Ledger {
637 pub hash: String,
638 pub sequence: u32,
639 #[serde(rename = "ledgerCloseTime")]
640 pub ledger_close_time: String,
641 #[serde(rename = "headerXdr")]
642 pub header_xdr: String,
643 #[serde(rename = "headerJson")]
644 pub header_json: Option<LedgerHeaderHistoryEntry>,
645 #[serde(rename = "metadataXdr")]
646 pub metadata_xdr: String,
647 #[serde(rename = "metadataJson")]
648 pub metadata_json: Option<LedgerCloseMeta>,
649}
650
651#[must_use]
667pub fn does_topic_match(topic: &[String], filter: &[String]) -> bool {
668 filter.len() == topic.len()
669 && filter
670 .iter()
671 .enumerate()
672 .all(|(i, s)| *s == "*" || topic[i] == *s)
673}
674
675#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
676pub struct Event {
677 #[serde(rename = "type")]
678 pub event_type: String,
679
680 pub ledger: u32,
681 #[serde(rename = "ledgerClosedAt")]
682 pub ledger_closed_at: String,
683
684 pub id: String,
685
686 #[serde(rename = "contractId")]
687 pub contract_id: String,
688 pub topic: Vec<String>,
689 pub value: String,
690}
691
692impl Display for Event {
693 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
694 writeln!(
695 f,
696 "Event {} [{}]:",
697 self.id,
698 self.event_type.to_ascii_uppercase()
699 )?;
700 writeln!(
701 f,
702 " Ledger: {} (closed at {})",
703 self.ledger, self.ledger_closed_at
704 )?;
705 writeln!(f, " Contract: {}", self.contract_id)?;
706 writeln!(f, " Topics:")?;
707
708 for topic in &self.topic {
709 let scval =
710 xdr::ScVal::from_xdr_base64(topic, Limits::none()).map_err(|_| std::fmt::Error)?;
711 writeln!(f, " {scval:?}")?;
712 }
713
714 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())
715 .map_err(|_| std::fmt::Error)?;
716
717 writeln!(f, " Value: {scval:?}")
718 }
719}
720
721impl Event {
722 pub fn parse_cursor(&self) -> Result<(u64, i32), Error> {
725 parse_cursor(&self.id)
726 }
727
728 pub fn pretty_print(&self) -> Result<(), Box<dyn std::error::Error>> {
731 let mut stdout = StandardStream::stdout(ColorChoice::Auto);
732
733 if !stdout.supports_color() {
734 println!("{self}");
735 return Ok(());
736 }
737
738 let color = match self.event_type.as_str() {
739 "system" => Color::Yellow,
740 _ => Color::Blue,
741 };
742 colored!(
743 stdout,
744 "{}Event{} {}{}{} [{}{}{}{}]:\n",
745 bold!(true),
746 bold!(false),
747 fg!(Some(Color::Green)),
748 self.id,
749 reset!(),
750 bold!(true),
751 fg!(Some(color)),
752 self.event_type.to_ascii_uppercase(),
753 reset!(),
754 )?;
755
756 colored!(
757 stdout,
758 " Ledger: {}{}{} (closed at {}{}{})\n",
759 fg!(Some(Color::Green)),
760 self.ledger,
761 reset!(),
762 fg!(Some(Color::Green)),
763 self.ledger_closed_at,
764 reset!(),
765 )?;
766
767 colored!(
768 stdout,
769 " Contract: {}{}{}\n",
770 fg!(Some(Color::Green)),
771 self.contract_id,
772 reset!(),
773 )?;
774
775 colored!(stdout, " Topics:\n")?;
776 for topic in &self.topic {
777 let scval = xdr::ScVal::from_xdr_base64(topic, Limits::none())?;
778 colored!(
779 stdout,
780 " {}{:?}{}\n",
781 fg!(Some(Color::Green)),
782 scval,
783 reset!(),
784 )?;
785 }
786
787 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())?;
788 colored!(
789 stdout,
790 " Value: {}{:?}{}\n\n",
791 fg!(Some(Color::Green)),
792 scval,
793 reset!(),
794 )?;
795
796 Ok(())
797 }
798}
799
800pub enum AuthMode {
802 Enforce,
803 Record,
804 RecordAllowNonRoot,
805}
806
807#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
808pub enum EventType {
809 All,
810 Contract,
811 System,
812}
813
814#[derive(Clone, Debug, Eq, Hash, PartialEq)]
815pub enum LedgerStart {
816 Ledger(u32),
817 Cursor(String),
818}
819
820#[derive(Clone, Debug, Eq, Hash, PartialEq)]
821pub enum EventStart {
822 Ledger(u32),
823 Cursor(String),
824}
825
826#[derive(Debug, Clone)]
827pub struct FullLedgerEntry {
828 pub key: LedgerKey,
829 pub val: LedgerEntryData,
830 pub last_modified_ledger: u32,
831 pub live_until_ledger_seq: u32,
832}
833
834#[derive(Debug, Clone)]
835pub struct FullLedgerEntries {
836 pub entries: Vec<FullLedgerEntry>,
837 pub latest_ledger: i64,
838}
839
840#[derive(Debug, Clone)]
841pub struct Client {
842 base_url: Arc<str>,
843 timeout_in_secs: u64,
844 http_client: Arc<HttpClient>,
845}
846
847#[allow(deprecated)] impl Client {
849 pub fn new(base_url: &str) -> Result<Self, Error> {
852 let uri = base_url.parse::<Uri>().map_err(Error::InvalidRpcUrl)?;
857 let mut parts = uri.into_parts();
858
859 if let (Some(scheme), Some(authority)) = (&parts.scheme, &parts.authority) {
860 if authority.port().is_none() {
861 let port = match scheme.as_str() {
862 "http" => Some(80),
863 "https" => Some(443),
864 _ => None,
865 };
866 if let Some(port) = port {
867 let host = authority.host();
868 parts.authority = Some(
869 Authority::from_str(&format!("{host}:{port}"))
870 .map_err(Error::InvalidRpcUrl)?,
871 );
872 }
873 }
874 }
875
876 let uri = Uri::from_parts(parts).map_err(Error::InvalidRpcUrlFromUriParts)?;
877 let base_url = Arc::from(uri.to_string());
878 let headers = Self::default_http_headers();
879 let http_client = Arc::new(
880 HttpClientBuilder::default()
881 .set_headers(headers)
882 .build(&base_url)?,
883 );
884
885 Ok(Self {
886 base_url,
887 timeout_in_secs: 30,
888 http_client,
889 })
890 }
891
892 #[deprecated(
895 note = "To be marked private in a future major release. Please use `new_with_headers` instead."
896 )]
897 pub fn new_with_timeout(base_url: &str, timeout: u64) -> Result<Self, Error> {
898 let mut client = Self::new(base_url)?;
899 client.timeout_in_secs = timeout;
900 Ok(client)
901 }
902
903 pub fn new_with_headers(base_url: &str, additional_headers: HeaderMap) -> Result<Self, Error> {
906 let mut client = Self::new(base_url)?;
907 let mut headers = Self::default_http_headers();
908
909 for (key, value) in additional_headers {
910 headers.insert(key.ok_or(Error::InvalidResponse)?, value);
911 }
912
913 let http_client = Arc::new(
914 HttpClientBuilder::default()
915 .set_headers(headers)
916 .build(base_url)?,
917 );
918
919 client.http_client = http_client;
920 Ok(client)
921 }
922
923 fn default_http_headers() -> HeaderMap {
924 let mut headers = HeaderMap::new();
925 headers.insert("X-Client-Name", unsafe {
926 "rs-stellar-rpc-client".parse().unwrap_unchecked()
927 });
928 let version = VERSION.unwrap_or("devel");
929 headers.insert("X-Client-Version", unsafe {
930 version.parse().unwrap_unchecked()
931 });
932
933 headers
934 }
935
936 #[must_use]
937 pub fn base_url(&self) -> &str {
938 &self.base_url
939 }
940
941 #[must_use]
942 pub fn client(&self) -> &HttpClient {
943 &self.http_client
944 }
945
946 pub async fn friendbot_url(&self) -> Result<String, Error> {
949 let network = self.get_network().await?;
950 network.friendbot_url.ok_or_else(|| {
951 Error::NotFound(
952 "Friendbot".to_string(),
953 "Friendbot is not available on this network".to_string(),
954 )
955 })
956 }
957 pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result<String, Error> {
960 let server = self.get_network().await?.passphrase;
961
962 if let Some(expected) = expected {
963 if expected != server {
964 return Err(Error::InvalidNetworkPassphrase {
965 expected: expected.to_string(),
966 server,
967 });
968 }
969 }
970
971 Ok(server)
972 }
973
974 pub async fn get_network(&self) -> Result<GetNetworkResponse, Error> {
977 Ok(self
978 .client()
979 .request("getNetwork", ObjectParams::new())
980 .await?)
981 }
982
983 pub async fn get_health(&self) -> Result<GetHealthResponse, Error> {
986 Ok(self
987 .client()
988 .request("getHealth", ObjectParams::new())
989 .await?)
990 }
991
992 pub async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, Error> {
995 Ok(self
996 .client()
997 .request("getLatestLedger", ObjectParams::new())
998 .await?)
999 }
1000
1001 pub async fn get_ledgers(
1004 &self,
1005 start: LedgerStart,
1006 limit: Option<usize>,
1007 format: Option<String>,
1008 ) -> Result<GetLedgersResponse, Error> {
1009 let mut oparams = ObjectParams::new();
1010
1011 let mut pagination = serde_json::Map::new();
1012 if let Some(limit) = limit {
1013 pagination.insert("limit".to_string(), limit.into());
1014 }
1015
1016 match start {
1017 LedgerStart::Ledger(l) => oparams.insert("startLedger", l)?,
1018 LedgerStart::Cursor(c) => {
1019 pagination.insert("cursor".to_string(), c.into());
1020 }
1021 };
1022
1023 oparams.insert("pagination", pagination)?;
1024
1025 if let Some(f) = format {
1026 oparams.insert("xdrFormat", f)?;
1027 }
1028
1029 Ok(self.client().request("getLedgers", oparams).await?)
1030 }
1031
1032 pub async fn get_account(&self, address: &str) -> Result<AccountEntry, Error> {
1035 let key = LedgerKey::Account(LedgerKeyAccount {
1036 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
1037 stellar_strkey::ed25519::PublicKey::from_string(address)?.0,
1038 ))),
1039 });
1040 let keys = Vec::from([key]);
1041 let response = self.get_ledger_entries(&keys).await?;
1042 let entries = response.entries.unwrap_or_default();
1043
1044 if entries.is_empty() {
1045 return Err(Error::NotFound("Account".to_string(), address.to_owned()));
1046 }
1047
1048 let ledger_entry = &entries[0];
1049 let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none());
1050
1051 if let LedgerEntryData::Account(entry) = LedgerEntryData::read_xdr_base64(&mut read)? {
1052 Ok(entry)
1053 } else {
1054 Err(Error::InvalidResponse)
1055 }
1056 }
1057
1058 pub async fn get_fee_stats(&self) -> Result<GetFeeStatsResponse, Error> {
1061 Ok(self
1062 .client()
1063 .request("getFeeStats", ObjectParams::new())
1064 .await?)
1065 }
1066
1067 pub async fn get_version_info(&self) -> Result<GetVersionInfoResponse, Error> {
1070 Ok(self
1071 .client()
1072 .request("getVersionInfo", ObjectParams::new())
1073 .await?)
1074 }
1075
1076 pub async fn send_transaction(&self, tx: &TransactionEnvelope) -> Result<Hash, Error> {
1079 let mut oparams = ObjectParams::new();
1080 oparams.insert("transaction", tx.to_xdr_base64(Limits::none())?)?;
1081 let SendTransactionResponse {
1082 hash,
1083 error_result_xdr,
1084 status,
1085 ..
1086 } = self
1087 .client()
1088 .request("sendTransaction", oparams)
1089 .await
1090 .map_err(|err| {
1091 Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}"))
1092 })?;
1093
1094 if status == "ERROR" {
1095 let error = error_result_xdr
1096 .ok_or(Error::MissingError)
1097 .and_then(|x| {
1098 TransactionResult::read_xdr_base64(&mut Limited::new(
1099 x.as_bytes(),
1100 Limits::none(),
1101 ))
1102 .map_err(|_| Error::InvalidResponse)
1103 })
1104 .map(|r| r.result)?;
1105
1106 return Err(Error::TransactionSubmissionFailed(format!("{error:#?}")));
1107 }
1108
1109 Ok(Hash::from_str(&hash)?)
1110 }
1111
1112 pub async fn send_transaction_polling(
1115 &self,
1116 tx: &TransactionEnvelope,
1117 ) -> Result<GetTransactionResponse, Error> {
1118 let hash = self.send_transaction(tx).await?;
1119 self.get_transaction_polling(&hash, None).await
1120 }
1121
1122 pub async fn simulate_transaction_envelope(
1125 &self,
1126 tx: &TransactionEnvelope,
1127 auth_mode: Option<AuthMode>,
1128 ) -> Result<SimulateTransactionResponse, Error> {
1129 let base64_tx = tx.to_xdr_base64(Limits::none())?;
1130 let mut params = ObjectParams::new();
1131
1132 params.insert("transaction", base64_tx)?;
1133
1134 match auth_mode {
1135 Some(AuthMode::Enforce) => {
1136 params.insert("authMode", "enforce")?;
1137 }
1138 Some(AuthMode::Record) => {
1139 params.insert("authMode", "record")?;
1140 }
1141 Some(AuthMode::RecordAllowNonRoot) => {
1142 params.insert("authMode", "record_allow_nonroot")?;
1143 }
1144 None => {}
1145 }
1146
1147 let sim_res = self.client().request("simulateTransaction", params).await?;
1148
1149 Ok(sim_res)
1150 }
1151
1152 pub async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, Error> {
1155 let mut oparams = ObjectParams::new();
1156 oparams.insert("hash", tx_id)?;
1157 let resp: GetTransactionResponseRaw =
1158 self.client().request("getTransaction", oparams).await?;
1159
1160 Ok(resp.try_into()?)
1161 }
1162
1163 pub async fn get_transactions(
1166 &self,
1167 request: GetTransactionsRequest,
1168 ) -> Result<GetTransactionsResponse, Error> {
1169 let mut oparams = ObjectParams::new();
1170
1171 if let Some(start_ledger) = request.start_ledger {
1172 oparams.insert("startLedger", start_ledger)?;
1173 }
1174
1175 if let Some(pagination_params) = request.pagination {
1176 let pagination = serde_json::json!(pagination_params);
1177 oparams.insert("pagination", pagination)?;
1178 }
1179
1180 let resp: GetTransactionsResponseRaw =
1181 self.client().request("getTransactions", oparams).await?;
1182
1183 Ok(resp.try_into()?)
1184 }
1185
1186 pub async fn get_transaction_polling(
1196 &self,
1197 tx_id: &Hash,
1198 timeout_s: Option<Duration>,
1199 ) -> Result<GetTransactionResponse, Error> {
1200 let start = Instant::now();
1202 let timeout = timeout_s.unwrap_or(Duration::from_secs(self.timeout_in_secs));
1203 let exponential_backoff: f64 = 1.0 / (1.0 - E.powf(-1.0));
1206 let mut sleep_time = Duration::from_secs(1);
1207 loop {
1208 let response = self.get_transaction(tx_id).await?;
1209 match response.status.as_str() {
1210 "SUCCESS" => return Ok(response),
1211
1212 "FAILED" => {
1213 return Err(Error::TransactionSubmissionFailed(format!(
1214 "{:#?}",
1215 response.result
1216 )))
1217 }
1218
1219 "NOT_FOUND" => (),
1220 _ => {
1221 return Err(Error::UnexpectedTransactionStatus(response.status));
1222 }
1223 }
1224
1225 if start.elapsed() > timeout {
1226 return Err(Error::TransactionSubmissionTimeout);
1227 }
1228
1229 sleep(sleep_time).await;
1230 sleep_time = Duration::from_secs_f64(sleep_time.as_secs_f64() * exponential_backoff);
1231 }
1232 }
1233
1234 pub async fn get_ledger_entries(
1237 &self,
1238 keys: &[LedgerKey],
1239 ) -> Result<GetLedgerEntriesResponse, Error> {
1240 let mut base64_keys: Vec<String> = vec![];
1241
1242 for k in keys {
1243 let base64_result = k.to_xdr_base64(Limits::none());
1244 if base64_result.is_err() {
1245 return Err(Error::Xdr(XdrError::Invalid));
1246 }
1247 base64_keys.push(k.to_xdr_base64(Limits::none())?);
1248 }
1249
1250 let mut oparams = ObjectParams::new();
1251 oparams.insert("keys", base64_keys)?;
1252
1253 Ok(self.client().request("getLedgerEntries", oparams).await?)
1254 }
1255
1256 pub async fn get_full_ledger_entries(
1259 &self,
1260 ledger_keys: &[LedgerKey],
1261 ) -> Result<FullLedgerEntries, Error> {
1262 let keys = ledger_keys
1263 .iter()
1264 .filter(|key| !matches!(key, LedgerKey::Ttl(_)))
1265 .map(Clone::clone)
1266 .collect::<Vec<_>>();
1267 let GetLedgerEntriesResponse {
1268 entries,
1269 latest_ledger,
1270 } = self.get_ledger_entries(&keys).await?;
1271 let entries = entries
1272 .unwrap_or_default()
1273 .iter()
1274 .map(
1275 |LedgerEntryResult {
1276 key,
1277 xdr,
1278 last_modified_ledger,
1279 live_until_ledger_seq_ledger_seq,
1280 }| {
1281 Ok(FullLedgerEntry {
1282 key: LedgerKey::from_xdr_base64(key, Limits::none())?,
1283 val: LedgerEntryData::from_xdr_base64(xdr, Limits::none())?,
1284 live_until_ledger_seq: live_until_ledger_seq_ledger_seq.unwrap_or_default(),
1285 last_modified_ledger: *last_modified_ledger,
1286 })
1287 },
1288 )
1289 .collect::<Result<Vec<_>, Error>>()?;
1290 Ok(FullLedgerEntries {
1291 entries,
1292 latest_ledger,
1293 })
1294 }
1295 pub async fn get_events(
1298 &self,
1299 start: EventStart,
1300 event_type: Option<EventType>,
1301 contract_ids: &[String],
1302 topics: &[String],
1303 limit: Option<usize>,
1304 ) -> Result<GetEventsResponse, Error> {
1305 let mut filters = serde_json::Map::new();
1306
1307 event_type
1308 .and_then(|t| match t {
1309 EventType::All => None, EventType::Contract => Some("contract"),
1311 EventType::System => Some("system"),
1312 })
1313 .map(|t| filters.insert("type".to_string(), t.into()));
1314
1315 filters.insert("topics".to_string(), topics.into());
1316 filters.insert("contractIds".to_string(), contract_ids.into());
1317
1318 let mut pagination = serde_json::Map::new();
1319 if let Some(limit) = limit {
1320 pagination.insert("limit".to_string(), limit.into());
1321 }
1322
1323 let mut oparams = ObjectParams::new();
1324 match start {
1325 EventStart::Ledger(l) => oparams.insert("startLedger", l)?,
1326 EventStart::Cursor(c) => {
1327 pagination.insert("cursor".to_string(), c.into());
1328 }
1329 }
1330 oparams.insert("filters", vec![filters])?;
1331 oparams.insert("pagination", pagination)?;
1332
1333 Ok(self.client().request("getEvents", oparams).await?)
1334 }
1335
1336 pub async fn get_contract_data(
1339 &self,
1340 contract_id: &[u8; 32],
1341 ) -> Result<ContractDataEntry, Error> {
1342 let contract_key = LedgerKey::ContractData(xdr::LedgerKeyContractData {
1344 contract: xdr::ScAddress::Contract(ContractId(xdr::Hash(*contract_id))),
1345 key: xdr::ScVal::LedgerKeyContractInstance,
1346 durability: xdr::ContractDataDurability::Persistent,
1347 });
1348 let contract_ref = self.get_ledger_entries(&[contract_key]).await?;
1349 let entries = contract_ref.entries.unwrap_or_default();
1350 if entries.is_empty() {
1351 let contract_address = stellar_strkey::Contract(*contract_id).to_string();
1352 return Err(Error::NotFound("Contract".to_string(), contract_address));
1353 }
1354 let contract_ref_entry = &entries[0];
1355 match LedgerEntryData::from_xdr_base64(&contract_ref_entry.xdr, Limits::none())? {
1356 LedgerEntryData::ContractData(contract_data) => Ok(contract_data),
1357 scval => Err(Error::UnexpectedContractCodeDataType(scval)),
1358 }
1359 }
1360
1361 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1364 pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result<Vec<u8>, Error> {
1365 match self.get_contract_data(contract_id).await? {
1366 xdr::ContractDataEntry {
1367 val:
1368 xdr::ScVal::ContractInstance(xdr::ScContractInstance {
1369 executable: xdr::ContractExecutable::Wasm(hash),
1370 ..
1371 }),
1372 ..
1373 } => self.get_remote_wasm_from_hash(hash).await,
1374 scval => Err(Error::UnexpectedToken(scval)),
1375 }
1376 }
1377
1378 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1381 pub async fn get_remote_wasm_from_hash(&self, hash: Hash) -> Result<Vec<u8>, Error> {
1382 let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
1383 let contract_data = self.get_ledger_entries(&[code_key]).await?;
1384 let entries = contract_data.entries.unwrap_or_default();
1385 if entries.is_empty() {
1386 return Err(Error::NotFound(
1387 "Contract Code".to_string(),
1388 hex::encode(hash),
1389 ));
1390 }
1391 let contract_data_entry = &entries[0];
1392 match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
1393 LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
1394 scval => Err(Error::UnexpectedContractCodeDataType(scval)),
1395 }
1396 }
1397
1398 pub async fn get_contract_instance(
1403 &self,
1404 contract_id: &[u8; 32],
1405 ) -> Result<ScContractInstance, Error> {
1406 let contract_data = self.get_contract_data(contract_id).await?;
1407 match contract_data.val {
1408 xdr::ScVal::ContractInstance(instance) => Ok(instance),
1409 scval => Err(Error::UnexpectedContractInstance(scval)),
1410 }
1411 }
1412}
1413
1414pub(crate) fn parse_cursor(c: &str) -> Result<(u64, i32), Error> {
1415 let (toid_part, event_index) = c.split('-').collect_tuple().ok_or(Error::InvalidCursor)?;
1416 let toid_part: u64 = toid_part.parse().map_err(|_| Error::InvalidCursor)?;
1417 let start_index: i32 = event_index.parse().map_err(|_| Error::InvalidCursor)?;
1418 Ok((toid_part, start_index))
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423 use super::*;
1424 use std::env;
1425 use std::fs;
1426 use std::path::PathBuf;
1427
1428 fn get_repo_root() -> PathBuf {
1429 let mut path = env::current_exe().expect("Failed to get current executable path");
1430 while path.pop() {
1432 if path.join("Cargo.toml").exists() {
1433 return path;
1434 }
1435 }
1436 panic!("Could not find repository root");
1437 }
1438
1439 fn read_json_file(name: &str) -> String {
1440 let repo_root = get_repo_root();
1441 let fixture_path = repo_root.join("src").join("fixtures").join(name);
1442 fs::read_to_string(fixture_path).expect(&format!("Failed to read {name:?}"))
1443 }
1444
1445 #[test]
1446 fn simulation_transaction_response_parsing() {
1447 let s = r#"{
1448 "minResourceFee": "100000000",
1449 "cost": { "cpuInsns": "1000", "memBytes": "1000" },
1450 "transactionData": "",
1451 "latestLedger": 1234,
1452 "stateChanges": [{
1453 "type": "created",
1454 "key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==",
1455 "before": null,
1456 "after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
1457 }]
1458 }"#;
1459
1460 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1461 assert_eq!(
1462 resp.state_changes.unwrap()[0],
1463 LedgerEntryChange::Created { key: "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==".to_string(), after: "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string() },
1464 );
1465 assert_eq!(resp.min_resource_fee, 100_000_000);
1466 }
1467
1468 #[test]
1469 fn simulation_transaction_response_parsing_mostly_empty() {
1470 let s = r#"{
1471 "latestLedger": 1234
1472 }"#;
1473
1474 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1475 assert_eq!(resp.latest_ledger, 1_234);
1476 }
1477
1478 #[test]
1479 fn test_parse_transaction_response_p23() {
1480 let response_content = read_json_file("transaction_response_p23.json");
1481 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1482 .expect("Failed to parse JSON from transaction_response_p23.json");
1483 let result = full_response["result"].clone();
1484 let raw_response: GetTransactionResponseRaw = serde_json::from_value(result)
1485 .expect("Failed to parse 'result' into GetTransactionResponseRaw");
1486 let response: GetTransactionResponse = raw_response
1487 .try_into()
1488 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1489
1490 assert_eq!(2, response.events.transaction_events.iter().len());
1491 assert_eq!(1, response.events.contract_events.len());
1492 assert_eq!(21, response.events.diagnostic_events.iter().len());
1493 }
1494
1495 #[test]
1496 fn test_parse_transaction_response_p22() {
1497 let response_content = read_json_file("transaction_response_p22.json");
1498 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1499 .expect("Failed to parse JSON from transaction_response_p22.json");
1500 let result = full_response["result"].clone();
1501 let raw_response: GetTransactionResponseRaw = serde_json::from_value(result)
1502 .expect("Failed to parse 'result' into GetTransactionResponseRaw");
1503 let response: GetTransactionResponse = raw_response
1504 .try_into()
1505 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1506
1507 assert_eq!(23, response.events.diagnostic_events.iter().len());
1508 }
1509
1510 #[test]
1511 fn test_parse_get_transactions_response() {
1512 let response_content = read_json_file("transactions_response.json");
1513
1514 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1516 .expect("Failed to parse JSON from transactions_response.json");
1517
1518 let result = full_response["result"].clone();
1520 let raw_response: GetTransactionsResponseRaw = serde_json::from_value(result)
1522 .expect("Failed to parse 'result' into GetTransactionsResponseRaw");
1523
1524 let response: GetTransactionsResponse = raw_response
1526 .try_into()
1527 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1528
1529 assert_eq!(response.transactions.len(), 5);
1531 assert_eq!(response.latest_ledger, 556_962);
1532 assert_eq!(response.cursor, 2_379_420_471_922_689);
1533
1534 assert_eq!(response.transactions[0].status, "SUCCESS");
1536 }
1539
1540 #[test]
1541 fn test_rpc_url_default_ports() {
1542 let client = Client::new("http://example.com").unwrap();
1544 assert_eq!(client.base_url(), "http://example.com:80/");
1545 let client = Client::new("https://example.com").unwrap();
1546 assert_eq!(client.base_url(), "https://example.com:443/");
1547
1548 let client = Client::new("http://example.com:8080").unwrap();
1550 assert_eq!(client.base_url(), "http://example.com:8080/");
1551 let client = Client::new("https://example.com:8080").unwrap();
1552 assert_eq!(client.base_url(), "https://example.com:8080/");
1553
1554 let client = Client::new("http://example.com/a/b/c").unwrap();
1556 assert_eq!(client.base_url(), "http://example.com:80/a/b/c");
1557 let client = Client::new("https://example.com/a/b/c").unwrap();
1558 assert_eq!(client.base_url(), "https://example.com:443/a/b/c");
1559 let client = Client::new("http://example.com/a/b/c/").unwrap();
1560 assert_eq!(client.base_url(), "http://example.com:80/a/b/c/");
1561 let client = Client::new("https://example.com/a/b/c/").unwrap();
1562 assert_eq!(client.base_url(), "https://example.com:443/a/b/c/");
1563 let client = Client::new("http://example.com/a/b:80/c/").unwrap();
1564 assert_eq!(client.base_url(), "http://example.com:80/a/b:80/c/");
1565 let client = Client::new("https://example.com/a/b:80/c/").unwrap();
1566 assert_eq!(client.base_url(), "https://example.com:443/a/b:80/c/");
1567 }
1568
1569 #[test]
1570 fn test_does_topic_match() {
1573 struct TestCase<'a> {
1574 name: &'a str,
1575 filter: Vec<&'a str>,
1576 includes: Vec<Vec<&'a str>>,
1577 excludes: Vec<Vec<&'a str>>,
1578 }
1579
1580 let xfer = "AAAABQAAAAh0cmFuc2Zlcg==";
1581 let number = "AAAAAQB6Mcc=";
1582 let star = "*";
1583
1584 for tc in vec![
1585 TestCase {
1587 name: "<empty>",
1588 filter: vec![],
1589 includes: vec![],
1590 excludes: vec![vec![xfer]],
1591 },
1592 TestCase {
1596 name: "*",
1597 filter: vec![star],
1598 includes: vec![vec![xfer]],
1599 excludes: vec![vec![xfer, xfer], vec![xfer, number]],
1600 },
1601 TestCase {
1604 name: "*/transfer",
1605 filter: vec![star, xfer],
1606 includes: vec![vec![number, xfer], vec![xfer, xfer]],
1607 excludes: vec![
1608 vec![number],
1609 vec![number, number],
1610 vec![number, xfer, number],
1611 vec![xfer],
1612 vec![xfer, number],
1613 vec![xfer, xfer, xfer],
1614 ],
1615 },
1616 TestCase {
1620 name: "transfer/*",
1621 filter: vec![xfer, star],
1622 includes: vec![vec![xfer, number], vec![xfer, xfer]],
1623 excludes: vec![
1624 vec![number],
1625 vec![number, number],
1626 vec![number, xfer, number],
1627 vec![xfer],
1628 vec![number, xfer],
1629 vec![xfer, xfer, xfer],
1630 ],
1631 },
1632 TestCase {
1634 name: "transfer/*/*",
1635 filter: vec![xfer, star, star],
1636 includes: vec![vec![xfer, number, number], vec![xfer, xfer, xfer]],
1637 excludes: vec![
1638 vec![number],
1639 vec![number, number],
1640 vec![number, xfer],
1641 vec![number, xfer, number, number],
1642 vec![xfer],
1643 vec![xfer, xfer, xfer, xfer],
1644 ],
1645 },
1646 TestCase {
1650 name: "transfer/*/number",
1651 filter: vec![xfer, star, number],
1652 includes: vec![vec![xfer, number, number], vec![xfer, xfer, number]],
1653 excludes: vec![
1654 vec![number],
1655 vec![number, number],
1656 vec![number, number, number],
1657 vec![number, xfer, number],
1658 vec![xfer],
1659 vec![number, xfer],
1660 vec![xfer, xfer, xfer],
1661 vec![xfer, number, xfer],
1662 ],
1663 },
1664 ] {
1665 for topic in tc.includes {
1666 assert!(
1667 does_topic_match(
1668 &topic
1669 .iter()
1670 .map(std::string::ToString::to_string)
1671 .collect::<Vec<String>>(),
1672 &tc.filter
1673 .iter()
1674 .map(std::string::ToString::to_string)
1675 .collect::<Vec<String>>()
1676 ),
1677 "test: {}, topic ({:?}) should be matched by filter ({:?})",
1678 tc.name,
1679 topic,
1680 tc.filter
1681 );
1682 }
1683
1684 for topic in tc.excludes {
1685 assert!(
1686 !does_topic_match(
1687 &topic
1689 .iter()
1690 .map(std::string::ToString::to_string)
1691 .collect::<Vec<String>>(),
1692 &tc.filter
1693 .iter()
1694 .map(std::string::ToString::to_string)
1695 .collect::<Vec<String>>()
1696 ),
1697 "test: {}, topic ({:?}) should NOT be matched by filter ({:?})",
1698 tc.name,
1699 topic,
1700 tc.filter
1701 );
1702 }
1703 }
1704 }
1705}