use itertools::Itertools;
use serde_aux::prelude::{
deserialize_default_from_null, deserialize_number_from_string,
deserialize_option_number_from_string,
};
use serde_with::{serde_as, DisplayFromStr};
use stellar_xdr::curr::{
self as xdr, AccountEntry, AccountId, ContractDataEntry, ContractEvent, ContractId,
DiagnosticEvent, Error as XdrError, Hash, LedgerCloseMeta, LedgerEntryData, LedgerFootprint,
LedgerHeaderHistoryEntry, LedgerKey, LedgerKeyAccount, Limited, Limits, PublicKey, ReadXdr,
ScContractInstance, SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData,
TransactionEnvelope, TransactionEvent, TransactionMetaV3, TransactionResult, Uint256, VecM,
WriteXdr,
};
use wasi_jsonrpsee_core::params::ObjectParams;
use wasi_jsonrpsee_core::{self, client::ClientT};
use wasi_jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder};
use std::{
f64::consts::E,
fmt::Display,
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
use termcolor::{Color, ColorChoice, StandardStream, WriteColor};
use termcolor_output::colored;
use tokio::time::sleep;
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
pub type LogEvents = fn(
footprint: &LedgerFootprint,
auth: &[VecM<SorobanAuthorizationEntry>],
events: &[DiagnosticEvent],
) -> ();
pub type LogResources = fn(resources: &SorobanResources) -> ();
#[derive(thiserror::Error, Debug)]
#[allow(deprecated)] pub enum Error {
#[error(transparent)]
InvalidAddress(#[from] stellar_strkey::DecodeError),
#[error("invalid response from server")]
InvalidResponse,
#[error("provided network passphrase {expected:?} does not match the server: {server:?}")]
InvalidNetworkPassphrase { expected: String, server: String },
#[error("xdr processing error: {0}")]
Xdr(#[from] XdrError),
#[error("invalid friendbot url: {0}")]
InvalidUrl(String),
#[error(transparent)]
JsonRpc(#[from] wasi_jsonrpsee_core::ClientError),
#[error("json decoding error: {0}")]
Serde(#[from] serde_json::Error),
#[error("transaction failed: {0}")]
TransactionFailed(String),
#[error("transaction submission failed: {0}")]
TransactionSubmissionFailed(String),
#[error("expected transaction status: {0}")]
UnexpectedTransactionStatus(String),
#[error("transaction submission timeout")]
TransactionSubmissionTimeout,
#[error("transaction simulation failed: {0}")]
TransactionSimulationFailed(String),
#[error("{0} not found: {1}")]
NotFound(String, String),
#[error("Missing result in successful response")]
MissingResult,
#[error("Failed to read Error response from server")]
MissingError,
#[error("Missing signing key for account {address}")]
MissingSignerForAddress { address: String },
#[error("cursor is not valid")]
InvalidCursor,
#[error("unexpected ({length}) simulate transaction result length")]
UnexpectedSimulateTransactionResultSize { length: usize },
#[error("unexpected ({count}) number of operations")]
UnexpectedOperationCount { count: usize },
#[error("Transaction contains unsupported operation type")]
UnsupportedOperationType,
#[error("unexpected contract code data type: {0:?}")]
UnexpectedContractCodeDataType(Box<LedgerEntryData>),
#[error("unexpected contract instance type: {0:?}")]
UnexpectedContractInstance(Box<xdr::ScVal>),
#[error("unexpected contract code got token {0:?}")]
#[deprecated(note = "To be removed in future versions")]
UnexpectedToken(Box<ContractDataEntry>),
#[error("Fee was too large {0}")]
LargeFee(u64),
#[error("Cannot authorize raw transactions")]
CannotAuthorizeRawTransaction,
#[error("Missing result for tnx")]
MissingOp,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct SendTransactionResponse {
pub hash: String,
pub status: String,
#[serde(
rename = "errorResultXdr",
skip_serializing_if = "Option::is_none",
default
)]
pub error_result_xdr: Option<String>,
#[serde(rename = "latestLedger")]
pub latest_ledger: u32,
#[serde(
rename = "latestLedgerCloseTime",
deserialize_with = "deserialize_number_from_string"
)]
pub latest_ledger_close_time: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetTransactionResponseRaw {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ledger: Option<u32>,
#[serde(
rename = "applicationOrder",
skip_serializing_if = "Option::is_none",
default
)]
pub application_order: Option<u32>,
#[serde(rename = "feeBump", skip_serializing_if = "Option::is_none", default)]
pub fee_bump: Option<bool>,
#[serde(
rename = "envelopeXdr",
skip_serializing_if = "Option::is_none",
default
)]
pub envelope_xdr: Option<String>,
#[serde(rename = "resultXdr", skip_serializing_if = "Option::is_none", default)]
pub result_xdr: Option<String>,
#[serde(
rename = "resultMetaXdr",
skip_serializing_if = "Option::is_none",
default
)]
pub result_meta_xdr: Option<String>,
#[serde(rename = "txHash", skip_serializing_if = "Option::is_none", default)]
pub tx_hash: Option<String>,
#[serde(
rename = "createdAt",
deserialize_with = "deserialize_option_i64_from_string_or_number",
skip_serializing_if = "Option::is_none",
default
)]
pub created_at: Option<i64>,
#[serde(rename = "events", skip_serializing_if = "Option::is_none", default)]
pub events: Option<GetTransactionEventsRaw>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Default)]
pub struct GetTransactionEventsRaw {
#[serde(
rename = "contractEventsXdr",
skip_serializing_if = "Option::is_none",
default
)]
pub contract_events_xdr: Option<Vec<Vec<String>>>,
#[serde(
rename = "diagnosticEventsXdr",
skip_serializing_if = "Option::is_none",
default
)]
pub diagnostic_events_xdr: Option<Vec<String>>,
#[serde(
rename = "transactionEventsXdr",
skip_serializing_if = "Option::is_none",
default
)]
pub transaction_events_xdr: Option<Vec<String>>,
}
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
pub struct GetTransactionEvents {
pub contract_events: Vec<Vec<ContractEvent>>,
pub diagnostic_events: Vec<DiagnosticEvent>,
pub transaction_events: Vec<TransactionEvent>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetTransactionResponse {
pub status: String,
pub ledger: Option<u32>,
pub application_order: Option<u32>,
pub fee_bump: Option<bool>,
pub tx_hash: Option<String>,
pub created_at: Option<i64>,
pub envelope: Option<xdr::TransactionEnvelope>,
pub result: Option<xdr::TransactionResult>,
pub result_meta: Option<xdr::TransactionMeta>,
pub events: GetTransactionEvents,
}
impl TryInto<GetTransactionResponse> for GetTransactionResponseRaw {
type Error = xdr::Error;
fn try_into(self) -> Result<GetTransactionResponse, Self::Error> {
let events = self.events.unwrap_or_default();
let result_meta: Option<xdr::TransactionMeta> = self
.result_meta_xdr
.map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
.transpose()?;
let events = match result_meta {
Some(xdr::TransactionMeta::V4(_)) => GetTransactionEvents {
contract_events: events
.contract_events_xdr
.unwrap_or_default()
.into_iter()
.map(|es| {
es.into_iter()
.filter_map(|e| ContractEvent::from_xdr_base64(e, Limits::none()).ok())
.collect::<Vec<_>>()
})
.collect::<Vec<Vec<ContractEvent>>>(),
diagnostic_events: events
.diagnostic_events_xdr
.unwrap_or_default()
.iter()
.filter_map(|e| DiagnosticEvent::from_xdr_base64(e, Limits::none()).ok())
.collect(),
transaction_events: events
.transaction_events_xdr
.unwrap_or_default()
.iter()
.filter_map(|e| TransactionEvent::from_xdr_base64(e, Limits::none()).ok())
.collect(),
},
Some(xdr::TransactionMeta::V3(TransactionMetaV3 {
soroban_meta: Some(ref meta),
..
})) => GetTransactionEvents {
contract_events: vec![],
transaction_events: vec![],
diagnostic_events: meta.diagnostic_events.clone().into(),
},
_ => GetTransactionEvents {
contract_events: vec![],
transaction_events: vec![],
diagnostic_events: vec![],
},
};
Ok(GetTransactionResponse {
status: self.status,
ledger: self.ledger,
application_order: self.application_order,
fee_bump: self.fee_bump,
tx_hash: self.tx_hash,
created_at: self.created_at,
envelope: self
.envelope_xdr
.map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
.transpose()?,
result: self
.result_xdr
.map(|v| ReadXdr::from_xdr_base64(v, Limits::none()))
.transpose()?,
result_meta,
events,
})
}
}
impl GetTransactionResponse {
pub fn return_value(&self) -> Result<xdr::ScVal, Error> {
if let Some(xdr::TransactionMeta::V3(xdr::TransactionMetaV3 {
soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }),
..
})) = &self.result_meta
{
return Ok(return_value.clone());
}
if let Some(xdr::TransactionMeta::V4(xdr::TransactionMetaV4 {
soroban_meta:
Some(xdr::SorobanTransactionMetaV2 {
return_value: Some(return_value),
..
}),
..
})) = &self.result_meta
{
return Ok(return_value.clone());
}
Err(Error::MissingOp)
}
}
#[serde_as]
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetTransactionsResponseRaw {
pub transactions: Vec<GetTransactionResponseRaw>,
#[serde(rename = "latestLedger")]
pub latest_ledger: u32,
#[serde(rename = "latestLedgerCloseTimestamp")]
pub latest_ledger_close_time: i64,
#[serde(rename = "oldestLedger")]
pub oldest_ledger: u32,
#[serde(rename = "oldestLedgerCloseTimestamp")]
pub oldest_ledger_close_time: i64,
#[serde_as(as = "DisplayFromStr")]
pub cursor: u64,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetTransactionsResponse {
pub transactions: Vec<GetTransactionResponse>,
pub latest_ledger: u32,
pub latest_ledger_close_time: i64,
pub oldest_ledger: u32,
pub oldest_ledger_close_time: i64,
pub cursor: u64,
}
impl TryInto<GetTransactionsResponse> for GetTransactionsResponseRaw {
type Error = xdr::Error;
fn try_into(self) -> Result<GetTransactionsResponse, Self::Error> {
Ok(GetTransactionsResponse {
transactions: self
.transactions
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, xdr::Error>>()?,
latest_ledger: self.latest_ledger,
latest_ledger_close_time: self.latest_ledger_close_time,
oldest_ledger: self.oldest_ledger,
oldest_ledger_close_time: self.oldest_ledger_close_time,
cursor: self.cursor,
})
}
}
#[serde_as]
#[derive(serde::Serialize, Debug, Clone)]
pub struct TransactionsPaginationOptions {
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(serde::Serialize, Debug, Clone)]
pub struct GetTransactionsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub start_ledger: Option<u32>,
pub pagination: Option<TransactionsPaginationOptions>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct LedgerEntryResult {
pub key: String,
pub xdr: String,
#[serde(rename = "lastModifiedLedgerSeq")]
pub last_modified_ledger: u32,
#[serde(
rename = "liveUntilLedgerSeq",
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_option_number_from_string",
default
)]
pub live_until_ledger_seq_ledger_seq: Option<u32>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetLedgerEntriesResponse {
pub entries: Option<Vec<LedgerEntryResult>>,
#[serde(rename = "latestLedger")]
pub latest_ledger: i64,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetNetworkResponse {
#[serde(
rename = "friendbotUrl",
skip_serializing_if = "Option::is_none",
default
)]
pub friendbot_url: Option<String>,
pub passphrase: String,
#[serde(rename = "protocolVersion")]
pub protocol_version: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetHealthResponse {
pub status: String,
#[serde(rename = "latestLedger")]
pub latest_ledger: u32,
#[serde(rename = "oldestLedger")]
pub oldest_ledger: u32,
#[serde(rename = "ledgerRetentionWindow")]
pub ledger_retention_window: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetVersionInfoResponse {
pub version: String,
#[serde(rename = "commitHash")]
pub commmit_hash: String,
#[serde(rename = "buildTimestamp")]
pub build_timestamp: String,
#[serde(rename = "captiveCoreVersion")]
pub captive_core_version: String,
#[serde(rename = "protocolVersion")]
pub protocol_version: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetLatestLedgerResponse {
pub id: String,
#[serde(rename = "protocolVersion")]
pub protocol_version: u32,
pub sequence: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetFeeStatsResponse {
#[serde(rename = "sorobanInclusionFee")]
pub soroban_inclusion_fee: FeeStat,
#[serde(rename = "inclusionFee")]
pub inclusion_fee: FeeStat,
#[serde(
rename = "latestLedger",
deserialize_with = "deserialize_number_from_string"
)]
pub latest_ledger: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct FeeStat {
pub max: String,
pub min: String,
pub mode: String,
pub p10: String,
pub p20: String,
pub p30: String,
pub p40: String,
pub p50: String,
pub p60: String,
pub p70: String,
pub p80: String,
pub p90: String,
pub p95: String,
pub p99: String,
#[serde(
rename = "transactionCount",
deserialize_with = "deserialize_number_from_string"
)]
pub transaction_count: u32,
#[serde(
rename = "ledgerCount",
deserialize_with = "deserialize_number_from_string"
)]
pub ledger_count: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
pub struct Cost {
#[serde(
rename = "cpuInsns",
deserialize_with = "deserialize_number_from_string"
)]
pub cpu_insns: u64,
#[serde(
rename = "memBytes",
deserialize_with = "deserialize_number_from_string"
)]
pub mem_bytes: u64,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct SimulateHostFunctionResultRaw {
#[serde(deserialize_with = "deserialize_default_from_null")]
pub auth: Vec<String>,
pub xdr: String,
}
#[derive(Debug, Clone)]
pub struct SimulateHostFunctionResult {
pub auth: Vec<SorobanAuthorizationEntry>,
pub xdr: xdr::ScVal,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
#[serde(tag = "type")]
pub enum LedgerEntryChange {
#[serde(rename = "created")]
Created { key: String, after: String },
#[serde(rename = "deleted")]
Deleted { key: String, before: String },
#[serde(rename = "updated")]
Updated {
key: String,
before: String,
after: String,
},
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
pub struct SimulateTransactionResponse {
#[serde(
rename = "minResourceFee",
deserialize_with = "deserialize_number_from_string",
default
)]
pub min_resource_fee: u64,
#[serde(default)]
pub cost: Cost,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub results: Vec<SimulateHostFunctionResultRaw>,
#[serde(rename = "transactionData", default)]
pub transaction_data: String,
#[serde(
deserialize_with = "deserialize_default_from_null",
skip_serializing_if = "Vec::is_empty",
default
)]
pub events: Vec<String>,
#[serde(
rename = "restorePreamble",
skip_serializing_if = "Option::is_none",
default
)]
pub restore_preamble: Option<RestorePreamble>,
#[serde(
rename = "stateChanges",
skip_serializing_if = "Option::is_none",
default
)]
pub state_changes: Option<Vec<LedgerEntryChange>>,
#[serde(rename = "latestLedger")]
pub latest_ledger: u32,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub error: Option<String>,
}
impl SimulateTransactionResponse {
pub fn results(&self) -> Result<Vec<SimulateHostFunctionResult>, Error> {
self.results
.iter()
.map(|r| {
Ok(SimulateHostFunctionResult {
auth: r
.auth
.iter()
.map(|a| {
Ok(SorobanAuthorizationEntry::from_xdr_base64(
a,
Limits::none(),
)?)
})
.collect::<Result<_, Error>>()?,
xdr: xdr::ScVal::from_xdr_base64(&r.xdr, Limits::none())?,
})
})
.collect()
}
pub fn events(&self) -> Result<Vec<DiagnosticEvent>, Error> {
self.events
.iter()
.map(|e| Ok(DiagnosticEvent::from_xdr_base64(e, Limits::none())?))
.collect()
}
pub fn transaction_data(&self) -> Result<SorobanTransactionData, Error> {
Ok(SorobanTransactionData::from_xdr_base64(
&self.transaction_data,
Limits::none(),
)?)
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)]
pub struct RestorePreamble {
#[serde(rename = "transactionData")]
pub transaction_data: String,
#[serde(
rename = "minResourceFee",
deserialize_with = "deserialize_number_from_string"
)]
pub min_resource_fee: u64,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetEventsResponse {
#[serde(deserialize_with = "deserialize_default_from_null")]
pub events: Vec<Event>,
#[serde(rename = "latestLedger")]
pub latest_ledger: u32,
#[serde(rename = "latestLedgerCloseTime")]
pub latest_ledger_close_time: String,
#[serde(rename = "oldestLedger")]
pub oldest_ledger: u32,
#[serde(rename = "oldestLedgerCloseTime")]
pub oldest_ledger_close_time: String,
pub cursor: String,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GetLedgersResponse {
#[serde(rename = "latestLedger")]
pub latest_ledger: u32,
#[serde(
rename = "latestLedgerCloseTime",
deserialize_with = "deserialize_number_from_string"
)]
pub latest_ledger_close_time: i64,
#[serde(rename = "oldestLedger")]
pub oldest_ledger: u32,
#[serde(rename = "oldestLedgerCloseTime")]
pub oldest_ledger_close_time: i64,
pub cursor: String,
pub ledgers: Vec<Ledger>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct Ledger {
pub hash: String,
pub sequence: u32,
#[serde(rename = "ledgerCloseTime")]
pub ledger_close_time: String,
#[serde(rename = "headerXdr")]
pub header_xdr: String,
#[serde(rename = "headerJson")]
pub header_json: Option<LedgerHeaderHistoryEntry>,
#[serde(rename = "metadataXdr")]
pub metadata_xdr: String,
#[serde(rename = "metadataJson")]
pub metadata_json: Option<LedgerCloseMeta>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct Event {
#[serde(rename = "type")]
pub event_type: String,
pub ledger: u32,
#[serde(rename = "ledgerClosedAt")]
pub ledger_closed_at: String,
#[serde(rename = "contractId")]
pub contract_id: String,
pub id: String,
#[serde(
rename = "operationIndex",
default,
skip_serializing_if = "Option::is_none"
)]
pub operation_index: Option<u32>,
#[serde(
rename = "transactionIndex",
default,
skip_serializing_if = "Option::is_none"
)]
pub transaction_index: Option<u32>,
#[serde(rename = "txHash", default, skip_serializing_if = "Option::is_none")]
pub tx_hash: Option<String>,
#[deprecated(
note = "This field is deprecated by Stellar RPC. See https://stellar.org/blog/developers/protocol-23-upgrade-guide"
)]
#[serde(
rename = "inSuccessfulContractCall",
default,
skip_serializing_if = "Option::is_none"
)]
pub is_successful_contract_call: Option<bool>,
pub topic: Vec<String>,
pub value: String,
}
impl Display for Event {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"Event {} [{}]:",
self.id,
self.event_type.to_ascii_uppercase()
)?;
writeln!(
f,
" Ledger: {} (closed at {})",
self.ledger, self.ledger_closed_at
)?;
writeln!(f, " Contract: {}", self.contract_id)?;
writeln!(f, " Topics:")?;
for topic in &self.topic {
let scval =
xdr::ScVal::from_xdr_base64(topic, Limits::none()).map_err(|_| std::fmt::Error)?;
writeln!(f, " {scval:?}")?;
}
let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())
.map_err(|_| std::fmt::Error)?;
writeln!(f, " Value: {scval:?}")
}
}
pub type SegmentFilter = String;
pub type TopicFilter = Vec<SegmentFilter>;
impl Event {
pub fn parse_cursor(&self) -> Result<(u64, i32), Error> {
parse_cursor(&self.id)
}
pub fn pretty_print(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut stdout = StandardStream::stdout(ColorChoice::Auto);
if !stdout.supports_color() {
println!("{self}");
return Ok(());
}
let color = match self.event_type.as_str() {
"system" => Color::Yellow,
_ => Color::Blue,
};
colored!(
stdout,
"{}Event{} {}{}{} [{}{}{}{}]:\n",
bold!(true),
bold!(false),
fg!(Some(Color::Green)),
self.id,
reset!(),
bold!(true),
fg!(Some(color)),
self.event_type.to_ascii_uppercase(),
reset!(),
)?;
colored!(
stdout,
" Ledger: {}{}{} (closed at {}{}{})\n",
fg!(Some(Color::Green)),
self.ledger,
reset!(),
fg!(Some(Color::Green)),
self.ledger_closed_at,
reset!(),
)?;
colored!(
stdout,
" Contract: {}{}{}\n",
fg!(Some(Color::Green)),
self.contract_id,
reset!(),
)?;
colored!(stdout, " Topics:\n")?;
for topic in &self.topic {
let scval = xdr::ScVal::from_xdr_base64(topic, Limits::none())?;
colored!(
stdout,
" {}{:?}{}\n",
fg!(Some(Color::Green)),
scval,
reset!(),
)?;
}
let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())?;
colored!(
stdout,
" Value: {}{:?}{}\n\n",
fg!(Some(Color::Green)),
scval,
reset!(),
)?;
Ok(())
}
}
pub enum AuthMode {
Enforce,
Record,
RecordAllowNonRoot,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
pub enum EventType {
All,
Contract,
System,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum LedgerStart {
Ledger(u32),
Cursor(String),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct LedgerRange {
start: u32,
end: u32,
}
impl LedgerRange {
pub fn start(&self) -> u32 {
self.start
}
pub fn end(&self) -> u32 {
self.end
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum EventStart {
Ledger(u32),
LedgerRange(LedgerRange),
Cursor(String),
}
impl EventStart {
pub fn ledger_range(start: u32, end: u32) -> Result<Self, String> {
if start > end {
return Err(format!(
"invalid ledger range: start ({start}) must be <= end ({end})"
));
}
Ok(EventStart::LedgerRange(LedgerRange { start, end }))
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
pub struct FullLedgerEntry {
pub key: LedgerKey,
pub val: LedgerEntryData,
#[serde(rename = "lastModifiedLedgerSeq")]
pub last_modified_ledger: u32,
#[serde(
rename = "liveUntilLedgerSeq",
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_option_number_from_string",
default
)]
pub live_until_ledger_seq: Option<u32>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct FullLedgerEntries {
pub entries: Vec<FullLedgerEntry>,
#[serde(rename = "latestLedger")]
pub latest_ledger: i64,
}
#[derive(Debug, Clone)]
pub struct Client {
base_url: Arc<str>,
timeout_in_secs: u64,
http_client: Arc<HttpClient>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct ResourceConfig {
#[serde(rename = "instructionLeeway")]
pub instruction_leeway: u64,
}
#[allow(deprecated)] impl Client {
pub fn new(base_url: &str) -> Result<Self, Error> {
let headers = Self::default_http_headers();
let http_client = Arc::new(
HttpClientBuilder::default()
.set_headers(headers)
.build(base_url)?,
);
Ok(Self {
base_url: Arc::from(base_url),
timeout_in_secs: 30,
http_client,
})
}
#[deprecated(
note = "To be marked private in a future major release. Please use `new_with_headers` instead."
)]
pub fn new_with_timeout(base_url: &str, timeout: u64) -> Result<Self, Error> {
let mut client = Self::new(base_url)?;
client.timeout_in_secs = timeout;
Ok(client)
}
pub fn new_with_headers(base_url: &str, additional_headers: HeaderMap) -> Result<Self, Error> {
let mut client = Self::new(base_url)?;
let mut headers = Self::default_http_headers();
for (key, value) in additional_headers {
headers.insert(key.ok_or(Error::InvalidResponse)?, value);
}
let http_client = Arc::new(
HttpClientBuilder::default()
.set_headers(headers)
.build(base_url)?,
);
client.http_client = http_client;
Ok(client)
}
fn default_http_headers() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("X-Client-Name", unsafe {
"rs-stellar-rpc-client".parse().unwrap_unchecked()
});
let version = VERSION.unwrap_or("devel");
headers.insert("X-Client-Version", unsafe {
version.parse().unwrap_unchecked()
});
headers
}
#[must_use]
pub fn base_url(&self) -> &str {
&self.base_url
}
#[must_use]
pub fn client(&self) -> &HttpClient {
&self.http_client
}
pub async fn friendbot_url(&self) -> Result<String, Error> {
let network = self.get_network().await?;
network.friendbot_url.ok_or_else(|| {
Error::NotFound(
"Friendbot".to_string(),
"Friendbot is not available on this network".to_string(),
)
})
}
pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result<String, Error> {
let server = self.get_network().await?.passphrase;
if let Some(expected) = expected {
if expected != server {
return Err(Error::InvalidNetworkPassphrase {
expected: expected.to_string(),
server,
});
}
}
Ok(server)
}
pub async fn get_network(&self) -> Result<GetNetworkResponse, Error> {
Ok(self
.client()
.request("getNetwork", ObjectParams::new())
.await?)
}
pub async fn get_health(&self) -> Result<GetHealthResponse, Error> {
Ok(self
.client()
.request("getHealth", ObjectParams::new())
.await?)
}
pub async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, Error> {
Ok(self
.client()
.request("getLatestLedger", ObjectParams::new())
.await?)
}
pub async fn get_ledgers(
&self,
start: LedgerStart,
limit: Option<usize>,
format: Option<String>,
) -> Result<GetLedgersResponse, Error> {
let mut oparams = ObjectParams::new();
let mut pagination = serde_json::Map::new();
if let Some(limit) = limit {
pagination.insert("limit".to_string(), limit.into());
}
match start {
LedgerStart::Ledger(l) => oparams.insert("startLedger", l)?,
LedgerStart::Cursor(c) => {
pagination.insert("cursor".to_string(), c.into());
}
}
oparams.insert("pagination", pagination)?;
if let Some(f) = format {
oparams.insert("xdrFormat", f)?;
}
Ok(self.client().request("getLedgers", oparams).await?)
}
pub async fn get_account(&self, address: &str) -> Result<AccountEntry, Error> {
let key = LedgerKey::Account(LedgerKeyAccount {
account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
stellar_strkey::ed25519::PublicKey::from_string(address)?.0,
))),
});
let keys = Vec::from([key]);
let response = self.get_ledger_entries(&keys).await?;
let entries = response.entries.unwrap_or_default();
if entries.is_empty() {
return Err(Error::NotFound("Account".to_string(), address.to_owned()));
}
let ledger_entry = &entries[0];
let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none());
if let LedgerEntryData::Account(entry) = LedgerEntryData::read_xdr_base64(&mut read)? {
Ok(entry)
} else {
Err(Error::InvalidResponse)
}
}
pub async fn get_fee_stats(&self) -> Result<GetFeeStatsResponse, Error> {
Ok(self
.client()
.request("getFeeStats", ObjectParams::new())
.await?)
}
pub async fn get_version_info(&self) -> Result<GetVersionInfoResponse, Error> {
Ok(self
.client()
.request("getVersionInfo", ObjectParams::new())
.await?)
}
pub async fn send_transaction(&self, tx: &TransactionEnvelope) -> Result<Hash, Error> {
let mut oparams = ObjectParams::new();
oparams.insert("transaction", tx.to_xdr_base64(Limits::none())?)?;
let SendTransactionResponse {
hash,
error_result_xdr,
status,
..
} = self
.client()
.request("sendTransaction", oparams)
.await
.map_err(|err| {
Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}"))
})?;
if status == "ERROR" {
let error = error_result_xdr
.ok_or(Error::MissingError)
.and_then(|x| {
TransactionResult::read_xdr_base64(&mut Limited::new(
x.as_bytes(),
Limits::none(),
))
.map_err(|_| Error::InvalidResponse)
})
.map(|r| r.result)?;
return Err(Error::TransactionSubmissionFailed(format!("{error:#?}")));
}
Ok(Hash::from_str(&hash)?)
}
pub async fn send_transaction_polling(
&self,
tx: &TransactionEnvelope,
) -> Result<GetTransactionResponse, Error> {
let hash = self.send_transaction(tx).await?;
self.get_transaction_polling(&hash, None).await
}
pub async fn simulate_transaction_envelope(
&self,
tx: &TransactionEnvelope,
auth_mode: Option<AuthMode>,
) -> Result<SimulateTransactionResponse, Error> {
let base64_tx = tx.to_xdr_base64(Limits::none())?;
let mut params = ObjectParams::new();
params.insert("transaction", base64_tx)?;
match auth_mode {
Some(AuthMode::Enforce) => {
params.insert("authMode", "enforce")?;
}
Some(AuthMode::Record) => {
params.insert("authMode", "record")?;
}
Some(AuthMode::RecordAllowNonRoot) => {
params.insert("authMode", "record_allow_nonroot")?;
}
None => {}
}
let sim_res = self.client().request("simulateTransaction", params).await?;
Ok(sim_res)
}
pub async fn next_simulate_transaction_envelope(
&self,
tx: &TransactionEnvelope,
auth_mode: Option<AuthMode>,
resource_config: Option<ResourceConfig>,
) -> Result<SimulateTransactionResponse, Error> {
let base64_tx = tx.to_xdr_base64(Limits::none())?;
let mut params = ObjectParams::new();
params.insert("transaction", base64_tx)?;
match auth_mode {
Some(AuthMode::Enforce) => {
params.insert("authMode", "enforce")?;
}
Some(AuthMode::Record) => {
params.insert("authMode", "record")?;
}
Some(AuthMode::RecordAllowNonRoot) => {
params.insert("authMode", "record_allow_nonroot")?;
}
None => {}
}
if let Some(ref config) = resource_config {
let mut resource_config_params = ObjectParams::new();
resource_config_params.insert("instructionLeeway", config.instruction_leeway)?;
params.insert("resourceConfig", resource_config)?;
}
let sim_res = self.client().request("simulateTransaction", params).await?;
Ok(sim_res)
}
pub async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, Error> {
let mut oparams = ObjectParams::new();
oparams.insert("hash", tx_id)?;
let resp: GetTransactionResponseRaw =
self.client().request("getTransaction", oparams).await?;
Ok(resp.try_into()?)
}
pub async fn get_transactions(
&self,
request: GetTransactionsRequest,
) -> Result<GetTransactionsResponse, Error> {
let mut oparams = ObjectParams::new();
if let Some(start_ledger) = request.start_ledger {
oparams.insert("startLedger", start_ledger)?;
}
if let Some(pagination_params) = request.pagination {
let pagination = serde_json::json!(pagination_params);
oparams.insert("pagination", pagination)?;
}
let resp: GetTransactionsResponseRaw =
self.client().request("getTransactions", oparams).await?;
Ok(resp.try_into()?)
}
pub async fn get_transaction_polling(
&self,
tx_id: &Hash,
timeout_s: Option<Duration>,
) -> Result<GetTransactionResponse, Error> {
let start = Instant::now();
let timeout = timeout_s.unwrap_or(Duration::from_secs(self.timeout_in_secs));
let exponential_backoff: f64 = 1.0 / (1.0 - E.powf(-1.0));
let mut sleep_time = Duration::from_secs(1);
loop {
let response = self.get_transaction(tx_id).await?;
match response.status.as_str() {
"SUCCESS" => return Ok(response),
"FAILED" => {
return Err(Error::TransactionSubmissionFailed(format!(
"{:#?}",
response.result
)))
}
"NOT_FOUND" => (),
_ => {
return Err(Error::UnexpectedTransactionStatus(response.status));
}
}
if start.elapsed() > timeout {
return Err(Error::TransactionSubmissionTimeout);
}
sleep(sleep_time).await;
sleep_time = Duration::from_secs_f64(sleep_time.as_secs_f64() * exponential_backoff);
}
}
pub async fn get_ledger_entries(
&self,
keys: &[LedgerKey],
) -> Result<GetLedgerEntriesResponse, Error> {
let mut base64_keys: Vec<String> = vec![];
for k in keys {
let base64_result = k.to_xdr_base64(Limits::none());
if base64_result.is_err() {
return Err(Error::Xdr(XdrError::Invalid));
}
base64_keys.push(k.to_xdr_base64(Limits::none())?);
}
let mut oparams = ObjectParams::new();
oparams.insert("keys", base64_keys)?;
Ok(self.client().request("getLedgerEntries", oparams).await?)
}
pub async fn get_full_ledger_entries(
&self,
ledger_keys: &[LedgerKey],
) -> Result<FullLedgerEntries, Error> {
let keys = ledger_keys
.iter()
.filter(|key| !matches!(key, LedgerKey::Ttl(_)))
.map(Clone::clone)
.collect::<Vec<_>>();
let GetLedgerEntriesResponse {
entries,
latest_ledger,
} = self.get_ledger_entries(&keys).await?;
let entries = entries
.unwrap_or_default()
.iter()
.map(
|LedgerEntryResult {
key,
xdr,
last_modified_ledger,
live_until_ledger_seq_ledger_seq,
}| {
Ok(FullLedgerEntry {
key: LedgerKey::from_xdr_base64(key, Limits::none())?,
val: LedgerEntryData::from_xdr_base64(xdr, Limits::none())?,
live_until_ledger_seq: *live_until_ledger_seq_ledger_seq,
last_modified_ledger: *last_modified_ledger,
})
},
)
.collect::<Result<Vec<_>, Error>>()?;
Ok(FullLedgerEntries {
entries,
latest_ledger,
})
}
pub async fn get_events(
&self,
start: EventStart,
event_type: Option<EventType>,
contract_ids: &[String],
topics: &[TopicFilter],
limit: Option<usize>,
) -> Result<GetEventsResponse, Error> {
let mut filters = serde_json::Map::new();
event_type
.and_then(|t| match t {
EventType::All => None, EventType::Contract => Some("contract"),
EventType::System => Some("system"),
})
.map(|t| filters.insert("type".to_string(), t.into()));
filters.insert("topics".to_string(), topics.into());
filters.insert("contractIds".to_string(), contract_ids.into());
let mut pagination = serde_json::Map::new();
if let Some(limit) = limit {
pagination.insert("limit".to_string(), limit.into());
}
let mut oparams = ObjectParams::new();
match start {
EventStart::Ledger(l) => oparams.insert("startLedger", l)?,
EventStart::LedgerRange(r) => {
oparams.insert("startLedger", r.start())?;
oparams.insert("endLedger", r.end())?;
}
EventStart::Cursor(c) => {
pagination.insert("cursor".to_string(), c.into());
}
}
oparams.insert("filters", vec![filters])?;
oparams.insert("pagination", pagination)?;
Ok(self.client().request("getEvents", oparams).await?)
}
pub async fn get_contract_data(
&self,
contract_id: &[u8; 32],
) -> Result<ContractDataEntry, Error> {
let contract_key = LedgerKey::ContractData(xdr::LedgerKeyContractData {
contract: xdr::ScAddress::Contract(ContractId(xdr::Hash(*contract_id))),
key: xdr::ScVal::LedgerKeyContractInstance,
durability: xdr::ContractDataDurability::Persistent,
});
let contract_ref = self.get_ledger_entries(&[contract_key]).await?;
let entries = contract_ref.entries.unwrap_or_default();
if entries.is_empty() {
let contract_address = stellar_strkey::Contract(*contract_id).to_string();
return Err(Error::NotFound(
"Contract".to_string(),
contract_address.to_string(),
));
}
let contract_ref_entry = &entries[0];
match LedgerEntryData::from_xdr_base64(&contract_ref_entry.xdr, Limits::none())? {
LedgerEntryData::ContractData(contract_data) => Ok(contract_data),
scval => Err(Error::UnexpectedContractCodeDataType(Box::new(scval))),
}
}
#[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result<Vec<u8>, Error> {
match self.get_contract_data(contract_id).await? {
xdr::ContractDataEntry {
val:
xdr::ScVal::ContractInstance(xdr::ScContractInstance {
executable: xdr::ContractExecutable::Wasm(hash),
..
}),
..
} => self.get_remote_wasm_from_hash(hash).await,
scval => Err(Error::UnexpectedToken(Box::new(scval))),
}
}
#[deprecated(note = "To be removed in future versions, use get_ledger_entries()")]
pub async fn get_remote_wasm_from_hash(&self, hash: Hash) -> Result<Vec<u8>, Error> {
let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
let contract_data = self.get_ledger_entries(&[code_key]).await?;
let entries = contract_data.entries.unwrap_or_default();
if entries.is_empty() {
return Err(Error::NotFound(
"Contract Code".to_string(),
hex::encode(hash),
));
}
let contract_data_entry = &entries[0];
match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
scval => Err(Error::UnexpectedContractCodeDataType(Box::new(scval))),
}
}
pub async fn get_contract_instance(
&self,
contract_id: &[u8; 32],
) -> Result<ScContractInstance, Error> {
let contract_data = self.get_contract_data(contract_id).await?;
match contract_data.val {
xdr::ScVal::ContractInstance(instance) => Ok(instance),
scval => Err(Error::UnexpectedContractInstance(Box::new(scval))),
}
}
}
pub(crate) fn parse_cursor(c: &str) -> Result<(u64, i32), Error> {
let (toid_part, event_index) = c.split('-').collect_tuple().ok_or(Error::InvalidCursor)?;
let toid_part: u64 = toid_part.parse().map_err(|_| Error::InvalidCursor)?;
let start_index: i32 = event_index.parse().map_err(|_| Error::InvalidCursor)?;
Ok((toid_part, start_index))
}
fn deserialize_option_i64_from_string_or_number<'de, D>(
deserializer: D,
) -> Result<Option<i64>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrNumber {
String(String),
Number(i64),
}
match Option::<StringOrNumber>::deserialize(deserializer)? {
None => Ok(None),
Some(StringOrNumber::String(s)) => {
s.parse::<i64>().map(Some).map_err(serde::de::Error::custom)
}
Some(StringOrNumber::Number(n)) => Ok(Some(n)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
use std::path::PathBuf;
fn does_topic_match(topic: &[String], filter: &[String]) -> bool {
if filter.is_empty() {
return false;
}
if let Some((last, prefix)) = filter.split_last() {
if last == "**" {
return topic.len() >= prefix.len()
&& prefix
.iter()
.enumerate()
.all(|(i, s)| *s == "*" || topic[i] == *s);
}
}
filter.len() == topic.len()
&& filter
.iter()
.enumerate()
.all(|(i, s)| *s == "*" || topic[i] == *s)
}
fn get_repo_root() -> PathBuf {
let mut path = env::current_exe().expect("Failed to get current executable path");
while path.pop() {
if path.join("Cargo.toml").exists() {
return path;
}
}
panic!("Could not find repository root");
}
fn read_json_file(name: &str) -> String {
let repo_root = get_repo_root();
let fixture_path = repo_root.join("src").join("fixtures").join(name);
fs::read_to_string(fixture_path).unwrap_or_else(|_| panic!("Failed to read {name:?}"))
}
#[test]
fn simulation_transaction_response_parsing() {
let s = r#"{
"minResourceFee": "100000000",
"cost": { "cpuInsns": "1000", "memBytes": "1000" },
"transactionData": "",
"latestLedger": 1234,
"stateChanges": [{
"type": "created",
"key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==",
"before": null,
"after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}]
}"#;
let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
assert_eq!(
resp.state_changes.unwrap()[0],
LedgerEntryChange::Created { key: "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==".to_string(), after: "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string() },
);
assert_eq!(resp.min_resource_fee, 100_000_000);
}
#[test]
fn simulation_transaction_response_parsing_mostly_empty() {
let s = r#"{
"latestLedger": 1234
}"#;
let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap();
assert_eq!(resp.latest_ledger, 1_234);
}
#[test]
fn test_parse_transaction_response_p23() {
let response_content = read_json_file("transaction_response_p23.json");
let full_response: serde_json::Value = serde_json::from_str(&response_content)
.expect("Failed to parse JSON from transaction_response_p23.json");
let result = full_response["result"].clone();
let raw_response: GetTransactionResponseRaw = serde_json::from_value(result)
.expect("Failed to parse 'result' into GetTransactionResponseRaw");
let response: GetTransactionResponse = raw_response
.try_into()
.expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
assert_eq!(2, response.events.transaction_events.iter().len());
assert_eq!(1, response.events.contract_events.len());
assert_eq!(21, response.events.diagnostic_events.iter().len());
assert_eq!(
response.tx_hash.as_deref(),
Some("bfe15f83ea850b7bf86fd7152f9074033f2aec2a045e40a8872ac56726a6e35c")
);
assert_eq!(response.created_at, Some(1_751_666_924));
assert_eq!(response.application_order, Some(1));
assert_eq!(response.fee_bump, Some(false));
}
#[test]
fn test_parse_transaction_response_p22() {
let response_content = read_json_file("transaction_response_p22.json");
let full_response: serde_json::Value = serde_json::from_str(&response_content)
.expect("Failed to parse JSON from transaction_response_p22.json");
let result = full_response["result"].clone();
let raw_response: GetTransactionResponseRaw = serde_json::from_value(result)
.expect("Failed to parse 'result' into GetTransactionResponseRaw");
let response: GetTransactionResponse = raw_response
.try_into()
.expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
assert_eq!(23, response.events.diagnostic_events.iter().len());
assert_eq!(
response.tx_hash.as_deref(),
Some("a738ccc7f8f457d4367b78c098569ebee23258c71f128d7a2c61585652345937")
);
assert_eq!(response.created_at, Some(1_751_747_980));
assert_eq!(response.application_order, Some(1));
assert_eq!(response.fee_bump, Some(false));
}
#[test]
fn test_parse_get_transactions_response() {
let response_content = read_json_file("transactions_response.json");
let full_response: serde_json::Value = serde_json::from_str(&response_content)
.expect("Failed to parse JSON from transactions_response.json");
let result = full_response["result"].clone();
let raw_response: GetTransactionsResponseRaw = serde_json::from_value(result)
.expect("Failed to parse 'result' into GetTransactionsResponseRaw");
let response: GetTransactionsResponse = raw_response
.try_into()
.expect("Failed to convert GetTransactionsResponseRaw to GetTransactionsResponse");
assert_eq!(response.transactions.len(), 5);
assert_eq!(response.latest_ledger, 556_962);
assert_eq!(response.cursor, 2_379_420_471_922_689);
assert_eq!(response.transactions[0].status, "SUCCESS");
}
#[test]
fn test_rpc_url_default_ports() {
let client = Client::new("http://example.com").unwrap();
assert_eq!(client.base_url(), "http://example.com");
let client = Client::new("https://example.com").unwrap();
assert_eq!(client.base_url(), "https://example.com");
let client = Client::new("http://example.com:8080").unwrap();
assert_eq!(client.base_url(), "http://example.com:8080");
let client = Client::new("https://example.com:8080").unwrap();
assert_eq!(client.base_url(), "https://example.com:8080");
let client = Client::new("http://example.com/a/b/c").unwrap();
assert_eq!(client.base_url(), "http://example.com/a/b/c");
let client = Client::new("https://example.com/a/b/c/").unwrap();
assert_eq!(client.base_url(), "https://example.com/a/b/c/");
}
#[test]
fn test_parse_events_response() {
let response_content = read_json_file("events_response_p23.json");
let full_response: serde_json::Value = serde_json::from_str(&response_content)
.expect("Failed to parse JSON from events_response_p23.json");
let result = full_response["result"].clone();
let resp: GetEventsResponse = serde_json::from_value(result.clone())
.expect("Failed to parse 'result' into GetEventsResponse");
assert_eq!(resp.events[0].operation_index, Some(0));
assert_eq!(resp.events[0].transaction_index, Some(0));
assert_eq!(
resp.events[0].tx_hash.as_deref(),
Some("e42da3c70c90cc319e2cfaa2f69a7bd04aefcc4159b12caa0df216fbb3ab43b4")
);
#[allow(deprecated)]
{
assert_eq!(resp.events[0].is_successful_contract_call, Some(true));
}
let reserialized = serde_json::to_value(&resp).expect("Failed to serialize response");
assert_eq!(
result, reserialized,
"Deserialization should preserve all data"
);
}
#[test]
fn test_parse_events_response_p22() {
let response_content = read_json_file("events_response_p22.json");
let full_response: serde_json::Value = serde_json::from_str(&response_content)
.expect("Failed to parse JSON from events_response_p22.json");
let first_event = full_response["result"]["events"][0].clone();
let event: Event = serde_json::from_value(first_event)
.expect("Failed to parse protocol 22 event into Event");
assert!(event.operation_index.is_none());
assert!(event.transaction_index.is_none());
}
#[test]
fn test_ledger_range_valid() {
let r = EventStart::ledger_range(10, 20).unwrap();
assert_eq!(r, EventStart::ledger_range(10, 20).unwrap());
assert!(EventStart::ledger_range(10, 10).is_ok());
}
#[test]
fn test_ledger_range_invalid() {
let err = EventStart::ledger_range(100, 50).unwrap_err();
assert!(err.contains("start (100)") && err.contains("end (50)"));
}
#[test]
fn test_does_topic_match() {
struct TestCase<'a> {
name: &'a str,
filter: Vec<&'a str>,
includes: Vec<Vec<&'a str>>,
excludes: Vec<Vec<&'a str>>,
}
let xfer = "AAAABQAAAAh0cmFuc2Zlcg==";
let number = "AAAAAQB6Mcc=";
let star = "*";
for tc in vec![
TestCase {
name: "<empty>",
filter: vec![],
includes: vec![],
excludes: vec![vec![xfer]],
},
TestCase {
name: "*",
filter: vec![star],
includes: vec![vec![xfer]],
excludes: vec![vec![xfer, xfer], vec![xfer, number]],
},
TestCase {
name: "*/transfer",
filter: vec![star, xfer],
includes: vec![vec![number, xfer], vec![xfer, xfer]],
excludes: vec![
vec![number],
vec![number, number],
vec![number, xfer, number],
vec![xfer],
vec![xfer, number],
vec![xfer, xfer, xfer],
],
},
TestCase {
name: "transfer/*",
filter: vec![xfer, star],
includes: vec![vec![xfer, number], vec![xfer, xfer]],
excludes: vec![
vec![number],
vec![number, number],
vec![number, xfer, number],
vec![xfer],
vec![number, xfer],
vec![xfer, xfer, xfer],
],
},
TestCase {
name: "transfer/*/*",
filter: vec![xfer, star, star],
includes: vec![vec![xfer, number, number], vec![xfer, xfer, xfer]],
excludes: vec![
vec![number],
vec![number, number],
vec![number, xfer],
vec![number, xfer, number, number],
vec![xfer],
vec![xfer, xfer, xfer, xfer],
],
},
TestCase {
name: "transfer/*/number",
filter: vec![xfer, star, number],
includes: vec![vec![xfer, number, number], vec![xfer, xfer, number]],
excludes: vec![
vec![number],
vec![number, number],
vec![number, number, number],
vec![number, xfer, number],
vec![xfer],
vec![number, xfer],
vec![xfer, xfer, xfer],
vec![xfer, number, xfer],
],
},
TestCase {
name: "**",
filter: vec!["**"],
includes: vec![
vec![],
vec![xfer],
vec![xfer, number],
vec![xfer, number, number],
],
excludes: vec![],
},
TestCase {
name: "transfer/**",
filter: vec![xfer, "**"],
includes: vec![
vec![xfer],
vec![xfer, number],
vec![xfer, number, number],
vec![xfer, xfer, xfer],
],
excludes: vec![
vec![],
vec![number],
vec![number, xfer],
vec![number, number],
],
},
TestCase {
name: "transfer/number/**",
filter: vec![xfer, number, "**"],
includes: vec![
vec![xfer, number],
vec![xfer, number, number],
vec![xfer, number, xfer, number],
],
excludes: vec![
vec![],
vec![xfer],
vec![number],
vec![number, xfer],
vec![xfer, xfer],
],
},
] {
for topic in tc.includes {
assert!(
does_topic_match(
&topic
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>(),
&tc.filter
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
),
"test: {}, topic ({:?}) should be matched by filter ({:?})",
tc.name,
topic,
tc.filter
);
}
for topic in tc.excludes {
assert!(
!does_topic_match(
&topic
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>(),
&tc.filter
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
),
"test: {}, topic ({:?}) should NOT be matched by filter ({:?})",
tc.name,
topic,
tc.filter
);
}
}
}
}