1use itertools::Itertools;
2use serde_aux::prelude::{
3 deserialize_default_from_null, deserialize_number_from_string,
4 deserialize_option_number_from_string,
5};
6use serde_with::{serde_as, DisplayFromStr};
7use stellar_xdr::curr::{
8 self as xdr, AccountEntry, AccountId, ContractDataEntry, ContractEvent, ContractId,
9 DiagnosticEvent, Error as XdrError, Hash, LedgerCloseMeta, LedgerEntryData, LedgerFootprint,
10 LedgerHeaderHistoryEntry, LedgerKey, LedgerKeyAccount, Limited, Limits, PublicKey, ReadXdr,
11 ScContractInstance, SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData,
12 TransactionEnvelope, TransactionEvent, TransactionMetaV3, TransactionResult, Uint256, VecM,
13 WriteXdr,
14};
15use wasi_jsonrpsee_core::params::ObjectParams;
16use wasi_jsonrpsee_core::{self, client::ClientT};
17use wasi_jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder};
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 friendbot url: {0}")]
53 InvalidUrl(String),
54 #[error(transparent)]
55 JsonRpc(#[from] wasi_jsonrpsee_core::ClientError),
56 #[error("json decoding error: {0}")]
57 Serde(#[from] serde_json::Error),
58 #[error("transaction failed: {0}")]
59 TransactionFailed(String),
60 #[error("transaction submission failed: {0}")]
61 TransactionSubmissionFailed(String),
62 #[error("expected transaction status: {0}")]
63 UnexpectedTransactionStatus(String),
64 #[error("transaction submission timeout")]
65 TransactionSubmissionTimeout,
66 #[error("transaction simulation failed: {0}")]
67 TransactionSimulationFailed(String),
68 #[error("{0} not found: {1}")]
69 NotFound(String, String),
70 #[error("Missing result in successful response")]
71 MissingResult,
72 #[error("Failed to read Error response from server")]
73 MissingError,
74 #[error("Missing signing key for account {address}")]
75 MissingSignerForAddress { address: String },
76 #[error("cursor is not valid")]
77 InvalidCursor,
78 #[error("unexpected ({length}) simulate transaction result length")]
79 UnexpectedSimulateTransactionResultSize { length: usize },
80 #[error("unexpected ({count}) number of operations")]
81 UnexpectedOperationCount { count: usize },
82 #[error("Transaction contains unsupported operation type")]
83 UnsupportedOperationType,
84 #[error("unexpected contract code data type: {0:?}")]
85 UnexpectedContractCodeDataType(Box<LedgerEntryData>),
86 #[error("unexpected contract instance type: {0:?}")]
87 UnexpectedContractInstance(Box<xdr::ScVal>),
88 #[error("unexpected contract code got token {0:?}")]
89 #[deprecated(note = "To be removed in future versions")]
90 UnexpectedToken(Box<ContractDataEntry>),
91 #[error("Fee was too large {0}")]
92 LargeFee(u64),
93 #[error("Cannot authorize raw transactions")]
94 CannotAuthorizeRawTransaction,
95 #[error("Missing result for tnx")]
96 MissingOp,
97}
98
99#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
100pub struct SendTransactionResponse {
101 pub hash: String,
102 pub status: String,
103 #[serde(
104 rename = "errorResultXdr",
105 skip_serializing_if = "Option::is_none",
106 default
107 )]
108 pub error_result_xdr: Option<String>,
109 #[serde(rename = "latestLedger")]
110 pub latest_ledger: u32,
111 #[serde(
112 rename = "latestLedgerCloseTime",
113 deserialize_with = "deserialize_number_from_string"
114 )]
115 pub latest_ledger_close_time: u32,
116}
117
118#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
119pub struct GetTransactionResponseRaw {
121 pub status: String,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub ledger: Option<u32>,
125
126 #[serde(
127 rename = "applicationOrder",
128 skip_serializing_if = "Option::is_none",
129 default
130 )]
131 pub application_order: Option<u32>,
132
133 #[serde(rename = "feeBump", skip_serializing_if = "Option::is_none", default)]
134 pub fee_bump: Option<bool>,
135
136 #[serde(
137 rename = "envelopeXdr",
138 skip_serializing_if = "Option::is_none",
139 default
140 )]
141 pub envelope_xdr: Option<String>,
142
143 #[serde(rename = "resultXdr", skip_serializing_if = "Option::is_none", default)]
144 pub result_xdr: Option<String>,
145
146 #[serde(
147 rename = "resultMetaXdr",
148 skip_serializing_if = "Option::is_none",
149 default
150 )]
151 pub result_meta_xdr: Option<String>,
152
153 #[serde(rename = "txHash", skip_serializing_if = "Option::is_none", default)]
154 pub tx_hash: Option<String>,
155
156 #[serde(
157 rename = "createdAt",
158 deserialize_with = "deserialize_option_i64_from_string_or_number",
159 skip_serializing_if = "Option::is_none",
160 default
161 )]
162 pub created_at: Option<i64>,
163
164 #[serde(rename = "events", skip_serializing_if = "Option::is_none", default)]
165 pub events: Option<GetTransactionEventsRaw>,
166}
167
168#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Default)]
169pub struct GetTransactionEventsRaw {
170 #[serde(
171 rename = "contractEventsXdr",
172 skip_serializing_if = "Option::is_none",
173 default
174 )]
175 pub contract_events_xdr: Option<Vec<Vec<String>>>,
176
177 #[serde(
178 rename = "diagnosticEventsXdr",
179 skip_serializing_if = "Option::is_none",
180 default
181 )]
182 pub diagnostic_events_xdr: Option<Vec<String>>,
183
184 #[serde(
185 rename = "transactionEventsXdr",
186 skip_serializing_if = "Option::is_none",
187 default
188 )]
189 pub transaction_events_xdr: Option<Vec<String>>,
190}
191
192#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
193pub struct GetTransactionEvents {
194 pub contract_events: Vec<Vec<ContractEvent>>,
195 pub diagnostic_events: Vec<DiagnosticEvent>,
196 pub transaction_events: Vec<TransactionEvent>,
197}
198
199#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
200pub struct GetTransactionResponse {
201 pub status: String,
202 pub ledger: Option<u32>,
203 pub application_order: Option<u32>,
204 pub fee_bump: Option<bool>,
205 pub tx_hash: Option<String>,
206 pub created_at: Option<i64>,
207 pub envelope: Option<xdr::TransactionEnvelope>,
208 pub result: Option<xdr::TransactionResult>,
209 pub result_meta: Option<xdr::TransactionMeta>,
210 pub events: GetTransactionEvents,
211}
212
213impl TryInto<GetTransactionResponse> for GetTransactionResponseRaw {
214 type Error = xdr::Error;
215
216 fn try_into(self) -> Result<GetTransactionResponse, Self::Error> {
217 let events = self.events.unwrap_or_default();
218 let result_meta: Option<xdr::TransactionMeta> = self
219 .result_meta_xdr
220 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
221 .transpose()?;
222
223 let events = match result_meta {
224 Some(xdr::TransactionMeta::V4(_)) => GetTransactionEvents {
225 contract_events: events
226 .contract_events_xdr
227 .unwrap_or_default()
228 .into_iter()
229 .map(|es| {
230 es.into_iter()
231 .filter_map(|e| ContractEvent::from_xdr_base64(e, Limits::none()).ok())
232 .collect::<Vec<_>>()
233 })
234 .collect::<Vec<Vec<ContractEvent>>>(),
235
236 diagnostic_events: events
237 .diagnostic_events_xdr
238 .unwrap_or_default()
239 .iter()
240 .filter_map(|e| DiagnosticEvent::from_xdr_base64(e, Limits::none()).ok())
241 .collect(),
242
243 transaction_events: events
244 .transaction_events_xdr
245 .unwrap_or_default()
246 .iter()
247 .filter_map(|e| TransactionEvent::from_xdr_base64(e, Limits::none()).ok())
248 .collect(),
249 },
250
251 Some(xdr::TransactionMeta::V3(TransactionMetaV3 {
252 soroban_meta: Some(ref meta),
253 ..
254 })) => GetTransactionEvents {
255 contract_events: vec![],
256 transaction_events: vec![],
257 diagnostic_events: meta.diagnostic_events.clone().into(),
258 },
259
260 _ => GetTransactionEvents {
261 contract_events: vec![],
262 transaction_events: vec![],
263 diagnostic_events: vec![],
264 },
265 };
266
267 Ok(GetTransactionResponse {
268 status: self.status,
269 ledger: self.ledger,
270 application_order: self.application_order,
271 fee_bump: self.fee_bump,
272 tx_hash: self.tx_hash,
273 created_at: self.created_at,
274 envelope: self
275 .envelope_xdr
276 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
277 .transpose()?,
278 result: self
279 .result_xdr
280 .map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
281 .transpose()?,
282 result_meta,
283 events,
284 })
285 }
286}
287
288impl GetTransactionResponse {
289 pub fn return_value(&self) -> Result<xdr::ScVal, Error> {
292 if let Some(xdr::TransactionMeta::V3(xdr::TransactionMetaV3 {
293 soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }),
294 ..
295 })) = &self.result_meta
296 {
297 return Ok(return_value.clone());
298 }
299
300 if let Some(xdr::TransactionMeta::V4(xdr::TransactionMetaV4 {
301 soroban_meta:
302 Some(xdr::SorobanTransactionMetaV2 {
303 return_value: Some(return_value),
304 ..
305 }),
306 ..
307 })) = &self.result_meta
308 {
309 return Ok(return_value.clone());
310 }
311
312 Err(Error::MissingOp)
313 }
314}
315
316#[serde_as]
317#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
318pub struct GetTransactionsResponseRaw {
319 pub transactions: Vec<GetTransactionResponseRaw>,
320 #[serde(rename = "latestLedger")]
321 pub latest_ledger: u32,
322 #[serde(rename = "latestLedgerCloseTimestamp")]
323 pub latest_ledger_close_time: i64,
324 #[serde(rename = "oldestLedger")]
325 pub oldest_ledger: u32,
326 #[serde(rename = "oldestLedgerCloseTimestamp")]
327 pub oldest_ledger_close_time: i64,
328 #[serde_as(as = "DisplayFromStr")]
329 pub cursor: u64,
330}
331
332#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
333pub struct GetTransactionsResponse {
334 pub transactions: Vec<GetTransactionResponse>,
335 pub latest_ledger: u32,
336 pub latest_ledger_close_time: i64,
337 pub oldest_ledger: u32,
338 pub oldest_ledger_close_time: i64,
339 pub cursor: u64,
340}
341
342impl TryInto<GetTransactionsResponse> for GetTransactionsResponseRaw {
343 type Error = xdr::Error; fn try_into(self) -> Result<GetTransactionsResponse, Self::Error> {
346 Ok(GetTransactionsResponse {
347 transactions: self
348 .transactions
349 .into_iter()
350 .map(TryInto::try_into)
351 .collect::<Result<Vec<_>, xdr::Error>>()?,
352 latest_ledger: self.latest_ledger,
353 latest_ledger_close_time: self.latest_ledger_close_time,
354 oldest_ledger: self.oldest_ledger,
355 oldest_ledger_close_time: self.oldest_ledger_close_time,
356 cursor: self.cursor,
357 })
358 }
359}
360
361#[serde_as]
362#[derive(serde::Serialize, Debug, Clone)]
363pub struct TransactionsPaginationOptions {
364 #[serde_as(as = "Option<DisplayFromStr>")]
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub cursor: Option<u64>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub limit: Option<u32>,
369}
370
371#[derive(serde::Serialize, Debug, Clone)]
372pub struct GetTransactionsRequest {
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub start_ledger: Option<u32>,
375 pub pagination: Option<TransactionsPaginationOptions>,
376}
377
378#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
379pub struct LedgerEntryResult {
380 pub key: String,
381 pub xdr: String,
382 #[serde(rename = "lastModifiedLedgerSeq")]
383 pub last_modified_ledger: u32,
384 #[serde(
385 rename = "liveUntilLedgerSeq",
386 skip_serializing_if = "Option::is_none",
387 deserialize_with = "deserialize_option_number_from_string",
388 default
389 )]
390 pub live_until_ledger_seq_ledger_seq: Option<u32>,
391}
392
393#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
394pub struct GetLedgerEntriesResponse {
395 pub entries: Option<Vec<LedgerEntryResult>>,
396 #[serde(rename = "latestLedger")]
397 pub latest_ledger: i64,
398}
399
400#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
401pub struct GetNetworkResponse {
402 #[serde(
403 rename = "friendbotUrl",
404 skip_serializing_if = "Option::is_none",
405 default
406 )]
407 pub friendbot_url: Option<String>,
408 pub passphrase: String,
409 #[serde(rename = "protocolVersion")]
410 pub protocol_version: u32,
411}
412
413#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
414pub struct GetHealthResponse {
415 pub status: String,
416 #[serde(rename = "latestLedger")]
417 pub latest_ledger: u32,
418 #[serde(rename = "oldestLedger")]
419 pub oldest_ledger: u32,
420 #[serde(rename = "ledgerRetentionWindow")]
421 pub ledger_retention_window: u32,
422}
423
424#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
425pub struct GetVersionInfoResponse {
426 pub version: String,
427 #[serde(rename = "commitHash")]
428 pub commmit_hash: String,
429 #[serde(rename = "buildTimestamp")]
430 pub build_timestamp: String,
431 #[serde(rename = "captiveCoreVersion")]
432 pub captive_core_version: String,
433 #[serde(rename = "protocolVersion")]
434 pub protocol_version: u32,
435}
436
437#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
438pub struct GetLatestLedgerResponse {
439 pub id: String,
440 #[serde(rename = "protocolVersion")]
441 pub protocol_version: u32,
442 pub sequence: u32,
443}
444
445#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
446pub struct GetFeeStatsResponse {
447 #[serde(rename = "sorobanInclusionFee")]
448 pub soroban_inclusion_fee: FeeStat,
449 #[serde(rename = "inclusionFee")]
450 pub inclusion_fee: FeeStat,
451 #[serde(
452 rename = "latestLedger",
453 deserialize_with = "deserialize_number_from_string"
454 )]
455 pub latest_ledger: u32,
456}
457
458#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
459pub struct FeeStat {
460 pub max: String,
461 pub min: String,
462 pub mode: String,
464 pub p10: String,
466 pub p20: String,
468 pub p30: String,
470 pub p40: String,
472 pub p50: String,
474 pub p60: String,
476 pub p70: String,
478 pub p80: String,
480 pub p90: String,
482 pub p95: String,
484 pub p99: String,
486 #[serde(
488 rename = "transactionCount",
489 deserialize_with = "deserialize_number_from_string"
490 )]
491 pub transaction_count: u32,
492 #[serde(
494 rename = "ledgerCount",
495 deserialize_with = "deserialize_number_from_string"
496 )]
497 pub ledger_count: u32,
498}
499
500#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
501pub struct Cost {
502 #[serde(
503 rename = "cpuInsns",
504 deserialize_with = "deserialize_number_from_string"
505 )]
506 pub cpu_insns: u64,
507 #[serde(
508 rename = "memBytes",
509 deserialize_with = "deserialize_number_from_string"
510 )]
511 pub mem_bytes: u64,
512}
513
514#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
515pub struct SimulateHostFunctionResultRaw {
516 #[serde(deserialize_with = "deserialize_default_from_null")]
517 pub auth: Vec<String>,
518 pub xdr: String,
519}
520
521#[derive(Debug, Clone)]
522pub struct SimulateHostFunctionResult {
523 pub auth: Vec<SorobanAuthorizationEntry>,
524 pub xdr: xdr::ScVal,
525}
526
527#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
528#[serde(tag = "type")]
529pub enum LedgerEntryChange {
530 #[serde(rename = "created")]
531 Created { key: String, after: String },
532 #[serde(rename = "deleted")]
533 Deleted { key: String, before: String },
534 #[serde(rename = "updated")]
535 Updated {
536 key: String,
537 before: String,
538 after: String,
539 },
540}
541
542#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
543pub struct SimulateTransactionResponse {
544 #[serde(
545 rename = "minResourceFee",
546 deserialize_with = "deserialize_number_from_string",
547 default
548 )]
549 pub min_resource_fee: u64,
550 #[serde(default)]
551 pub cost: Cost,
552 #[serde(skip_serializing_if = "Vec::is_empty", default)]
553 pub results: Vec<SimulateHostFunctionResultRaw>,
554 #[serde(rename = "transactionData", default)]
555 pub transaction_data: String,
556 #[serde(
557 deserialize_with = "deserialize_default_from_null",
558 skip_serializing_if = "Vec::is_empty",
559 default
560 )]
561 pub events: Vec<String>,
562 #[serde(
563 rename = "restorePreamble",
564 skip_serializing_if = "Option::is_none",
565 default
566 )]
567 pub restore_preamble: Option<RestorePreamble>,
568 #[serde(
569 rename = "stateChanges",
570 skip_serializing_if = "Option::is_none",
571 default
572 )]
573 pub state_changes: Option<Vec<LedgerEntryChange>>,
574 #[serde(rename = "latestLedger")]
575 pub latest_ledger: u32,
576 #[serde(skip_serializing_if = "Option::is_none", default)]
577 pub error: Option<String>,
578}
579
580impl SimulateTransactionResponse {
581 pub fn results(&self) -> Result<Vec<SimulateHostFunctionResult>, Error> {
584 self.results
585 .iter()
586 .map(|r| {
587 Ok(SimulateHostFunctionResult {
588 auth: r
589 .auth
590 .iter()
591 .map(|a| {
592 Ok(SorobanAuthorizationEntry::from_xdr_base64(
593 a,
594 Limits::none(),
595 )?)
596 })
597 .collect::<Result<_, Error>>()?,
598 xdr: xdr::ScVal::from_xdr_base64(&r.xdr, Limits::none())?,
599 })
600 })
601 .collect()
602 }
603
604 pub fn events(&self) -> Result<Vec<DiagnosticEvent>, Error> {
607 self.events
608 .iter()
609 .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e, Limits::none())?))
610 .collect()
611 }
612
613 pub fn transaction_data(&self) -> Result<SorobanTransactionData, Error> {
616 Ok(SorobanTransactionData::from_xdr_base64(
617 &self.transaction_data,
618 Limits::none(),
619 )?)
620 }
621}
622
623#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
624pub struct RestorePreamble {
625 #[serde(rename = "transactionData")]
626 pub transaction_data: String,
627 #[serde(
628 rename = "minResourceFee",
629 deserialize_with = "deserialize_number_from_string"
630 )]
631 pub min_resource_fee: u64,
632}
633
634#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
635pub struct GetEventsResponse {
636 #[serde(deserialize_with = "deserialize_default_from_null")]
637 pub events: Vec<Event>,
638 #[serde(rename = "latestLedger")]
639 pub latest_ledger: u32,
640 #[serde(rename = "latestLedgerCloseTime")]
641 pub latest_ledger_close_time: String,
642 #[serde(rename = "oldestLedger")]
643 pub oldest_ledger: u32,
644 #[serde(rename = "oldestLedgerCloseTime")]
645 pub oldest_ledger_close_time: String,
646 pub cursor: String,
647}
648
649#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
650pub struct GetLedgersResponse {
651 #[serde(rename = "latestLedger")]
652 pub latest_ledger: u32,
653 #[serde(
654 rename = "latestLedgerCloseTime",
655 deserialize_with = "deserialize_number_from_string"
656 )]
657 pub latest_ledger_close_time: i64,
658 #[serde(rename = "oldestLedger")]
659 pub oldest_ledger: u32,
660 #[serde(rename = "oldestLedgerCloseTime")]
661 pub oldest_ledger_close_time: i64,
662 pub cursor: String,
663 pub ledgers: Vec<Ledger>,
664}
665
666#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
667pub struct Ledger {
668 pub hash: String,
669 pub sequence: u32,
670 #[serde(rename = "ledgerCloseTime")]
671 pub ledger_close_time: String,
672 #[serde(rename = "headerXdr")]
673 pub header_xdr: String,
674 #[serde(rename = "headerJson")]
675 pub header_json: Option<LedgerHeaderHistoryEntry>,
676 #[serde(rename = "metadataXdr")]
677 pub metadata_xdr: String,
678 #[serde(rename = "metadataJson")]
679 pub metadata_json: Option<LedgerCloseMeta>,
680}
681
682#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
683pub struct Event {
684 #[serde(rename = "type")]
685 pub event_type: String,
686
687 pub ledger: u32,
688 #[serde(rename = "ledgerClosedAt")]
689 pub ledger_closed_at: String,
690 #[serde(rename = "contractId")]
691 pub contract_id: String,
692
693 pub id: String,
694
695 #[serde(
696 rename = "operationIndex",
697 default,
698 skip_serializing_if = "Option::is_none"
699 )]
700 pub operation_index: Option<u32>,
701 #[serde(
702 rename = "transactionIndex",
703 default,
704 skip_serializing_if = "Option::is_none"
705 )]
706 pub transaction_index: Option<u32>,
707 #[serde(rename = "txHash", default, skip_serializing_if = "Option::is_none")]
708 pub tx_hash: Option<String>,
709 #[deprecated(
710 note = "This field is deprecated by Stellar RPC. See https://stellar.org/blog/developers/protocol-23-upgrade-guide"
711 )]
712 #[serde(
713 rename = "inSuccessfulContractCall",
714 default,
715 skip_serializing_if = "Option::is_none"
716 )]
717 pub is_successful_contract_call: Option<bool>,
718
719 pub topic: Vec<String>,
720 pub value: String,
721}
722
723impl Display for Event {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 writeln!(
726 f,
727 "Event {} [{}]:",
728 self.id,
729 self.event_type.to_ascii_uppercase()
730 )?;
731 writeln!(
732 f,
733 " Ledger: {} (closed at {})",
734 self.ledger, self.ledger_closed_at
735 )?;
736 writeln!(f, " Contract: {}", self.contract_id)?;
737 writeln!(f, " Topics:")?;
738
739 for topic in &self.topic {
740 let scval =
741 xdr::ScVal::from_xdr_base64(topic, Limits::none()).map_err(|_| std::fmt::Error)?;
742 writeln!(f, " {scval:?}")?;
743 }
744
745 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())
746 .map_err(|_| std::fmt::Error)?;
747
748 writeln!(f, " Value: {scval:?}")
749 }
750}
751
752pub type SegmentFilter = String;
753pub type TopicFilter = Vec<SegmentFilter>;
754
755impl Event {
756 pub fn parse_cursor(&self) -> Result<(u64, i32), Error> {
759 parse_cursor(&self.id)
760 }
761
762 pub fn pretty_print(&self) -> Result<(), Box<dyn std::error::Error>> {
765 let mut stdout = StandardStream::stdout(ColorChoice::Auto);
766
767 if !stdout.supports_color() {
768 println!("{self}");
769 return Ok(());
770 }
771
772 let color = match self.event_type.as_str() {
773 "system" => Color::Yellow,
774 _ => Color::Blue,
775 };
776 colored!(
777 stdout,
778 "{}Event{} {}{}{} [{}{}{}{}]:\n",
779 bold!(true),
780 bold!(false),
781 fg!(Some(Color::Green)),
782 self.id,
783 reset!(),
784 bold!(true),
785 fg!(Some(color)),
786 self.event_type.to_ascii_uppercase(),
787 reset!(),
788 )?;
789
790 colored!(
791 stdout,
792 " Ledger: {}{}{} (closed at {}{}{})\n",
793 fg!(Some(Color::Green)),
794 self.ledger,
795 reset!(),
796 fg!(Some(Color::Green)),
797 self.ledger_closed_at,
798 reset!(),
799 )?;
800
801 colored!(
802 stdout,
803 " Contract: {}{}{}\n",
804 fg!(Some(Color::Green)),
805 self.contract_id,
806 reset!(),
807 )?;
808
809 colored!(stdout, " Topics:\n")?;
810 for topic in &self.topic {
811 let scval = xdr::ScVal::from_xdr_base64(topic, Limits::none())?;
812 colored!(
813 stdout,
814 " {}{:?}{}\n",
815 fg!(Some(Color::Green)),
816 scval,
817 reset!(),
818 )?;
819 }
820
821 let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())?;
822 colored!(
823 stdout,
824 " Value: {}{:?}{}\n\n",
825 fg!(Some(Color::Green)),
826 scval,
827 reset!(),
828 )?;
829
830 Ok(())
831 }
832}
833
834pub enum AuthMode {
836 Enforce,
837 Record,
838 RecordAllowNonRoot,
839}
840
841#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
842pub enum EventType {
843 All,
844 Contract,
845 System,
846}
847
848#[derive(Clone, Debug, Eq, Hash, PartialEq)]
849pub enum LedgerStart {
850 Ledger(u32),
851 Cursor(String),
852}
853
854#[derive(Clone, Debug, Eq, Hash, PartialEq)]
856pub struct LedgerRange {
857 start: u32,
858 end: u32,
859}
860
861impl LedgerRange {
862 pub fn start(&self) -> u32 {
863 self.start
864 }
865
866 pub fn end(&self) -> u32 {
867 self.end
868 }
869}
870
871#[derive(Clone, Debug, Eq, Hash, PartialEq)]
872pub enum EventStart {
873 Ledger(u32),
874 LedgerRange(LedgerRange),
877 Cursor(String),
878}
879
880impl EventStart {
881 pub fn ledger_range(start: u32, end: u32) -> Result<Self, String> {
885 if start > end {
886 return Err(format!(
887 "invalid ledger range: start ({start}) must be <= end ({end})"
888 ));
889 }
890 Ok(EventStart::LedgerRange(LedgerRange { start, end }))
891 }
892}
893
894#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
895pub struct FullLedgerEntry {
896 pub key: LedgerKey,
897 pub val: LedgerEntryData,
898 #[serde(rename = "lastModifiedLedgerSeq")]
899 pub last_modified_ledger: u32,
900 #[serde(
901 rename = "liveUntilLedgerSeq",
902 skip_serializing_if = "Option::is_none",
903 deserialize_with = "deserialize_option_number_from_string",
904 default
905 )]
906 pub live_until_ledger_seq: Option<u32>,
907}
908
909#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
910pub struct FullLedgerEntries {
911 pub entries: Vec<FullLedgerEntry>,
912 #[serde(rename = "latestLedger")]
913 pub latest_ledger: i64,
914}
915
916#[derive(Debug, Clone)]
917pub struct Client {
918 base_url: Arc<str>,
919 timeout_in_secs: u64,
920 http_client: Arc<HttpClient>,
921}
922
923#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
924pub struct ResourceConfig {
926 #[serde(rename = "instructionLeeway")]
928 pub instruction_leeway: u64,
929}
930
931#[allow(deprecated)] impl Client {
933 pub fn new(base_url: &str) -> Result<Self, Error> {
936 let headers = Self::default_http_headers();
937 let http_client = Arc::new(
938 HttpClientBuilder::default()
939 .set_headers(headers)
940 .build(base_url)?,
941 );
942
943 Ok(Self {
944 base_url: Arc::from(base_url),
945 timeout_in_secs: 30,
946 http_client,
947 })
948 }
949
950 #[deprecated(
953 note = "To be marked private in a future major release. Please use `new_with_headers` instead."
954 )]
955 pub fn new_with_timeout(base_url: &str, timeout: u64) -> Result<Self, Error> {
956 let mut client = Self::new(base_url)?;
957 client.timeout_in_secs = timeout;
958 Ok(client)
959 }
960
961 pub fn new_with_headers(base_url: &str, additional_headers: HeaderMap) -> Result<Self, Error> {
964 let mut client = Self::new(base_url)?;
965 let mut headers = Self::default_http_headers();
966
967 for (key, value) in additional_headers {
968 headers.insert(key.ok_or(Error::InvalidResponse)?, value);
969 }
970
971 let http_client = Arc::new(
972 HttpClientBuilder::default()
973 .set_headers(headers)
974 .build(base_url)?,
975 );
976
977 client.http_client = http_client;
978 Ok(client)
979 }
980
981 fn default_http_headers() -> HeaderMap {
982 let mut headers = HeaderMap::new();
983 headers.insert("X-Client-Name", unsafe {
984 "rs-stellar-rpc-client".parse().unwrap_unchecked()
985 });
986 let version = VERSION.unwrap_or("devel");
987 headers.insert("X-Client-Version", unsafe {
988 version.parse().unwrap_unchecked()
989 });
990
991 headers
992 }
993
994 #[must_use]
995 pub fn base_url(&self) -> &str {
996 &self.base_url
997 }
998
999 #[must_use]
1000 pub fn client(&self) -> &HttpClient {
1001 &self.http_client
1002 }
1003
1004 pub async fn friendbot_url(&self) -> Result<String, Error> {
1007 let network = self.get_network().await?;
1008 network.friendbot_url.ok_or_else(|| {
1009 Error::NotFound(
1010 "Friendbot".to_string(),
1011 "Friendbot is not available on this network".to_string(),
1012 )
1013 })
1014 }
1015 pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result<String, Error> {
1018 let server = self.get_network().await?.passphrase;
1019
1020 if let Some(expected) = expected {
1021 if expected != server {
1022 return Err(Error::InvalidNetworkPassphrase {
1023 expected: expected.to_string(),
1024 server,
1025 });
1026 }
1027 }
1028
1029 Ok(server)
1030 }
1031
1032 pub async fn get_network(&self) -> Result<GetNetworkResponse, Error> {
1035 Ok(self
1036 .client()
1037 .request("getNetwork", ObjectParams::new())
1038 .await?)
1039 }
1040
1041 pub async fn get_health(&self) -> Result<GetHealthResponse, Error> {
1044 Ok(self
1045 .client()
1046 .request("getHealth", ObjectParams::new())
1047 .await?)
1048 }
1049
1050 pub async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, Error> {
1053 Ok(self
1054 .client()
1055 .request("getLatestLedger", ObjectParams::new())
1056 .await?)
1057 }
1058
1059 pub async fn get_ledgers(
1062 &self,
1063 start: LedgerStart,
1064 limit: Option<usize>,
1065 format: Option<String>,
1066 ) -> Result<GetLedgersResponse, Error> {
1067 let mut oparams = ObjectParams::new();
1068
1069 let mut pagination = serde_json::Map::new();
1070 if let Some(limit) = limit {
1071 pagination.insert("limit".to_string(), limit.into());
1072 }
1073
1074 match start {
1075 LedgerStart::Ledger(l) => oparams.insert("startLedger", l)?,
1076 LedgerStart::Cursor(c) => {
1077 pagination.insert("cursor".to_string(), c.into());
1078 }
1079 }
1080
1081 oparams.insert("pagination", pagination)?;
1082
1083 if let Some(f) = format {
1084 oparams.insert("xdrFormat", f)?;
1085 }
1086
1087 Ok(self.client().request("getLedgers", oparams).await?)
1088 }
1089
1090 pub async fn get_account(&self, address: &str) -> Result<AccountEntry, Error> {
1093 let key = LedgerKey::Account(LedgerKeyAccount {
1094 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
1095 stellar_strkey::ed25519::PublicKey::from_string(address)?.0,
1096 ))),
1097 });
1098 let keys = Vec::from([key]);
1099 let response = self.get_ledger_entries(&keys).await?;
1100 let entries = response.entries.unwrap_or_default();
1101
1102 if entries.is_empty() {
1103 return Err(Error::NotFound("Account".to_string(), address.to_owned()));
1104 }
1105
1106 let ledger_entry = &entries[0];
1107 let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none());
1108
1109 if let LedgerEntryData::Account(entry) = LedgerEntryData::read_xdr_base64(&mut read)? {
1110 Ok(entry)
1111 } else {
1112 Err(Error::InvalidResponse)
1113 }
1114 }
1115
1116 pub async fn get_fee_stats(&self) -> Result<GetFeeStatsResponse, Error> {
1119 Ok(self
1120 .client()
1121 .request("getFeeStats", ObjectParams::new())
1122 .await?)
1123 }
1124
1125 pub async fn get_version_info(&self) -> Result<GetVersionInfoResponse, Error> {
1128 Ok(self
1129 .client()
1130 .request("getVersionInfo", ObjectParams::new())
1131 .await?)
1132 }
1133
1134 pub async fn send_transaction(&self, tx: &TransactionEnvelope) -> Result<Hash, Error> {
1137 let mut oparams = ObjectParams::new();
1138 oparams.insert("transaction", tx.to_xdr_base64(Limits::none())?)?;
1139 let SendTransactionResponse {
1140 hash,
1141 error_result_xdr,
1142 status,
1143 ..
1144 } = self
1145 .client()
1146 .request("sendTransaction", oparams)
1147 .await
1148 .map_err(|err| {
1149 Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}"))
1150 })?;
1151
1152 if status == "ERROR" {
1153 let error = error_result_xdr
1154 .ok_or(Error::MissingError)
1155 .and_then(|x| {
1156 TransactionResult::read_xdr_base64(&mut Limited::new(
1157 x.as_bytes(),
1158 Limits::none(),
1159 ))
1160 .map_err(|_| Error::InvalidResponse)
1161 })
1162 .map(|r| r.result)?;
1163
1164 return Err(Error::TransactionSubmissionFailed(format!("{error:#?}")));
1165 }
1166
1167 Ok(Hash::from_str(&hash)?)
1168 }
1169
1170 pub async fn send_transaction_polling(
1173 &self,
1174 tx: &TransactionEnvelope,
1175 ) -> Result<GetTransactionResponse, Error> {
1176 let hash = self.send_transaction(tx).await?;
1177 self.get_transaction_polling(&hash, None).await
1178 }
1179
1180 pub async fn simulate_transaction_envelope(
1183 &self,
1184 tx: &TransactionEnvelope,
1185 auth_mode: Option<AuthMode>,
1186 ) -> Result<SimulateTransactionResponse, Error> {
1187 let base64_tx = tx.to_xdr_base64(Limits::none())?;
1188 let mut params = ObjectParams::new();
1189
1190 params.insert("transaction", base64_tx)?;
1191
1192 match auth_mode {
1193 Some(AuthMode::Enforce) => {
1194 params.insert("authMode", "enforce")?;
1195 }
1196 Some(AuthMode::Record) => {
1197 params.insert("authMode", "record")?;
1198 }
1199 Some(AuthMode::RecordAllowNonRoot) => {
1200 params.insert("authMode", "record_allow_nonroot")?;
1201 }
1202 None => {}
1203 }
1204
1205 let sim_res = self.client().request("simulateTransaction", params).await?;
1206
1207 Ok(sim_res)
1208 }
1209
1210 pub async fn next_simulate_transaction_envelope(
1213 &self,
1214 tx: &TransactionEnvelope,
1215 auth_mode: Option<AuthMode>,
1216 resource_config: Option<ResourceConfig>,
1217 ) -> Result<SimulateTransactionResponse, Error> {
1218 let base64_tx = tx.to_xdr_base64(Limits::none())?;
1219 let mut params = ObjectParams::new();
1220
1221 params.insert("transaction", base64_tx)?;
1222
1223 match auth_mode {
1224 Some(AuthMode::Enforce) => {
1225 params.insert("authMode", "enforce")?;
1226 }
1227 Some(AuthMode::Record) => {
1228 params.insert("authMode", "record")?;
1229 }
1230 Some(AuthMode::RecordAllowNonRoot) => {
1231 params.insert("authMode", "record_allow_nonroot")?;
1232 }
1233 None => {}
1234 }
1235
1236 if let Some(ref config) = resource_config {
1237 let mut resource_config_params = ObjectParams::new();
1238 resource_config_params.insert("instructionLeeway", config.instruction_leeway)?;
1239 params.insert("resourceConfig", resource_config)?;
1240 }
1241
1242 let sim_res = self.client().request("simulateTransaction", params).await?;
1243
1244 Ok(sim_res)
1245 }
1246
1247 pub async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, Error> {
1250 let mut oparams = ObjectParams::new();
1251 oparams.insert("hash", tx_id)?;
1252 let resp: GetTransactionResponseRaw =
1253 self.client().request("getTransaction", oparams).await?;
1254
1255 Ok(resp.try_into()?)
1256 }
1257
1258 pub async fn get_transactions(
1261 &self,
1262 request: GetTransactionsRequest,
1263 ) -> Result<GetTransactionsResponse, Error> {
1264 let mut oparams = ObjectParams::new();
1265
1266 if let Some(start_ledger) = request.start_ledger {
1267 oparams.insert("startLedger", start_ledger)?;
1268 }
1269
1270 if let Some(pagination_params) = request.pagination {
1271 let pagination = serde_json::json!(pagination_params);
1272 oparams.insert("pagination", pagination)?;
1273 }
1274
1275 let resp: GetTransactionsResponseRaw =
1276 self.client().request("getTransactions", oparams).await?;
1277
1278 Ok(resp.try_into()?)
1279 }
1280
1281 pub async fn get_transaction_polling(
1291 &self,
1292 tx_id: &Hash,
1293 timeout_s: Option<Duration>,
1294 ) -> Result<GetTransactionResponse, Error> {
1295 let start = Instant::now();
1297 let timeout = timeout_s.unwrap_or(Duration::from_secs(self.timeout_in_secs));
1298 let exponential_backoff: f64 = 1.0 / (1.0 - E.powf(-1.0));
1301 let mut sleep_time = Duration::from_secs(1);
1302 loop {
1303 let response = self.get_transaction(tx_id).await?;
1304 match response.status.as_str() {
1305 "SUCCESS" => return Ok(response),
1306
1307 "FAILED" => {
1308 return Err(Error::TransactionSubmissionFailed(format!(
1309 "{:#?}",
1310 response.result
1311 )))
1312 }
1313
1314 "NOT_FOUND" => (),
1315 _ => {
1316 return Err(Error::UnexpectedTransactionStatus(response.status));
1317 }
1318 }
1319
1320 if start.elapsed() > timeout {
1321 return Err(Error::TransactionSubmissionTimeout);
1322 }
1323
1324 sleep(sleep_time).await;
1325 sleep_time = Duration::from_secs_f64(sleep_time.as_secs_f64() * exponential_backoff);
1326 }
1327 }
1328
1329 pub async fn get_ledger_entries(
1332 &self,
1333 keys: &[LedgerKey],
1334 ) -> Result<GetLedgerEntriesResponse, Error> {
1335 let mut base64_keys: Vec<String> = vec![];
1336
1337 for k in keys {
1338 let base64_result = k.to_xdr_base64(Limits::none());
1339 if base64_result.is_err() {
1340 return Err(Error::Xdr(XdrError::Invalid));
1341 }
1342 base64_keys.push(k.to_xdr_base64(Limits::none())?);
1343 }
1344
1345 let mut oparams = ObjectParams::new();
1346 oparams.insert("keys", base64_keys)?;
1347
1348 Ok(self.client().request("getLedgerEntries", oparams).await?)
1349 }
1350
1351 pub async fn get_full_ledger_entries(
1354 &self,
1355 ledger_keys: &[LedgerKey],
1356 ) -> Result<FullLedgerEntries, Error> {
1357 let keys = ledger_keys
1358 .iter()
1359 .filter(|key| !matches!(key, LedgerKey::Ttl(_)))
1360 .map(Clone::clone)
1361 .collect::<Vec<_>>();
1362 let GetLedgerEntriesResponse {
1363 entries,
1364 latest_ledger,
1365 } = self.get_ledger_entries(&keys).await?;
1366 let entries = entries
1367 .unwrap_or_default()
1368 .iter()
1369 .map(
1370 |LedgerEntryResult {
1371 key,
1372 xdr,
1373 last_modified_ledger,
1374 live_until_ledger_seq_ledger_seq,
1375 }| {
1376 Ok(FullLedgerEntry {
1377 key: LedgerKey::from_xdr_base64(key, Limits::none())?,
1378 val: LedgerEntryData::from_xdr_base64(xdr, Limits::none())?,
1379 live_until_ledger_seq: *live_until_ledger_seq_ledger_seq,
1380 last_modified_ledger: *last_modified_ledger,
1381 })
1382 },
1383 )
1384 .collect::<Result<Vec<_>, Error>>()?;
1385 Ok(FullLedgerEntries {
1386 entries,
1387 latest_ledger,
1388 })
1389 }
1390
1391 pub async fn get_events(
1394 &self,
1395 start: EventStart,
1396 event_type: Option<EventType>,
1397 contract_ids: &[String],
1398 topics: &[TopicFilter],
1399 limit: Option<usize>,
1400 ) -> Result<GetEventsResponse, Error> {
1401 let mut filters = serde_json::Map::new();
1402
1403 event_type
1404 .and_then(|t| match t {
1405 EventType::All => None, EventType::Contract => Some("contract"),
1407 EventType::System => Some("system"),
1408 })
1409 .map(|t| filters.insert("type".to_string(), t.into()));
1410
1411 filters.insert("topics".to_string(), topics.into());
1412 filters.insert("contractIds".to_string(), contract_ids.into());
1413
1414 let mut pagination = serde_json::Map::new();
1415 if let Some(limit) = limit {
1416 pagination.insert("limit".to_string(), limit.into());
1417 }
1418
1419 let mut oparams = ObjectParams::new();
1420 match start {
1421 EventStart::Ledger(l) => oparams.insert("startLedger", l)?,
1422 EventStart::LedgerRange(r) => {
1423 oparams.insert("startLedger", r.start())?;
1424 oparams.insert("endLedger", r.end())?;
1425 }
1426 EventStart::Cursor(c) => {
1427 pagination.insert("cursor".to_string(), c.into());
1428 }
1429 }
1430 oparams.insert("filters", vec![filters])?;
1431 oparams.insert("pagination", pagination)?;
1432
1433 Ok(self.client().request("getEvents", oparams).await?)
1434 }
1435
1436 pub async fn get_contract_data(
1439 &self,
1440 contract_id: &[u8; 32],
1441 ) -> Result<ContractDataEntry, Error> {
1442 let contract_key = LedgerKey::ContractData(xdr::LedgerKeyContractData {
1444 contract: xdr::ScAddress::Contract(ContractId(xdr::Hash(*contract_id))),
1445 key: xdr::ScVal::LedgerKeyContractInstance,
1446 durability: xdr::ContractDataDurability::Persistent,
1447 });
1448 let contract_ref = self.get_ledger_entries(&[contract_key]).await?;
1449 let entries = contract_ref.entries.unwrap_or_default();
1450 if entries.is_empty() {
1451 let contract_address = stellar_strkey::Contract(*contract_id).to_string();
1452 return Err(Error::NotFound(
1453 "Contract".to_string(),
1454 contract_address.to_string(),
1455 ));
1456 }
1457 let contract_ref_entry = &entries[0];
1458 match LedgerEntryData::from_xdr_base64(&contract_ref_entry.xdr, Limits::none())? {
1459 LedgerEntryData::ContractData(contract_data) => Ok(contract_data),
1460 scval => Err(Error::UnexpectedContractCodeDataType(Box::new(scval))),
1461 }
1462 }
1463
1464 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1467 pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result<Vec<u8>, Error> {
1468 match self.get_contract_data(contract_id).await? {
1469 xdr::ContractDataEntry {
1470 val:
1471 xdr::ScVal::ContractInstance(xdr::ScContractInstance {
1472 executable: xdr::ContractExecutable::Wasm(hash),
1473 ..
1474 }),
1475 ..
1476 } => self.get_remote_wasm_from_hash(hash).await,
1477 scval => Err(Error::UnexpectedToken(Box::new(scval))),
1478 }
1479 }
1480
1481 #[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
1484 pub async fn get_remote_wasm_from_hash(&self, hash: Hash) -> Result<Vec<u8>, Error> {
1485 let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
1486 let contract_data = self.get_ledger_entries(&[code_key]).await?;
1487 let entries = contract_data.entries.unwrap_or_default();
1488 if entries.is_empty() {
1489 return Err(Error::NotFound(
1490 "Contract Code".to_string(),
1491 hex::encode(hash),
1492 ));
1493 }
1494 let contract_data_entry = &entries[0];
1495 match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
1496 LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
1497 scval => Err(Error::UnexpectedContractCodeDataType(Box::new(scval))),
1498 }
1499 }
1500
1501 pub async fn get_contract_instance(
1506 &self,
1507 contract_id: &[u8; 32],
1508 ) -> Result<ScContractInstance, Error> {
1509 let contract_data = self.get_contract_data(contract_id).await?;
1510 match contract_data.val {
1511 xdr::ScVal::ContractInstance(instance) => Ok(instance),
1512 scval => Err(Error::UnexpectedContractInstance(Box::new(scval))),
1513 }
1514 }
1515}
1516
1517pub(crate) fn parse_cursor(c: &str) -> Result<(u64, i32), Error> {
1518 let (toid_part, event_index) = c.split('-').collect_tuple().ok_or(Error::InvalidCursor)?;
1519 let toid_part: u64 = toid_part.parse().map_err(|_| Error::InvalidCursor)?;
1520 let start_index: i32 = event_index.parse().map_err(|_| Error::InvalidCursor)?;
1521 Ok((toid_part, start_index))
1522}
1523
1524fn deserialize_option_i64_from_string_or_number<'de, D>(
1525 deserializer: D,
1526) -> Result<Option<i64>, D::Error>
1527where
1528 D: serde::Deserializer<'de>,
1529{
1530 use serde::Deserialize;
1531
1532 #[derive(Deserialize)]
1533 #[serde(untagged)]
1534 enum StringOrNumber {
1535 String(String),
1536 Number(i64),
1537 }
1538
1539 match Option::<StringOrNumber>::deserialize(deserializer)? {
1540 None => Ok(None),
1541 Some(StringOrNumber::String(s)) => {
1542 s.parse::<i64>().map(Some).map_err(serde::de::Error::custom)
1543 }
1544 Some(StringOrNumber::Number(n)) => Ok(Some(n)),
1545 }
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550 use super::*;
1551 use std::env;
1552 use std::fs;
1553 use std::path::PathBuf;
1554
1555 fn does_topic_match(topic: &[String], filter: &[String]) -> bool {
1567 if filter.is_empty() {
1568 return false;
1569 }
1570
1571 if let Some((last, prefix)) = filter.split_last() {
1573 if last == "**" {
1574 return topic.len() >= prefix.len()
1575 && prefix
1576 .iter()
1577 .enumerate()
1578 .all(|(i, s)| *s == "*" || topic[i] == *s);
1579 }
1580 }
1581
1582 filter.len() == topic.len()
1583 && filter
1584 .iter()
1585 .enumerate()
1586 .all(|(i, s)| *s == "*" || topic[i] == *s)
1587 }
1588
1589 fn get_repo_root() -> PathBuf {
1590 let mut path = env::current_exe().expect("Failed to get current executable path");
1591 while path.pop() {
1593 if path.join("Cargo.toml").exists() {
1594 return path;
1595 }
1596 }
1597 panic!("Could not find repository root");
1598 }
1599
1600 fn read_json_file(name: &str) -> String {
1601 let repo_root = get_repo_root();
1602 let fixture_path = repo_root.join("src").join("fixtures").join(name);
1603 fs::read_to_string(fixture_path).unwrap_or_else(|_| panic!("Failed to read {name:?}"))
1604 }
1605
1606 #[test]
1607 fn simulation_transaction_response_parsing() {
1608 let s = r#"{
1609 "minResourceFee": "100000000",
1610 "cost": { "cpuInsns": "1000", "memBytes": "1000" },
1611 "transactionData": "",
1612 "latestLedger": 1234,
1613 "stateChanges": [{
1614 "type": "created",
1615 "key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==",
1616 "before": null,
1617 "after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
1618 }]
1619 }"#;
1620
1621 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1622 assert_eq!(
1623 resp.state_changes.unwrap()[0],
1624 LedgerEntryChange::Created { key: "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==".to_string(), after: "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string() },
1625 );
1626 assert_eq!(resp.min_resource_fee, 100_000_000);
1627 }
1628
1629 #[test]
1630 fn simulation_transaction_response_parsing_mostly_empty() {
1631 let s = r#"{
1632 "latestLedger": 1234
1633 }"#;
1634
1635 let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
1636 assert_eq!(resp.latest_ledger, 1_234);
1637 }
1638
1639 #[test]
1640 fn test_parse_transaction_response_p23() {
1641 let response_content = read_json_file("transaction_response_p23.json");
1642 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1643 .expect("Failed to parse JSON from transaction_response_p23.json");
1644 let result = full_response["result"].clone();
1645 let raw_response: GetTransactionResponseRaw = serde_json::from_value(result)
1646 .expect("Failed to parse 'result' into GetTransactionResponseRaw");
1647 let response: GetTransactionResponse = raw_response
1648 .try_into()
1649 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1650
1651 assert_eq!(2, response.events.transaction_events.iter().len());
1652 assert_eq!(1, response.events.contract_events.len());
1653 assert_eq!(21, response.events.diagnostic_events.iter().len());
1654 assert_eq!(
1655 response.tx_hash.as_deref(),
1656 Some("bfe15f83ea850b7bf86fd7152f9074033f2aec2a045e40a8872ac56726a6e35c")
1657 );
1658 assert_eq!(response.created_at, Some(1_751_666_924));
1659 assert_eq!(response.application_order, Some(1));
1660 assert_eq!(response.fee_bump, Some(false));
1661 }
1662
1663 #[test]
1664 fn test_parse_transaction_response_p22() {
1665 let response_content = read_json_file("transaction_response_p22.json");
1666 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1667 .expect("Failed to parse JSON from transaction_response_p22.json");
1668 let result = full_response["result"].clone();
1669 let raw_response: GetTransactionResponseRaw = serde_json::from_value(result)
1670 .expect("Failed to parse 'result' into GetTransactionResponseRaw");
1671 let response: GetTransactionResponse = raw_response
1672 .try_into()
1673 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1674
1675 assert_eq!(23, response.events.diagnostic_events.iter().len());
1676 assert_eq!(
1677 response.tx_hash.as_deref(),
1678 Some("a738ccc7f8f457d4367b78c098569ebee23258c71f128d7a2c61585652345937")
1679 );
1680 assert_eq!(response.created_at, Some(1_751_747_980));
1681 assert_eq!(response.application_order, Some(1));
1682 assert_eq!(response.fee_bump, Some(false));
1683 }
1684
1685 #[test]
1686 fn test_parse_get_transactions_response() {
1687 let response_content = read_json_file("transactions_response.json");
1688
1689 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1691 .expect("Failed to parse JSON from transactions_response.json");
1692
1693 let result = full_response["result"].clone();
1695 let raw_response: GetTransactionsResponseRaw = serde_json::from_value(result)
1697 .expect("Failed to parse 'result' into GetTransactionsResponseRaw");
1698
1699 let response: GetTransactionsResponse = raw_response
1701 .try_into()
1702 .expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
1703
1704 assert_eq!(response.transactions.len(), 5);
1706 assert_eq!(response.latest_ledger, 556_962);
1707 assert_eq!(response.cursor, 2_379_420_471_922_689);
1708
1709 assert_eq!(response.transactions[0].status, "SUCCESS");
1711 }
1714
1715 #[test]
1716 fn test_rpc_url_default_ports() {
1717 let client = Client::new("http://example.com").unwrap();
1720 assert_eq!(client.base_url(), "http://example.com");
1721 let client = Client::new("https://example.com").unwrap();
1722 assert_eq!(client.base_url(), "https://example.com");
1723
1724 let client = Client::new("http://example.com:8080").unwrap();
1726 assert_eq!(client.base_url(), "http://example.com:8080");
1727 let client = Client::new("https://example.com:8080").unwrap();
1728 assert_eq!(client.base_url(), "https://example.com:8080");
1729
1730 let client = Client::new("http://example.com/a/b/c").unwrap();
1732 assert_eq!(client.base_url(), "http://example.com/a/b/c");
1733 let client = Client::new("https://example.com/a/b/c/").unwrap();
1734 assert_eq!(client.base_url(), "https://example.com/a/b/c/");
1735 }
1736
1737 #[test]
1738 fn test_parse_events_response() {
1739 let response_content = read_json_file("events_response_p23.json");
1740 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1741 .expect("Failed to parse JSON from events_response_p23.json");
1742 let result = full_response["result"].clone();
1743
1744 let resp: GetEventsResponse = serde_json::from_value(result.clone())
1746 .expect("Failed to parse 'result' into GetEventsResponse");
1747
1748 assert_eq!(resp.events[0].operation_index, Some(0));
1750 assert_eq!(resp.events[0].transaction_index, Some(0));
1751 assert_eq!(
1752 resp.events[0].tx_hash.as_deref(),
1753 Some("e42da3c70c90cc319e2cfaa2f69a7bd04aefcc4159b12caa0df216fbb3ab43b4")
1754 );
1755 #[allow(deprecated)]
1756 {
1757 assert_eq!(resp.events[0].is_successful_contract_call, Some(true));
1758 }
1759
1760 let reserialized = serde_json::to_value(&resp).expect("Failed to serialize response");
1762
1763 assert_eq!(
1765 result, reserialized,
1766 "Deserialization should preserve all data"
1767 );
1768 }
1769
1770 #[test]
1771 fn test_parse_events_response_p22() {
1772 let response_content = read_json_file("events_response_p22.json");
1775 let full_response: serde_json::Value = serde_json::from_str(&response_content)
1776 .expect("Failed to parse JSON from events_response_p22.json");
1777 let first_event = full_response["result"]["events"][0].clone();
1778
1779 let event: Event = serde_json::from_value(first_event)
1781 .expect("Failed to parse protocol 22 event into Event");
1782
1783 assert!(event.operation_index.is_none());
1784 assert!(event.transaction_index.is_none());
1785 }
1786
1787 #[test]
1788 fn test_ledger_range_valid() {
1789 let r = EventStart::ledger_range(10, 20).unwrap();
1790 assert_eq!(r, EventStart::ledger_range(10, 20).unwrap());
1791
1792 assert!(EventStart::ledger_range(10, 10).is_ok());
1794 }
1795
1796 #[test]
1797 fn test_ledger_range_invalid() {
1798 let err = EventStart::ledger_range(100, 50).unwrap_err();
1799 assert!(err.contains("start (100)") && err.contains("end (50)"));
1800 }
1801
1802 #[test]
1803 fn test_does_topic_match() {
1806 struct TestCase<'a> {
1807 name: &'a str,
1808 filter: Vec<&'a str>,
1809 includes: Vec<Vec<&'a str>>,
1810 excludes: Vec<Vec<&'a str>>,
1811 }
1812
1813 let xfer = "AAAABQAAAAh0cmFuc2Zlcg==";
1814 let number = "AAAAAQB6Mcc=";
1815 let star = "*";
1816
1817 for tc in vec![
1818 TestCase {
1820 name: "<empty>",
1821 filter: vec![],
1822 includes: vec![],
1823 excludes: vec![vec![xfer]],
1824 },
1825 TestCase {
1829 name: "*",
1830 filter: vec![star],
1831 includes: vec![vec![xfer]],
1832 excludes: vec![vec![xfer, xfer], vec![xfer, number]],
1833 },
1834 TestCase {
1837 name: "*/transfer",
1838 filter: vec![star, xfer],
1839 includes: vec![vec![number, xfer], vec![xfer, xfer]],
1840 excludes: vec![
1841 vec![number],
1842 vec![number, number],
1843 vec![number, xfer, number],
1844 vec![xfer],
1845 vec![xfer, number],
1846 vec![xfer, xfer, xfer],
1847 ],
1848 },
1849 TestCase {
1853 name: "transfer/*",
1854 filter: vec![xfer, star],
1855 includes: vec![vec![xfer, number], vec![xfer, xfer]],
1856 excludes: vec![
1857 vec![number],
1858 vec![number, number],
1859 vec![number, xfer, number],
1860 vec![xfer],
1861 vec![number, xfer],
1862 vec![xfer, xfer, xfer],
1863 ],
1864 },
1865 TestCase {
1867 name: "transfer/*/*",
1868 filter: vec![xfer, star, star],
1869 includes: vec![vec![xfer, number, number], vec![xfer, xfer, xfer]],
1870 excludes: vec![
1871 vec![number],
1872 vec![number, number],
1873 vec![number, xfer],
1874 vec![number, xfer, number, number],
1875 vec![xfer],
1876 vec![xfer, xfer, xfer, xfer],
1877 ],
1878 },
1879 TestCase {
1883 name: "transfer/*/number",
1884 filter: vec![xfer, star, number],
1885 includes: vec![vec![xfer, number, number], vec![xfer, xfer, number]],
1886 excludes: vec![
1887 vec![number],
1888 vec![number, number],
1889 vec![number, number, number],
1890 vec![number, xfer, number],
1891 vec![xfer],
1892 vec![number, xfer],
1893 vec![xfer, xfer, xfer],
1894 vec![xfer, number, xfer],
1895 ],
1896 },
1897 TestCase {
1899 name: "**",
1900 filter: vec!["**"],
1901 includes: vec![
1902 vec![],
1903 vec![xfer],
1904 vec![xfer, number],
1905 vec![xfer, number, number],
1906 ],
1907 excludes: vec![],
1908 },
1909 TestCase {
1911 name: "transfer/**",
1912 filter: vec![xfer, "**"],
1913 includes: vec![
1914 vec![xfer],
1915 vec![xfer, number],
1916 vec![xfer, number, number],
1917 vec![xfer, xfer, xfer],
1918 ],
1919 excludes: vec![
1920 vec![],
1921 vec![number],
1922 vec![number, xfer],
1923 vec![number, number],
1924 ],
1925 },
1926 TestCase {
1929 name: "transfer/number/**",
1930 filter: vec![xfer, number, "**"],
1931 includes: vec![
1932 vec![xfer, number],
1933 vec![xfer, number, number],
1934 vec![xfer, number, xfer, number],
1935 ],
1936 excludes: vec![
1937 vec![],
1938 vec![xfer],
1939 vec![number],
1940 vec![number, xfer],
1941 vec![xfer, xfer],
1942 ],
1943 },
1944 ] {
1945 for topic in tc.includes {
1946 assert!(
1947 does_topic_match(
1948 &topic
1949 .iter()
1950 .map(std::string::ToString::to_string)
1951 .collect::<Vec<String>>(),
1952 &tc.filter
1953 .iter()
1954 .map(std::string::ToString::to_string)
1955 .collect::<Vec<String>>()
1956 ),
1957 "test: {}, topic ({:?}) should be matched by filter ({:?})",
1958 tc.name,
1959 topic,
1960 tc.filter
1961 );
1962 }
1963
1964 for topic in tc.excludes {
1965 assert!(
1966 !does_topic_match(
1967 &topic
1969 .iter()
1970 .map(std::string::ToString::to_string)
1971 .collect::<Vec<String>>(),
1972 &tc.filter
1973 .iter()
1974 .map(std::string::ToString::to_string)
1975 .collect::<Vec<String>>()
1976 ),
1977 "test: {}, topic ({:?}) should NOT be matched by filter ({:?})",
1978 tc.name,
1979 topic,
1980 tc.filter
1981 );
1982 }
1983 }
1984 }
1985}