use crate::jsonrpc::{JsonRpc, Response};
use crate::transaction::assemble_transaction;
use crate::{error, soroban_rpc::*};
use crate::{error::*, friendbot};
use futures::TryFutureExt;
use serde_json::json;
use std::option::Option;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
use stellar_baselib::account::Account;
use stellar_baselib::account::AccountBehavior;
use stellar_baselib::address::{Address, AddressTrait};
use stellar_baselib::keypair::KeypairBehavior;
use stellar_baselib::transaction::{Transaction, TransactionBehavior};
use stellar_baselib::xdr::{
ContractDataDurability, LedgerEntryData, LedgerKey, LedgerKeyAccount, LedgerKeyContractData,
Limits, ScVal, WriteXdr,
};
use tokio::time::{sleep, Instant};
pub const SUBMIT_TRANSACTION_TIMEOUT: u32 = 60 * 1000;
#[derive(Debug, PartialEq, Eq)]
pub enum Durability {
Temporary,
Persistent,
}
impl Durability {
fn to_xdr(&self) -> ContractDataDurability {
match self {
Durability::Temporary => ContractDataDurability::Temporary,
Durability::Persistent => ContractDataDurability::Persistent,
}
}
}
pub enum Pagination {
From(u32),
FromTo(u32, u32),
Cursor(String),
}
pub struct EventFilter {
event_type: EventType,
contract_ids: Vec<String>,
topics: Vec<Vec<Topic>>,
}
#[derive(Clone, Debug)]
pub enum Topic {
Val(ScVal),
Any,
Greedy,
}
impl EventFilter {
pub fn new(event_type: EventType) -> Self {
EventFilter {
event_type,
contract_ids: Vec::new(),
topics: Vec::new(),
}
}
pub fn contract(self, contract_id: &str) -> Self {
let mut contract_ids = self.contract_ids.to_vec();
contract_ids.push(contract_id.to_string());
EventFilter {
contract_ids,
..self
}
}
pub fn topic(self, filer: Vec<Topic>) -> Self {
let mut topics = self.topics.to_vec();
topics.push(filer);
EventFilter { topics, ..self }
}
fn event_type(&self) -> Option<String> {
match self.event_type {
EventType::Contract => Some("contract".to_string()),
EventType::System => Some("system".to_string()),
EventType::Diagnostic => Some("diagnostic".to_string()),
EventType::All => None,
}
}
fn contracts(&self) -> Vec<String> {
self.contract_ids.to_vec()
}
fn topics(&self) -> Vec<Vec<String>> {
self.topics
.iter()
.map(|v| {
v.iter()
.map(|vv| match vv {
Topic::Val(sc_val) => sc_val
.to_xdr_base64(Limits::none())
.expect("ScVal cannot be converted to base64"),
Topic::Any => "*".to_string(),
Topic::Greedy => "**".to_string(),
})
.collect()
})
.collect()
}
}
#[derive(Debug, Clone, Default)]
pub struct SimulationOptions {
pub cpu_instructions: u64,
pub auth_mode: Option<AuthMode>,
}
#[derive(Debug, Clone)]
pub enum AuthMode {
Enforce,
Record,
RecordAllowNonRoot,
}
impl FromStr for AuthMode {
type Err = error::AuthModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"enforce" => Ok(Self::Enforce),
"record" => Ok(Self::Record),
"record_allow_nonroot" => Ok(Self::RecordAllowNonRoot),
e => Err(AuthModeError::Invalid(e.to_string())),
}
}
}
impl From<AuthMode> for &str {
fn from(val: AuthMode) -> Self {
match val {
AuthMode::Enforce => "enforce",
AuthMode::Record => "record",
AuthMode::RecordAllowNonRoot => "record_allow_nonroot",
}
}
}
#[derive(Debug)]
pub struct Options {
pub allow_http: bool,
pub timeout: u64,
pub headers: HashMap<String, String>,
pub friendbot_url: Option<String>,
}
impl Default for Options {
fn default() -> Self {
Self {
allow_http: false,
timeout: 10,
headers: Default::default(),
friendbot_url: None,
}
}
}
#[derive(Debug)]
pub struct Server {
client: JsonRpc,
friendbot_url: Option<String>,
}
impl Server {
pub fn new(server_url: &str, opts: Options) -> Result<Self, Error> {
let server_url = reqwest::Url::from_str(server_url)
.map_err(|_e| Error::InvalidRpc(InvalidRpcUrl::InvalidUri))?;
let allow_http = opts.allow_http;
match server_url.scheme() {
"https" => {
}
"http" if allow_http => {
}
"http" if !allow_http => {
return Err(Error::InvalidRpc(InvalidRpcUrl::UnsecureHttpNotAllowed));
}
_ => {
return Err(Error::InvalidRpc(InvalidRpcUrl::NotHttpScheme));
}
};
Ok(Server {
client: JsonRpc::new(server_url, opts.timeout, opts.headers),
friendbot_url: opts.friendbot_url,
})
}
pub async fn get_events(
&self,
ledger: Pagination,
filters: Vec<EventFilter>,
limit: impl Into<Option<u32>>,
) -> Result<GetEventsResponse, Error> {
let (start_ledger, end_ledger, cursor) = match ledger {
Pagination::From(s) => (Some(s), None, None),
Pagination::FromTo(s, e) => (Some(s), Some(e), None),
Pagination::Cursor(c) => (None, None, Some(c)),
};
let filters = filters
.into_iter()
.map(|v| {
json!({
"type": v.event_type(),
"contractIds": v.contracts(),
"topics": v.topics(),
})
})
.collect::<Vec<serde_json::Value>>();
let params = json!(
{
"startLedger": start_ledger,
"endLedger": end_ledger,
"filters": filters,
"pagination": {
"cursor": cursor,
"limit": limit.into()
}
}
);
let response = self.client.post("getEvents", params).await?;
handle_response(response)
}
pub async fn get_fee_stats(&self) -> Result<GetFeeStatsResponse, Error> {
let response = self
.client
.post("getFeeStats", serde_json::Value::Null)
.await?;
handle_response(response)
}
pub async fn get_health(&self) -> Result<GetHealthResponse, Error> {
let response = self
.client
.post("getHealth", serde_json::Value::Null)
.await?;
handle_response(response)
}
pub async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, Error> {
let response = self
.client
.post("getLatestLedger", serde_json::Value::Null)
.await?;
handle_response(response)
}
pub async fn get_ledger_entries(
&self,
keys: Vec<LedgerKey>,
) -> Result<GetLedgerEntriesResponse, Error> {
let keys: Result<Vec<String>, Error> = keys
.into_iter()
.map(|k| k.to_xdr_base64(Limits::none()).map_err(|_| Error::XdrError))
.collect();
match keys {
Ok(keys) => {
let params = json!({"keys": keys});
let response: Response<GetLedgerEntriesResponse> =
self.client.post("getLedgerEntries", params).await?;
handle_response(response)
}
Err(err) => Err(err),
}
}
pub async fn get_ledgers(
&self,
ledger: Pagination,
limit: impl Into<Option<u32>>,
) -> Result<GetLedgersResponse, Error> {
let (start_ledger, cursor) = match ledger {
Pagination::From(s) => (Some(s), None),
Pagination::FromTo(s, _) => (Some(s), None),
Pagination::Cursor(c) => (None, Some(c)),
};
let params = json!(
{
"startLedger": start_ledger,
"pagination": {
"cursor": cursor,
"limit": limit.into()
}
}
);
let response = self.client.post("getLedgers", params).await?;
handle_response(response)
}
pub async fn get_network(&self) -> Result<GetNetworkResponse, Error> {
let response = self
.client
.post("getNetwork", serde_json::Value::Null)
.await?;
handle_response(response)
}
pub async fn get_transaction(&self, hash: &str) -> Result<GetTransactionResponse, Error> {
let params = json!({
"hash": hash
});
let response = self.client.post("getTransaction", params).await?;
handle_response(response)
}
pub async fn get_transactions(
&self,
ledger: Pagination,
limit: impl Into<Option<u32>>,
) -> Result<GetTransactionsResponse, Error> {
let (start_ledger, cursor) = match ledger {
Pagination::From(s) => (Some(s), None),
Pagination::FromTo(s, _) => (Some(s), None),
Pagination::Cursor(c) => (None, Some(c)),
};
let params = json!(
{
"startLedger": start_ledger,
"pagination": {
"cursor": cursor,
"limit": limit.into()
}
}
);
let response = self.client.post("getTransactions", params).await?;
handle_response(response)
}
pub async fn get_version_info(&self) -> Result<GetVersionInfoResponse, Error> {
let response = self
.client
.post("getVersionInfo", serde_json::Value::Null)
.await?;
handle_response(response)
}
pub async fn send_transaction(
&self,
transaction: Transaction,
) -> Result<SendTransactionResponse, Error> {
let transaction_xdr = transaction
.to_envelope()
.map_err(|_| Error::TransactionError)?
.to_xdr_base64(Limits::none())
.map_err(|_| Error::XdrError)?;
let params = json!({
"transaction": transaction_xdr
}
);
let response = self.client.post("sendTransaction", params).await?;
handle_response(response)
}
pub async fn simulate_transaction(
&self,
transaction: &Transaction,
options: Option<SimulationOptions>,
) -> Result<SimulateTransactionResponse, Error> {
let transaction_xdr = transaction
.to_envelope()
.map_err(|_| Error::TransactionError)?
.to_xdr_base64(Limits::none())
.map_err(|_| Error::XdrError)?;
let params = if let Some(resources) = options {
json!({
"transaction": transaction_xdr,
"resourceConfig": {
"instructionLeeway": resources.cpu_instructions
},
"authMode": resources.auth_mode.map(|a| {let mode: &str = a.into(); mode}),
})
} else {
json!({
"transaction": transaction_xdr
})
};
let response = self.client.post("simulateTransaction", params).await?;
handle_response(response)
}
pub async fn get_account(&self, address: &str) -> Result<Account, Error> {
let account_id = stellar_baselib::keypair::Keypair::from_public_key(address)
.map_err(|_| Error::AccountNotFound)?
.xdr_account_id();
let ledger_key = LedgerKey::Account(LedgerKeyAccount { account_id });
let resp = self.get_ledger_entries(vec![ledger_key]).await?;
let entries = resp.entries.unwrap_or_default();
if entries.is_empty() {
return Err(Error::AccountNotFound);
}
if let LedgerEntryData::Account(account_entry) = entries[0].to_data() {
Ok(Account::new(address, &account_entry.seq_num.0.to_string()).unwrap())
} else {
Err(Error::AccountNotFound)
}
}
pub async fn get_contract_data(
&self,
contract: &str,
key: ScVal,
durability: Durability,
) -> Result<LedgerEntryResult, Error> {
let sc_address = Address::new(contract)
.map_err(|_| Error::ContractDataNotFound)?
.to_sc_address()
.map_err(|_| Error::ContractDataNotFound)?;
let contract_key = LedgerKey::ContractData(LedgerKeyContractData {
key: key.clone(),
contract: sc_address,
durability: durability.to_xdr(),
});
let val = vec![contract_key];
let response = self.get_ledger_entries(val).await?;
if let Some(entries) = response.entries {
if let Some(entry) = entries.first() {
Ok(entry.clone())
} else {
Err(Error::ContractDataNotFound)
}
} else {
Err(Error::ContractDataNotFound)
}
}
pub async fn prepare_transaction(
&self,
transaction: &Transaction,
) -> Result<Transaction, Error> {
let sim_response = self.simulate_transaction(transaction, None).await?;
assemble_transaction(transaction, sim_response)
}
pub async fn request_airdrop(&self, account_id: &str) -> Result<Account, Error> {
let friendbot_url = if let Some(url) = self.friendbot_url.clone() {
url
} else {
let network = self.get_network().await?;
if let Some(url) = network.friendbot_url {
url
} else {
return Err(Error::NoFriendbot);
}
};
let client = reqwest::ClientBuilder::new()
.build()
.map_err(Error::NetworkError)?;
let response = client
.get(friendbot_url + "?addr=" + account_id)
.send()
.map_err(Error::NetworkError)
.await?;
let data: friendbot::FriendbotResponse =
response.json().map_err(Error::NetworkError).await?;
if let Some(success) = data.successful {
if success {
self.get_account(account_id).await
} else {
Err(Error::AccountNotFound)
}
} else {
self.get_account(account_id).await
}
}
pub async fn wait_transaction(
&self,
hash: &str,
max_wait: Duration,
) -> Result<GetTransactionResponse, (Error, Option<GetTransactionResponse>)> {
let mut delay = Duration::from_secs(1);
let start = Instant::now();
let mut last_response: Option<GetTransactionResponse> = None;
while start.elapsed() < max_wait {
match self.get_transaction(hash).await {
Ok(tx) => match tx.status {
TransactionStatus::Success | TransactionStatus::Failed => {
return Ok(tx);
}
TransactionStatus::NotFound => {
last_response = Some(tx);
sleep(delay).await;
delay = std::cmp::min(delay * 2, Duration::from_secs(60));
}
},
Err(e) => {
return Err((e, last_response));
}
}
}
Err((
Error::WaitTransactionTimeout(max_wait.as_secs(), start.elapsed().as_secs()),
last_response,
))
}
}
fn handle_response<T>(response: Response<T>) -> Result<T, Error> {
if let Some(result) = response.result {
Ok(result)
} else if let Some(error) = response.error {
Err(Error::RPCError {
code: error.code,
message: error.message.unwrap_or_default(),
})
} else {
Err(Error::UnexpectedError)
}
}
#[cfg(test)]
mod test {}