stellar_rpc_client/
lib.rs

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