pub mod config_pb {
include!("../../../proto/generated/xyz.aspens.arborter_config.v1.rs");
}
use config_pb::{Chain, GetConfigRequest, GetConfigResponse, Market, Token};
use eyre::{bail, Result};
use std::fs;
use std::path::Path;
use tracing::info;
use crate::grpc::create_channel;
pub async fn get_config(url: String) -> Result<GetConfigResponse> {
use config_pb::config_service_client::ConfigServiceClient;
let channel = create_channel(&url).await?;
let mut client = ConfigServiceClient::new(channel);
let request = tonic::Request::new(GetConfigRequest {});
let response = client.get_config(request).await?;
Ok(response.into_inner())
}
pub async fn download_config(url: String, path: String) -> Result<()> {
let config = get_config(url).await?;
let contents = match Path::new(&path).extension().and_then(|ext| ext.to_str()) {
Some("json") => serde_json::to_string_pretty(&config)?,
Some("toml") => toml::to_string_pretty(&config)?,
Some(ext) => bail!("Unsupported file extension: {}. Use .json or .toml", ext),
None => bail!("No file extension found. Use .json or .toml"),
};
fs::write(&path, contents)?;
info!("Configuration saved to: {}", path);
Ok(())
}
impl GetConfigResponse {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let contents = fs::read_to_string(path)?;
let config = match path.extension().and_then(|ext| ext.to_str()) {
Some("json") => serde_json::from_str(&contents)?,
Some("toml") => toml::from_str(&contents)?,
Some(ext) => bail!("Unsupported file extension: {}", ext),
None => bail!("No file extension found"),
};
Ok(config)
}
pub fn get_chain(&self, network: &str) -> Option<&Chain> {
self.config
.as_ref()?
.chains
.iter()
.find(|chain| chain.network == network)
}
pub fn get_token(&self, network: &str, symbol: &str) -> Option<&Token> {
self.get_chain(network)
.and_then(|chain| chain.tokens.get(symbol))
}
pub fn get_market(&self, name: &str) -> Option<&Market> {
self.config
.as_ref()?
.markets
.iter()
.find(|market| market.name == name)
}
pub fn get_market_by_tokens(
&self,
base_network: &str,
base_symbol: &str,
quote_network: &str,
quote_symbol: &str,
) -> Option<&Market> {
self.config.as_ref()?.markets.iter().find(|market| {
market.base_chain_network == base_network
&& market.base_chain_token_symbol == base_symbol
&& market.quote_chain_network == quote_network
&& market.quote_chain_token_symbol == quote_symbol
})
}
pub fn get_chain_by_id(&self, chain_id: u32) -> Option<&Chain> {
self.config
.as_ref()?
.chains
.iter()
.find(|chain| chain.chain_id == chain_id)
}
pub fn get_market_by_id(&self, market_id: &str) -> Option<&Market> {
self.config
.as_ref()?
.markets
.iter()
.find(|market| market.market_id == market_id)
}
}
pub async fn download_config_to_file<P: AsRef<Path>>(url: String, path: P) -> Result<()> {
info!("Downloading configuration to {}", path.as_ref().display());
let config = get_config(url).await?;
if let Some(parent) = path.as_ref().parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&config)?;
fs::write(path, json)?;
info!("Configuration downloaded successfully");
Ok(())
}
pub use config_pb::{ChainPublicKey, GetSignerPublicKeyResponse};
pub async fn get_signer_public_key(
url: String,
chain_network: Option<String>,
) -> Result<GetSignerPublicKeyResponse> {
use config_pb::config_service_client::ConfigServiceClient;
use config_pb::GetSignerPublicKeyRequest;
let channel = create_channel(&url).await?;
let mut client = ConfigServiceClient::new(channel);
let request = tonic::Request::new(GetSignerPublicKeyRequest { chain_network });
let response = client.get_signer_public_key(request).await?;
Ok(response.into_inner())
}
#[derive(Debug, Clone)]
pub struct SignerInfo {
pub chain_id: u32,
pub chain_network: String,
pub public_key: String,
pub gas_balance: Option<u128>,
}
impl SignerInfo {
pub fn formatted_gas_balance(&self) -> String {
match self.gas_balance {
Some(balance) => {
let balance_f64 = balance as f64 / 1e18;
format!("{:.6}", balance_f64)
}
None => "error".to_string(),
}
}
}
async fn get_native_balance(rpc_url: &str, address: &str) -> Result<u128> {
use alloy::primitives::Address;
use alloy::providers::{Provider, ProviderBuilder};
use url::Url;
let rpc_url = Url::parse(rpc_url)?;
let provider = ProviderBuilder::new().connect_http(rpc_url);
let address: Address = address.parse()?;
let balance = provider.get_balance(address).await?;
Ok(balance.to::<u128>())
}
pub async fn get_signer_public_key_with_balances(
url: String,
chain_network: Option<String>,
) -> Result<Vec<SignerInfo>> {
let signer_response = get_signer_public_key(url.clone(), chain_network).await?;
let config_response = get_config(url).await?;
let config = config_response
.config
.ok_or_else(|| eyre::eyre!("No configuration found"))?;
let chain_rpc_map: std::collections::HashMap<String, String> = config
.chains
.iter()
.map(|chain| (chain.network.clone(), chain.rpc_url.clone()))
.collect();
let mut signer_infos = Vec::new();
for (chain_network_key, key_info) in signer_response.chain_keys {
let gas_balance = if let Some(rpc_url) = chain_rpc_map.get(&chain_network_key) {
match get_native_balance(rpc_url, &key_info.public_key).await {
Ok(balance) => Some(balance),
Err(e) => {
tracing::warn!(
"Failed to get gas balance for chain {}: {}",
chain_network_key,
e
);
None
}
}
} else {
tracing::warn!("No RPC URL found for chain {}", chain_network_key);
None
};
signer_infos.push(SignerInfo {
chain_id: key_info.chain_id,
chain_network: chain_network_key,
public_key: key_info.public_key,
gas_balance,
});
}
signer_infos.sort_by_key(|info| info.chain_network.clone());
Ok(signer_infos)
}
pub use crate::attestation::v1::{
AttestationReport, GetAttestationRequest, GetAttestationResponse,
};
pub async fn get_attestation(
url: String,
report_data: Option<Vec<u8>>,
) -> Result<GetAttestationResponse> {
use config_pb::config_service_client::ConfigServiceClient;
let channel = create_channel(&url).await?;
let mut client = ConfigServiceClient::new(channel);
let request = tonic::Request::new(GetAttestationRequest { report_data });
let response = client.get_attestation(request).await?;
Ok(response.into_inner())
}
pub fn format_attestation_report(report: &AttestationReport) -> String {
let mut output = String::new();
output.push_str("TEE Attestation Report:\n");
output.push_str(&format!(" TEE TCB SVN: {}\n", report.tee_tcb_svn));
output.push_str(&format!(" MR SEAM: {}\n", report.mr_seam));
output.push_str(&format!(" MR Signer SEAM: {}\n", report.mr_signer_seam));
output.push_str(&format!(" SEAM Attributes: {}\n", report.seam_attributes));
output.push_str(&format!(" TD Attributes: {}\n", report.td_attributes));
output.push_str(&format!(" XFAM: {}\n", report.xfam));
output.push_str(&format!(" MR TD: {}\n", report.mr_td));
output.push_str(&format!(" MR Config ID: {}\n", report.mr_config_id));
output.push_str(&format!(" MR Owner: {}\n", report.mr_owner));
output.push_str(&format!(" MR Owner Config: {}\n", report.mr_owner_config));
output.push_str(&format!(" RTMR[0]: {}\n", report.rt_mr0));
output.push_str(&format!(" RTMR[1]: {}\n", report.rt_mr1));
output.push_str(&format!(" RTMR[2]: {}\n", report.rt_mr2));
output.push_str(&format!(" RTMR[3]: {}\n", report.rt_mr3));
output.push_str(&format!(" Report Data: {}\n", report.report_data));
output
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
#[ignore = "requires example config files"]
fn test_json_config_parsing() {
let config = GetConfigResponse::from_file("../example/config.json").unwrap();
verify_config(&config);
}
#[test]
#[ignore = "requires example config files"]
fn test_toml_config_parsing() {
let config = GetConfigResponse::from_file("../example/config.toml").unwrap();
verify_config(&config);
}
#[tokio::test]
#[ignore = "requires example config files and running server"]
async fn test_download_config_to_file() -> Result<()> {
let config = GetConfigResponse::from_file("../example/config.toml").unwrap();
let anvil1 = config.get_chain("anvil-1").unwrap();
let temp_dir = tempdir()?;
let config_path = temp_dir.path().join("config.json");
download_config_to_file(anvil1.rpc_url.clone(), &config_path).await?;
let contents = fs::read_to_string(&config_path)?;
let _: GetConfigResponse = serde_json::from_str(&contents)?;
Ok(())
}
fn verify_config(config: &GetConfigResponse) {
let anvil1 = config.get_chain("anvil-1").unwrap();
assert_eq!(anvil1.chain_id, 84531);
assert_eq!(anvil1.rpc_url, "http://localhost:8545");
let usdc = config.get_token("anvil-1", "USDC").unwrap();
assert_eq!(usdc.symbol, "USDC");
assert_eq!(usdc.name, "USD Coin");
assert_eq!(usdc.decimals, 6);
let market = config.get_market("A1USDC-A2USDT").unwrap();
assert_eq!(market.base_chain_network, "anvil-1");
assert_eq!(market.base_chain_token_symbol, "USDC");
assert_eq!(market.quote_chain_network, "anvil-2");
assert_eq!(market.quote_chain_token_symbol, "USDT");
let market = config.get_market_by_tokens("anvil-1", "USDC", "anvil-2", "USDT");
assert!(market.is_some());
assert_eq!(market.unwrap().name, "Anvil-1 USDC - Anvil-2 USDT");
}
}