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