use anyhow::{Context, Result};
use stellar_xdr::curr::{LedgerCloseMeta, LedgerCloseMetaBatch, Limits, ReadXdr, WriteXdr};
use stellar_strkey::Strkey;
use crate::config::Config;
const REFLECTOR_STELLAR_CONTRACT: &str = "CALI2BYU2JE6WVRUFYTS6MSBNEHGJ35P4AVCZYF3B6QOE3QKOB2PLE6M";
const REFLECTOR_CRYPTO_CONTRACT: &str = "CAFJZQWSED6YAWZU3GWRTOCNPPCGBN32L7QV43XX5LZLFTK6JLN34DLN";
const REFLECTOR_FIAT_CONTRACT: &str = "CBKGPWGKSKZF52CFHMTRR23TBWTPMRDIYZ4O2P5VS65BMHYH4DXMCJZC";
const REFLECTOR_DECIMALS: u32 = 14;
const CRYPTO_ASSETS: &[&str] = &[
"BTC", "ETH", "USDT", "XRP", "SOL", "USDC", "ADA", "AVAX", "DOT",
"MATIC", "LINK", "DAI", "ATOM", "XLM", "UNI", "EURC"
];
const FIAT_ASSETS: &[&str] = &[
"EUR", "GBP", "CAD", "BRL", "JPY", "CNY", "MXN", "KRW", "TRY", "ARS",
"PEN", "VES", "CLP", "CRC", "CDF", "COP", "HKD", "INR", "NGN", "PHP",
"RUB", "ZAR", "XAU"
];
fn get_oracle_for_asset(asset: &str) -> &'static str {
let asset_upper = asset.to_uppercase();
if CRYPTO_ASSETS.contains(&asset_upper.as_str()) {
REFLECTOR_CRYPTO_CONTRACT
} else if FIAT_ASSETS.contains(&asset_upper.as_str()) {
REFLECTOR_FIAT_CONTRACT
} else {
REFLECTOR_STELLAR_CONTRACT
}
}
#[allow(dead_code)]
#[derive(serde::Deserialize)]
struct RpcLedgerResponse {
ledgers: Vec<RpcLedger>,
}
#[allow(dead_code)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcLedger {
sequence: u32,
metadata_xdr: String,
}
pub fn fetch_from_rpc(ledger_seq: u32, _silent: bool) -> Result<Vec<u8>> {
let client = reqwest::blocking::Client::new();
let rpc_request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "getLedgers",
"params": {
"startLedger": ledger_seq,
"pagination": {
"limit": 1
}
}
});
let response = client
.post(Config::rpc_url())
.json(&rpc_request)
.send()
.context("Failed to call RPC")?;
if !response.status().is_success() {
anyhow::bail!("RPC returned error status: {}", response.status());
}
let json: serde_json::Value = response.json()
.context("Failed to parse RPC response")?;
if let Some(error) = json.get("error") {
anyhow::bail!("RPC error: {}", error);
}
let result = json.get("result")
.ok_or_else(|| anyhow::anyhow!("No result in RPC response"))?;
let ledgers = result.get("ledgers")
.and_then(|l| l.as_array())
.ok_or_else(|| anyhow::anyhow!("No ledgers in RPC response"))?;
if ledgers.is_empty() {
anyhow::bail!("Ledger {} not found in RPC", ledger_seq);
}
let ledger = &ledgers[0];
let metadata_xdr = ledger.get("metadataXdr")
.and_then(|m| m.as_str())
.ok_or_else(|| anyhow::anyhow!("No metadataXdr in RPC response"))?;
let ledger_close_meta = LedgerCloseMeta::from_xdr_base64(metadata_xdr, Limits::none())
.context("Failed to decode metadataXdr from RPC")?;
let batch = LedgerCloseMetaBatch {
start_sequence: ledger_seq,
end_sequence: ledger_seq,
ledger_close_metas: vec![ledger_close_meta].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create VecM"))?,
};
let xdr_bytes = batch.to_xdr(Limits::none())
.context("Failed to serialize batch to XDR")?;
Ok(xdr_bytes)
}
pub fn query_balance(address: &str, token_contract: &str) -> Result<serde_json::Value> {
use stellar_xdr::curr::*;
let client = reqwest::blocking::Client::new();
let address_bytes = match Strkey::from_string(address) {
Ok(Strkey::PublicKeyEd25519(pk)) => pk.0,
_ => anyhow::bail!("Invalid Stellar address format"),
};
let contract_bytes = match Strkey::from_string(token_contract) {
Ok(Strkey::Contract(contract)) => contract.0,
_ => anyhow::bail!("Invalid contract address format"),
};
let address_scval = ScVal::Address(ScAddress::Account(
AccountId(PublicKey::PublicKeyTypeEd25519(
Uint256(address_bytes),
)),
));
let function_name = ScSymbol("balance".as_bytes().to_vec().try_into()
.map_err(|_| anyhow::anyhow!("Function name too long"))?);
use stellar_xdr::curr::ContractId;
let contract_address = ScAddress::Contract(
ContractId(Hash(contract_bytes)),
);
let invoke_args = InvokeContractArgs {
contract_address,
function_name,
args: vec![address_scval].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create args vec"))?,
};
let host_function = HostFunction::InvokeContract(invoke_args);
let invoke_op = InvokeHostFunctionOp {
host_function,
auth: vec![].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create auth vec"))?,
};
let operation = Operation {
source_account: None,
body: OperationBody::InvokeHostFunction(invoke_op),
};
let source_account = MuxedAccount::Ed25519(Uint256([0u8; 32]));
let tx = Transaction {
source_account,
fee: 100,
seq_num: SequenceNumber(0),
cond: Preconditions::None,
memo: Memo::None,
operations: vec![operation].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create operations vec"))?,
ext: TransactionExt::V0,
};
let tx_envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
tx,
signatures: vec![].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create signatures vec"))?,
});
let tx_xdr = tx_envelope.to_xdr_base64(Limits::none())
.context("Failed to encode transaction to XDR")?;
let rpc_request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "simulateTransaction",
"params": {
"transaction": tx_xdr
}
});
let response = client
.post(Config::soroban_rpc_url())
.json(&rpc_request)
.send()
.context("Failed to call RPC for balance query")?;
if !response.status().is_success() {
anyhow::bail!("RPC returned error status: {}", response.status());
}
let json: serde_json::Value = response.json()
.context("Failed to parse RPC response")?;
if let Some(error) = json.get("error") {
anyhow::bail!("RPC error: {}", error);
}
let result = json.get("result")
.ok_or_else(|| anyhow::anyhow!("No result in RPC response"))?;
if let Some(result_xdr) = result.get("results")
.and_then(|r| r.get(0))
.and_then(|r| r.get("xdr"))
.and_then(|x| x.as_str())
{
match ScVal::from_xdr_base64(result_xdr, Limits::none()) {
Ok(ScVal::I128(parts)) => {
let raw_balance = i128::from(parts.hi) << 64 | i128::from(parts.lo as u64);
let balance = raw_balance as f64 / 10_f64.powi(7);
return Ok(serde_json::json!({
"address": address,
"token": token_contract,
"balance": balance,
"raw_balance": raw_balance.to_string()
}));
}
Ok(ScVal::U128(parts)) => {
let raw_balance = u128::from(parts.hi) << 64 | u128::from(parts.lo);
let balance = raw_balance as f64 / 10_f64.powi(7);
return Ok(serde_json::json!({
"address": address,
"token": token_contract,
"balance": balance,
"raw_balance": raw_balance.to_string()
}));
}
Ok(val) => {
return Ok(serde_json::json!({
"address": address,
"token": token_contract,
"result": format!("{:?}", val)
}));
}
Err(e) => {
return Ok(serde_json::json!({
"address": address,
"token": token_contract,
"error": format!("Failed to decode result: {}", e),
"raw_xdr": result_xdr
}));
}
}
}
Ok(serde_json::json!({
"address": address,
"token": token_contract,
"result": result
}))
}
pub fn query_price(asset_input: &str) -> Result<serde_json::Value> {
use stellar_xdr::curr::*;
let client = reqwest::blocking::Client::new();
let (asset_type, asset_value, reflector_contract) = if asset_input.starts_with('C') && asset_input.len() == 56 {
("Stellar", asset_input, REFLECTOR_STELLAR_CONTRACT)
} else {
let oracle = get_oracle_for_asset(asset_input);
("Other", asset_input, oracle)
};
let asset_scval = if asset_type == "Stellar" {
let contract_bytes = match Strkey::from_string(asset_value) {
Ok(Strkey::Contract(contract)) => contract.0,
_ => anyhow::bail!("Invalid contract address format: {}", asset_value),
};
ScVal::Vec(Some(
vec![
ScVal::Symbol(ScSymbol("Stellar".as_bytes().to_vec().try_into()
.map_err(|_| anyhow::anyhow!("Variant name too long"))?)),
ScVal::Address(ScAddress::Contract(
ContractId(Hash(contract_bytes)),
)),
]
.try_into()
.map_err(|_| anyhow::anyhow!("Failed to create vec"))?,
))
} else {
ScVal::Vec(Some(
vec![
ScVal::Symbol(ScSymbol("Other".as_bytes().to_vec().try_into()
.map_err(|_| anyhow::anyhow!("Variant name too long"))?)),
ScVal::Symbol(ScSymbol(asset_value.to_uppercase().as_bytes().to_vec().try_into()
.map_err(|_| anyhow::anyhow!("Asset symbol too long"))?)),
]
.try_into()
.map_err(|_| anyhow::anyhow!("Failed to create vec"))?,
))
};
let reflector_contract_bytes = match Strkey::from_string(reflector_contract) {
Ok(Strkey::Contract(contract)) => contract.0,
_ => anyhow::bail!("Invalid Reflector contract address"),
};
let function_name = ScSymbol("lastprice".as_bytes().to_vec().try_into()
.map_err(|_| anyhow::anyhow!("Function name too long"))?);
let contract_address = ScAddress::Contract(
ContractId(Hash(reflector_contract_bytes)),
);
let invoke_args = InvokeContractArgs {
contract_address,
function_name,
args: vec![asset_scval].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create args vec"))?,
};
let host_function = HostFunction::InvokeContract(invoke_args);
let invoke_op = InvokeHostFunctionOp {
host_function,
auth: vec![].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create auth vec"))?,
};
let operation = Operation {
source_account: None,
body: OperationBody::InvokeHostFunction(invoke_op),
};
let source_account = MuxedAccount::Ed25519(Uint256([0u8; 32]));
let tx = Transaction {
source_account,
fee: 100,
seq_num: SequenceNumber(0),
cond: Preconditions::None,
memo: Memo::None,
operations: vec![operation].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create operations vec"))?,
ext: TransactionExt::V0,
};
let tx_envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
tx,
signatures: vec![].try_into()
.map_err(|_| anyhow::anyhow!("Failed to create signatures vec"))?,
});
let tx_xdr = tx_envelope.to_xdr_base64(Limits::none())
.context("Failed to encode transaction to XDR")?;
let rpc_request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "simulateTransaction",
"params": {
"transaction": tx_xdr
}
});
let response = client
.post(Config::soroban_rpc_url())
.json(&rpc_request)
.send()
.context("Failed to call RPC for price query")?;
if !response.status().is_success() {
anyhow::bail!("RPC returned error status: {}", response.status());
}
let json: serde_json::Value = response.json()
.context("Failed to parse RPC response")?;
if let Some(error) = json.get("error") {
anyhow::bail!("RPC error: {}", error);
}
let result = json.get("result")
.ok_or_else(|| anyhow::anyhow!("No result in RPC response"))?;
if let Some(error) = result.get("error") {
return Ok(serde_json::json!({
"asset": asset_input,
"asset_type": asset_type,
"error": "Contract execution failed",
"contract_error": error,
"result": result
}));
}
if let Some(result_xdr) = result.get("results")
.and_then(|r| r.get(0))
.and_then(|r| r.get("xdr"))
.and_then(|x| x.as_str())
{
match ScVal::from_xdr_base64(result_xdr, Limits::none()) {
Ok(ScVal::Map(Some(map))) => {
let mut price_i128: Option<i128> = None;
let mut timestamp_u64: Option<u64> = None;
for entry in map.as_vec() {
if let ScVal::Symbol(ref key_sym) = entry.key {
let key_str = String::from_utf8_lossy(key_sym.as_vec());
match key_str.as_ref() {
"price" => {
if let ScVal::I128(parts) = &entry.val {
price_i128 = Some(i128::from(parts.hi) << 64 | i128::from(parts.lo as u64));
}
}
"timestamp" => {
if let ScVal::U64(ts) = &entry.val {
timestamp_u64 = Some(*ts);
}
}
_ => {}
}
}
}
if let (Some(price), Some(timestamp)) = (price_i128, timestamp_u64) {
let price_float = price as f64 / 10_f64.powi(REFLECTOR_DECIMALS as i32);
return Ok(serde_json::json!({
"asset": asset_input,
"asset_type": asset_type,
"price": price_float,
"price_raw": price.to_string(),
"timestamp": timestamp,
"decimals": REFLECTOR_DECIMALS,
"source": "reflector"
}));
}
}
Ok(val) => {
return Ok(serde_json::json!({
"asset": asset_input,
"asset_type": asset_type,
"error": "Unexpected result format",
"result": format!("{:?}", val)
}));
}
Err(e) => {
return Ok(serde_json::json!({
"asset": asset_input,
"asset_type": asset_type,
"error": format!("Failed to decode result: {}", e),
"raw_xdr": result_xdr
}));
}
}
}
Ok(serde_json::json!({
"asset": asset_input,
"asset_type": asset_type,
"error": "No result XDR found in response",
"result": result
}))
}
pub fn query_ttl(contract_address: &str) -> Result<serde_json::Value> {
use stellar_xdr::curr::*;
let client = reqwest::blocking::Client::new();
let contract_bytes = match Strkey::from_string(contract_address) {
Ok(Strkey::Contract(contract)) => contract.0,
_ => anyhow::bail!("Invalid contract address format"),
};
let contract_sc_address = ScAddress::Contract(ContractId(Hash(contract_bytes)));
fn parse_u32(value: &serde_json::Value) -> Option<u32> {
if let Some(num) = value.as_u64() {
return u32::try_from(num).ok();
}
if let Some(text) = value.as_str() {
return text.parse::<u32>().ok();
}
None
}
fn fetch_entry_with_ttl(
client: &reqwest::blocking::Client,
ledger_key: LedgerKey,
) -> Result<(serde_json::Value, Option<u32>, Option<Hash>, Option<String>)> {
let key_xdr = ledger_key.to_xdr_base64(Limits::none())
.context("Failed to encode ledger key to XDR")?;
let rpc_request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "getLedgerEntries",
"params": {
"keys": [key_xdr]
}
});
let response = client
.post(Config::soroban_rpc_url())
.json(&rpc_request)
.send()
.context("Failed to call RPC for ttl query")?;
if !response.status().is_success() {
anyhow::bail!("RPC returned error status: {}", response.status());
}
let json: serde_json::Value = response.json()
.context("Failed to parse RPC response")?;
if let Some(error) = json.get("error") {
anyhow::bail!("RPC error: {}", error);
}
let result = json.get("result")
.ok_or_else(|| anyhow::anyhow!("No result in RPC response"))?;
let latest_ledger = result.get("latestLedger")
.and_then(parse_u32)
.or_else(|| result.get("latestLedgerSeq").and_then(parse_u32));
let mut last_modified: Option<u32> = None;
let mut live_until: Option<u32> = None;
let mut found = false;
let mut wasm_hash: Option<Hash> = None;
let mut executable_type: Option<String> = None;
if let Some(entries) = result.get("entries").and_then(|e| e.as_array()) {
for entry in entries {
if let Some(last) = entry.get("lastModifiedLedgerSeq").and_then(parse_u32) {
last_modified = Some(last);
found = true;
}
if let Some(live) = entry.get("liveUntilLedgerSeq").and_then(parse_u32) {
live_until = Some(live);
}
if let Some(xdr) = entry.get("xdr").and_then(|x| x.as_str()) {
if let Ok(entry_data) = LedgerEntryData::from_xdr_base64(xdr, Limits::none()) {
match entry_data {
LedgerEntryData::ContractData(data_entry) => {
found = true;
if let ScVal::ContractInstance(instance) = data_entry.val {
match instance.executable {
ContractExecutable::Wasm(hash) => {
wasm_hash = Some(hash);
executable_type = Some("Wasm".to_string());
}
ContractExecutable::StellarAsset => {
executable_type = Some("StellarAsset".to_string());
}
}
}
}
LedgerEntryData::ContractCode(_) => {
found = true;
}
_ => {}
}
} else if let Ok(ledger_entry) =
LedgerEntry::from_xdr_base64(xdr, Limits::none())
{
last_modified = Some(ledger_entry.last_modified_ledger_seq);
match ledger_entry.data {
LedgerEntryData::ContractData(data_entry) => {
found = true;
if let ScVal::ContractInstance(instance) = data_entry.val {
match instance.executable {
ContractExecutable::Wasm(hash) => {
wasm_hash = Some(hash);
executable_type = Some("Wasm".to_string());
}
ContractExecutable::StellarAsset => {
executable_type = Some("StellarAsset".to_string());
}
}
}
}
LedgerEntryData::ContractCode(_) => {
found = true;
}
_ => {}
}
}
}
}
}
let ttl_remaining = match (live_until, latest_ledger) {
(Some(live), Some(current)) => Some(live.saturating_sub(current)),
_ => None,
};
let ttl_remaining_seconds = ttl_remaining.map(|remaining| remaining as u64 * 5);
let ttl_remaining_days = ttl_remaining_seconds.map(|seconds| {
let days = seconds as f64 / 86_400.0;
(days * 100.0).round() / 100.0
});
Ok((
serde_json::json!({
"exists": found,
"last_modified_ledger": last_modified,
"live_until_ledger": live_until,
"ttl_remaining_ledgers": ttl_remaining,
"ttl_remaining_days": ttl_remaining_days,
"ttl_estimate_seconds_per_ledger": 5,
"contract_executable": executable_type.as_ref(),
"contract_executable_wasm_hash": wasm_hash.as_ref().map(|h| h.to_string())
}),
latest_ledger,
wasm_hash,
executable_type,
))
}
let instance_key = LedgerKey::ContractData(LedgerKeyContractData {
contract: contract_sc_address.clone(),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
});
let (instance_info, instance_latest, wasm_hash, executable_type) =
fetch_entry_with_ttl(&client, instance_key)?;
let (code_info, code_latest) = if let Some(hash) = wasm_hash {
let code_hash = hash.clone();
let code_key = LedgerKey::ContractCode(LedgerKeyContractCode { hash });
let (info, latest, _, _) = fetch_entry_with_ttl(&client, code_key)?;
(
serde_json::json!({
"code_hash": code_hash.to_string(),
"ttl": info
}),
latest,
)
} else if executable_type.as_deref() == Some("StellarAsset") {
(
serde_json::json!({
"exists": false,
"error": "Contract executable is StellarAsset; no WASM code entry"
}),
None,
)
} else {
(
serde_json::json!({
"exists": false,
"error": "Contract instance did not provide a WASM code hash"
}),
None,
)
};
let latest_ledger = instance_latest.or(code_latest);
Ok(serde_json::json!({
"contract": contract_address,
"latest_ledger": latest_ledger,
"contract_instance": instance_info,
"contract_code": code_info
}))
}