use crate::runtime::values::Value;
use std::collections::HashMap;
use std::env;
#[cfg(feature = "http-interface")]
mod rpc {
use lazy_static::lazy_static;
use serde_json::{json, Value as JsonValue};
use std::sync::{Arc, Mutex};
lazy_static! {
static ref CLIENT: Arc<Mutex<Option<Arc<reqwest::blocking::Client>>>> =
Arc::new(Mutex::new(None));
}
fn get_client() -> Result<Arc<reqwest::blocking::Client>, String> {
let mut client_guard = CLIENT
.lock()
.map_err(|e| format!("Mutex poisoned: {}", e))?;
if let Some(ref client) = *client_guard {
return Ok(Arc::clone(client));
}
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| e.to_string())?;
let client_arc = Arc::new(client);
*client_guard = Some(Arc::clone(&client_arc));
Ok(client_arc)
}
pub(super) fn rpc_request(
rpc_url: &str,
method: &str,
params: Vec<JsonValue>,
) -> Result<JsonValue, String> {
let body = json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1
});
let client = get_client()?;
let resp = client
.post(rpc_url)
.json(&body)
.send()
.map_err(|e| e.to_string())?;
let status = resp.status();
let json: JsonValue = resp.json().map_err(|e| e.to_string())?;
if let Some(err) = json.get("error") {
let msg = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("RPC error");
return Err(msg.to_string());
}
if !status.is_success() {
return Err(format!("RPC HTTP {}", status));
}
json.get("result")
.cloned()
.ok_or_else(|| "Missing result".to_string())
}
pub(super) fn hex_to_i64(hex_str: &str) -> i64 {
let s = hex_str.strip_prefix("0x").unwrap_or(hex_str);
let s = s.trim_start_matches('0');
if s.is_empty() {
return 0;
}
let mut value: u128 = 0;
for c in s.chars() {
let d = c.to_digit(16).unwrap_or(0) as u128;
value = value.saturating_mul(16).saturating_add(d);
}
value.min(i64::MAX as u128) as i64
}
pub(super) fn hex_gas_price_to_gwei(hex_str: &str) -> f64 {
let raw = hex_to_i64(hex_str);
if raw <= 0 {
return 0.0;
}
raw as f64 / 1_000_000_000.0
}
}
#[derive(Debug, Clone)]
pub struct ChainConfig {
pub chain_id: i64,
pub name: String,
pub rpc_url: String,
pub explorer: String,
pub gas_limit: i64,
pub gas_price: f64,
pub confirmations: i64,
pub is_testnet: bool,
}
lazy_static::lazy_static! {
static ref CHAIN_REGISTRY: HashMap<i64, ChainConfig> = {
let mut m = HashMap::new();
m.insert(1, ChainConfig {
chain_id: 1,
name: "Ethereum Mainnet".to_string(),
rpc_url: "https://mainnet.infura.io/v3/YOUR_PROJECT_ID".to_string(),
explorer: "https://etherscan.io".to_string(),
gas_limit: 21000,
gas_price: 20.0,
confirmations: 12,
is_testnet: false,
});
m.insert(137, ChainConfig {
chain_id: 137,
name: "Polygon".to_string(),
rpc_url: "https://polygon-rpc.com".to_string(),
explorer: "https://polygonscan.com".to_string(),
gas_limit: 21000,
gas_price: 30.0,
confirmations: 256,
is_testnet: false,
});
m.insert(56, ChainConfig {
chain_id: 56,
name: "Binance Smart Chain".to_string(),
rpc_url: "https://bsc-dataseed.binance.org".to_string(),
explorer: "https://bscscan.com".to_string(),
gas_limit: 21000,
gas_price: 5.0,
confirmations: 15,
is_testnet: false,
});
m.insert(42161, ChainConfig {
chain_id: 42161,
name: "Arbitrum One".to_string(),
rpc_url: "https://arb1.arbitrum.io/rpc".to_string(),
explorer: "https://arbiscan.io".to_string(),
gas_limit: 21000,
gas_price: 0.1,
confirmations: 1,
is_testnet: false,
});
m.insert(43114, ChainConfig {
chain_id: 43114,
name: "Avalanche C-Chain".to_string(),
rpc_url: "https://api.avax.network/ext/bc/C/rpc".to_string(),
explorer: "https://snowtrace.io".to_string(),
gas_limit: 21000,
gas_price: 25.0,
confirmations: 1,
is_testnet: false,
});
m.insert(5, ChainConfig {
chain_id: 5,
name: "Ethereum Goerli".to_string(),
rpc_url: "https://goerli.infura.io/v3/YOUR_PROJECT_ID".to_string(),
explorer: "https://goerli.etherscan.io".to_string(),
gas_limit: 21000,
gas_price: 2.0,
confirmations: 6,
is_testnet: true,
});
m.insert(80001, ChainConfig {
chain_id: 80001,
name: "Polygon Mumbai".to_string(),
rpc_url: "https://rpc-mumbai.maticvigil.com".to_string(),
explorer: "https://mumbai.polygonscan.com".to_string(),
gas_limit: 21000,
gas_price: 1.0,
confirmations: 6,
is_testnet: true,
});
m
};
}
pub fn get_chain_config(chain_id: i64) -> Option<ChainConfig> {
CHAIN_REGISTRY.get(&chain_id).cloned()
}
pub fn get_supported_chains() -> Vec<ChainConfig> {
CHAIN_REGISTRY.values().cloned().collect()
}
pub fn deploy(
chain_id: i64,
contract_name: String,
constructor_args: HashMap<String, String>,
) -> String {
crate::stdlib::log::audit(
"deploy",
{
let mut data = std::collections::HashMap::new();
data.insert("chain_id".to_string(), Value::Int(chain_id));
data.insert(
"contract_name".to_string(),
Value::String(contract_name.clone()),
);
data.insert(
"constructor_args".to_string(),
Value::String(format!("{:?}", constructor_args)),
);
data
},
Some("chain"),
);
let chain_config = match get_chain_config(chain_id) {
Some(c) => c,
None => {
crate::stdlib::log::error(
"deploy",
{
let mut data = std::collections::HashMap::new();
data.insert("chain_id".to_string(), Value::Int(chain_id));
data.insert(
"error".to_string(),
Value::String(format!("Chain {} not supported", chain_id)),
);
data
},
Some("chain"),
);
return String::new();
}
};
#[cfg(feature = "http-interface")]
if let Some(raw_tx_hex) = constructor_args
.get("raw_transaction")
.or_else(|| constructor_args.get("signed_tx"))
{
if let Ok(addr) = deploy_via_raw_transaction(&chain_config.rpc_url, raw_tx_hex) {
crate::stdlib::log::info(
"deploy_success",
{
let mut data = std::collections::HashMap::new();
data.insert("chain_id".to_string(), Value::Int(chain_id));
data.insert("contract_name".to_string(), Value::String(contract_name));
data.insert("address".to_string(), Value::String(addr.clone()));
data
},
None,
);
return addr;
}
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let address = format!("0x{:040x}", timestamp);
crate::stdlib::log::info(
"deploy_success",
{
let mut data = std::collections::HashMap::new();
data.insert("chain_id".to_string(), Value::Int(chain_id));
data.insert("contract_name".to_string(), Value::String(contract_name));
data.insert("address".to_string(), Value::String(address.clone()));
data
},
None,
);
address
}
#[cfg(feature = "http-interface")]
fn deploy_via_raw_transaction(rpc_url: &str, raw_tx_hex: &str) -> Result<String, String> {
use serde_json::json;
let tx_hex = raw_tx_hex.strip_prefix("0x").unwrap_or(raw_tx_hex);
let result = rpc::rpc_request(
rpc_url,
"eth_sendRawTransaction",
vec![json!(format!("0x{}", tx_hex))],
)?;
let tx_hash = result.as_str().ok_or("expected tx hash string")?;
let receipt = wait_for_receipt(rpc_url, tx_hash, 30)?;
let addr = receipt
.get("contractAddress")
.and_then(|v| v.as_str())
.ok_or("no contractAddress in receipt")?
.to_string();
Ok(addr)
}
#[cfg(feature = "http-interface")]
fn wait_for_receipt(
rpc_url: &str,
tx_hash: &str,
max_attempts: u32,
) -> Result<serde_json::Value, String> {
use serde_json::json;
for _ in 0..max_attempts {
let result = rpc::rpc_request(rpc_url, "eth_getTransactionReceipt", vec![json!(tx_hash)])?;
if !result.is_null() {
return Ok(result);
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
Err("timeout waiting for transaction receipt".to_string())
}
pub fn estimate_gas(chain_id: i64, operation: String) -> i64 {
let chain_config = match get_chain_config(chain_id) {
Some(c) => c,
None => return 0,
};
#[cfg(feature = "http-interface")]
{
use serde_json::json;
let params = vec![json!({ "to": null, "data": "0x" })];
if let Ok(result) = rpc::rpc_request(&chain_config.rpc_url, "eth_estimateGas", params) {
if let Some(hex_str) = result.as_str() {
let gas = rpc::hex_to_i64(hex_str);
if gas > 0 {
return gas;
}
}
}
}
let base_gas = match operation.as_str() {
"transfer" => 21000,
"mint" => 50000,
"burn" => 30000,
"approve" => 46000,
"deploy" => 200000,
_ => 21000,
};
let adjusted_gas = if chain_config.is_testnet {
base_gas / 2
} else {
base_gas
};
adjusted_gas
}
pub fn get_gas_price(chain_id: i64) -> f64 {
let chain_config = match get_chain_config(chain_id) {
Some(c) => c,
None => return 0.0,
};
#[cfg(feature = "http-interface")]
if let Ok(result) = rpc::rpc_request(&chain_config.rpc_url, "eth_gasPrice", vec![]) {
if let Some(hex_str) = result.as_str() {
let gwei = rpc::hex_gas_price_to_gwei(hex_str);
if gwei > 0.0 {
return gwei;
}
}
}
let variation = (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
% 100) as f64
/ 100.0;
chain_config.gas_price + variation
}
pub fn get_block_timestamp(chain_id: i64) -> i64 {
#[cfg(feature = "http-interface")]
if let Some(config) = get_chain_config(chain_id) {
use serde_json::json;
if let Ok(result) = rpc::rpc_request(
&config.rpc_url,
"eth_getBlockByNumber",
vec![json!("latest"), json!(false)],
) {
if let Some(hex_ts) = result.get("timestamp").and_then(|v| v.as_str()) {
let ts = rpc::hex_to_i64(hex_ts);
if ts > 0 {
return ts;
}
}
}
}
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
pub fn get_block_hash(chain_id: i64) -> String {
#[cfg(feature = "http-interface")]
if let Some(config) = get_chain_config(chain_id) {
use serde_json::json;
if let Ok(result) = rpc::rpc_request(
&config.rpc_url,
"eth_getBlockByNumber",
vec![json!("latest"), json!(false)],
) {
if let Some(hash) = result.get("hash").and_then(|v| v.as_str()) {
return hash.to_string();
}
}
}
format!(
"0x{:064x}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
)
}
pub fn get_transaction_status(chain_id: i64, tx_hash: String) -> String {
let chain_config = match get_chain_config(chain_id) {
Some(c) => c,
None => {
return if tx_hash.starts_with("0x") && tx_hash.len() > 10 {
"confirmed".to_string()
} else {
"pending".to_string()
};
}
};
#[cfg(feature = "http-interface")]
{
use serde_json::json;
let hash = if tx_hash.starts_with("0x") {
tx_hash.clone()
} else {
format!("0x{}", tx_hash)
};
if let Ok(result) = rpc::rpc_request(
&chain_config.rpc_url,
"eth_getTransactionReceipt",
vec![json!(hash)],
) {
if result.is_null() {
return "pending".to_string();
}
if let Some(status) = result.get("status").and_then(|v| v.as_str()) {
return match status {
"0x1" => "confirmed".to_string(),
"0x0" => "failed".to_string(),
_ => "pending".to_string(),
};
}
}
}
if tx_hash.starts_with("0x") && tx_hash.len() > 10 {
"confirmed".to_string()
} else {
"pending".to_string()
}
}
pub fn get_balance(chain_id: i64, address: String) -> i64 {
let chain_config = get_chain_config(chain_id);
#[cfg(feature = "http-interface")]
if let Some(ref config) = chain_config {
use serde_json::json;
let addr = if address.starts_with("0x") {
address.clone()
} else {
format!("0x{}", address)
};
if let Ok(result) = rpc::rpc_request(
&config.rpc_url,
"eth_getBalance",
vec![json!(addr), json!("latest")],
) {
if let Some(hex_str) = result.as_str() {
return rpc::hex_to_i64(hex_str);
}
}
}
if let Some(_) = chain_config {
if address.starts_with("0x") {
let hash_sum: i64 = address
.chars()
.filter(|c| c.is_ascii_hexdigit())
.map(|c| c.to_digit(16).unwrap_or(0) as i64)
.sum();
let limited_sum = hash_sum % 1000;
let wei_per_unit: i64 = 1000000000000000;
return limited_sum.checked_mul(wei_per_unit).unwrap_or(0);
}
}
0
}
fn erc20_contract_for_symbol(chain_id: i64, symbol: &str) -> Option<String> {
if chain_id != 1 {
return None;
}
let s = symbol.to_uppercase();
let addr = match s.as_str() {
"USDC" => "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"USDT" => "0xdac17f958d2ee523a2206206994597c13d831ec7",
"WETH" => "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"DAI" => "0x6b175474e89094c44da98b954eedeac495271d0f",
"WBTC" => "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
_ => return None,
};
Some(format!("0x{}", addr))
}
pub fn get_token_balance(chain_id: i64, token_symbol_or_contract: String, address: String) -> i64 {
let contract = if token_symbol_or_contract.starts_with("0x") {
token_symbol_or_contract.clone()
} else if let Some(addr) = erc20_contract_for_symbol(chain_id, &token_symbol_or_contract) {
addr
} else {
return 0;
};
let chain_config = match get_chain_config(chain_id) {
Some(c) => c,
None => return 0,
};
#[cfg(feature = "http-interface")]
{
use serde_json::json;
let addr_hex = address
.strip_prefix("0x")
.unwrap_or(&address)
.to_lowercase();
let padded = format!("{:0>64}", addr_hex);
let data = format!("0x70a08231{}", padded);
let to = if contract.starts_with("0x") {
contract
} else {
format!("0x{}", contract)
};
let tx = json!({ "to": to, "data": data });
if let Ok(result) =
rpc::rpc_request(&chain_config.rpc_url, "eth_call", vec![tx, json!("latest")])
{
if let Some(hex_str) = result.as_str() {
return rpc::hex_to_i64(hex_str);
}
}
}
0
}
pub fn call(
chain_id: i64,
contract_address: String,
function_name: String,
args: HashMap<String, String>,
) -> String {
let chain_config = match get_chain_config(chain_id) {
Some(c) => c,
None => return "error: chain not supported".to_string(),
};
crate::stdlib::log::audit(
"contract_call",
{
let mut data = std::collections::HashMap::new();
data.insert("chain_id".to_string(), Value::Int(chain_id));
data.insert(
"contract_address".to_string(),
Value::String(contract_address.clone()),
);
data.insert(
"function_name".to_string(),
Value::String(function_name.clone()),
);
data.insert("args".to_string(), Value::String(format!("{:?}", args)));
data
},
Some("chain"),
);
#[cfg(feature = "http-interface")]
if let Some(call_data) = args.get("data").or_else(|| args.get("calldata")) {
use serde_json::json;
let to = if contract_address.starts_with("0x") {
contract_address.clone()
} else {
format!("0x{}", contract_address)
};
let data_hex = if call_data.starts_with("0x") {
call_data.clone()
} else {
format!("0x{}", call_data)
};
let tx = json!({ "to": to, "data": data_hex });
if let Ok(result) =
rpc::rpc_request(&chain_config.rpc_url, "eth_call", vec![tx, json!("latest")])
{
if let Some(hex_str) = result.as_str() {
return hex_str.to_string();
}
}
}
format!(
"success: {} called on {} at {}",
function_name, contract_address, chain_config.name
)
}
pub fn mint(name: String, metadata: HashMap<String, String>) -> i64 {
crate::stdlib::log::audit(
"mint",
{
let mut data = std::collections::HashMap::new();
data.insert("name".to_string(), Value::String(name.clone()));
data.insert(
"metadata".to_string(),
Value::String(format!("{:?}", metadata)),
);
data
},
Some("chain"),
);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let mut combined = name.as_bytes().to_vec();
combined.extend(format!("{:?}", metadata).as_bytes());
combined.extend(timestamp.to_be_bytes());
let hash = md5::compute(&combined);
let hash_val: i64 = (hash[0] as i64)
.wrapping_shl(24)
.wrapping_add((hash[1] as i64).wrapping_shl(16))
.wrapping_add((hash[2] as i64).wrapping_shl(8))
.wrapping_add(hash[3] as i64);
let asset_id = timestamp
.abs()
.wrapping_mul(10000)
.wrapping_add(hash_val & 0x7FFF_FFFF);
crate::stdlib::log::info(
"mint_success",
{
let mut data = std::collections::HashMap::new();
data.insert("asset_id".to_string(), Value::Int(asset_id));
data
},
None,
);
asset_id
}
pub fn update(asset_id: i64, updates: HashMap<String, String>) -> bool {
crate::stdlib::log::audit(
"update",
{
let mut data = std::collections::HashMap::new();
data.insert("asset_id".to_string(), Value::Int(asset_id));
data.insert(
"updates".to_string(),
Value::String(format!("{:?}", updates)),
);
data
},
Some("chain"),
);
#[cfg(feature = "http-interface")]
if let Some(raw_hex) = updates
.get("raw_transaction")
.or_else(|| updates.get("signed_tx"))
{
let chain_id = updates
.get("chain_id")
.and_then(|s| s.trim().parse::<i64>().ok())
.or_else(|| {
env::var("CHAIN_ASSET_CHAIN_ID")
.ok()
.and_then(|s| s.trim().parse().ok())
});
if let Some(chain_id) = chain_id {
if let Some(config) = get_chain_config(chain_id) {
let tx_hex = if raw_hex.starts_with("0x") {
raw_hex.clone()
} else {
format!("0x{}", raw_hex)
};
if let Ok(result) = rpc::rpc_request(
&config.rpc_url,
"eth_sendRawTransaction",
vec![serde_json::json!(tx_hex)],
) {
if let Some(s) = result.as_str() {
if s.len() > 2 {
return true;
}
}
}
}
}
}
let success = asset_id > 0;
if success {
crate::stdlib::log::info(
"update_success",
{
let mut data = std::collections::HashMap::new();
data.insert("asset_id".to_string(), Value::Int(asset_id));
data
},
None,
);
} else {
crate::stdlib::log::info(
"update_failed",
{
let mut data = std::collections::HashMap::new();
data.insert("asset_id".to_string(), Value::Int(asset_id));
data.insert(
"reason".to_string(),
Value::String("Invalid asset ID".to_string()),
);
data
},
None,
);
}
success
}
pub fn get(asset_id: i64) -> HashMap<String, String> {
let mut asset_info = HashMap::new();
asset_info.insert("id".to_string(), asset_id.to_string());
asset_info.insert("name".to_string(), format!("Asset_{}", asset_id));
asset_info.insert("created_at".to_string(), (asset_id / 10000).to_string());
asset_info.insert("status".to_string(), "active".to_string());
#[cfg(feature = "http-interface")]
if let (Ok(chain_id_str), Ok(contract)) = (
env::var("CHAIN_ASSET_CHAIN_ID"),
env::var("CHAIN_ASSET_CONTRACT"),
) {
if let Ok(chain_id) = chain_id_str.trim().parse::<i64>() {
if let Some(config) = get_chain_config(chain_id) {
let token_id = if asset_id < 0 { 0u64 } else { asset_id as u64 };
let data = format!("0xc87b56dd{:064x}", token_id); let to = if contract.starts_with("0x") {
contract.clone()
} else {
format!("0x{}", contract)
};
let tx = serde_json::json!({ "to": to, "data": data });
if let Ok(result) = rpc::rpc_request(
&config.rpc_url,
"eth_call",
vec![tx, serde_json::json!("latest")],
) {
if let Some(hex_str) = result.as_str() {
asset_info.insert("token_uri_result".to_string(), hex_str.to_string());
}
}
}
}
}
let mut metadata = HashMap::new();
metadata.insert("description".to_string(), "A blockchain asset".to_string());
metadata.insert("version".to_string(), "1".to_string());
asset_info.insert("metadata".to_string(), format!("{:?}", metadata));
asset_info
}
pub fn exists(asset_id: i64) -> bool {
#[cfg(feature = "http-interface")]
if let (Ok(chain_id_str), Ok(contract)) = (
env::var("CHAIN_ASSET_CHAIN_ID"),
env::var("CHAIN_ASSET_CONTRACT"),
) {
if let Ok(chain_id) = chain_id_str.trim().parse::<i64>() {
if let Some(config) = get_chain_config(chain_id) {
let token_id = if asset_id < 0 { 0u64 } else { asset_id as u64 };
let data = format!("0x6352211e{:064x}", token_id); let to = if contract.starts_with("0x") {
contract.clone()
} else {
format!("0x{}", contract)
};
let tx = serde_json::json!({ "to": to, "data": data });
if let Ok(result) = rpc::rpc_request(
&config.rpc_url,
"eth_call",
vec![tx, serde_json::json!("latest")],
) {
if let Some(hex_str) = result.as_str() {
let s = hex_str
.strip_prefix("0x")
.unwrap_or(hex_str)
.trim_start_matches('0');
if !s.is_empty() && s != "0" {
return true;
}
}
}
return false;
}
}
}
asset_id > 0
}