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>> {
if !silent {
println!("Ledger not in S3, fetching from RPC archive...");
}
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"))?;
if !silent {
println!("Decoding base64 XDR from RPC...");
}
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")?;
if !silent {
println!("Fetched {} bytes from RPC", xdr_bytes.len());
}
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
}))
}