use crate::error::{Result, WalletError};
use crate::history::TxStatus;
use crate::state_sync::ChainStateProvider;
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use tenzro_types::primitives::{Address, Hash, Signature};
use tenzro_types::{AssetId, Transaction};
pub struct TenzroRpcChainProvider {
client: Client,
rpc_url: String,
request_id: AtomicU64,
}
impl TenzroRpcChainProvider {
pub fn new(rpc_url: impl Into<String>) -> Result<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.pool_idle_timeout(Duration::from_secs(60))
.build()
.map_err(|e| WalletError::Other(format!("HTTP client init: {}", e)))?;
Ok(Self {
client,
rpc_url: rpc_url.into(),
request_id: AtomicU64::new(1),
})
}
pub fn with_client(client: Client, rpc_url: impl Into<String>) -> Self {
Self {
client,
rpc_url: rpc_url.into(),
request_id: AtomicU64::new(1),
}
}
async fn call(&self, method: &str, params: Value) -> Result<Value> {
let id = self.request_id.fetch_add(1, Ordering::Relaxed);
let body = json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": id,
});
let resp = self
.client
.post(&self.rpc_url)
.json(&body)
.send()
.await
.map_err(|e| WalletError::Other(format!("RPC {} send: {}", method, e)))?;
if !resp.status().is_success() {
return Err(WalletError::Other(format!(
"RPC {} HTTP {}",
method,
resp.status()
)));
}
let envelope: JsonRpcEnvelope = resp
.json()
.await
.map_err(|e| WalletError::Other(format!("RPC {} decode: {}", method, e)))?;
if let Some(err) = envelope.error {
return Err(WalletError::Other(format!(
"RPC {} error {}: {}",
method, err.code, err.message
)));
}
envelope
.result
.ok_or_else(|| WalletError::Other(format!("RPC {}: missing result", method)))
}
fn addr_hex(addr: &Address) -> String {
format!("0x{}", hex::encode(addr.as_bytes()))
}
fn parse_hex_quantity(value: &Value) -> Result<u128> {
let s = value
.as_str()
.ok_or_else(|| WalletError::Other("expected hex string quantity".into()))?;
let trimmed = s.strip_prefix("0x").unwrap_or(s);
if trimmed.is_empty() {
return Ok(0);
}
u128::from_str_radix(trimmed, 16)
.map_err(|e| WalletError::Other(format!("invalid hex quantity {}: {}", s, e)))
}
fn parse_hex_u64(value: &Value) -> Result<u64> {
Self::parse_hex_quantity(value).and_then(|q| {
u64::try_from(q)
.map_err(|_| WalletError::Other(format!("hex quantity overflows u64: {}", q)))
})
}
}
#[derive(Debug, Deserialize)]
struct JsonRpcEnvelope {
#[serde(default)]
result: Option<Value>,
#[serde(default)]
error: Option<JsonRpcErrorBody>,
}
#[derive(Debug, Deserialize)]
struct JsonRpcErrorBody {
code: i64,
message: String,
}
#[async_trait]
impl ChainStateProvider for TenzroRpcChainProvider {
async fn get_on_chain_balance(
&self,
address: &Address,
asset_id: &AssetId,
) -> Result<u128> {
let addr_hex = Self::addr_hex(address);
if asset_id.as_str() == "TNZO" {
let result = self
.call("eth_getBalance", json!([addr_hex, "latest"]))
.await?;
Self::parse_hex_quantity(&result)
} else {
let result = self
.call(
"tenzro_tokenBalance",
json!({
"address": addr_hex,
"symbol": asset_id.as_str(),
}),
)
.await?;
if let Some(obj) = result.as_object()
&& let Some(bal) = obj.get("balance")
{
return Self::parse_hex_quantity(bal);
}
Self::parse_hex_quantity(&result)
}
}
async fn get_on_chain_balances(
&self,
address: &Address,
) -> Result<Vec<(AssetId, u128)>> {
let tnzo = AssetId::tnzo();
let bal = self.get_on_chain_balance(address, &tnzo).await?;
Ok(vec![(tnzo, bal)])
}
async fn get_on_chain_nonce(&self, address: &Address) -> Result<u64> {
let addr_hex = Self::addr_hex(address);
let result = self
.call("eth_getTransactionCount", json!([addr_hex, "pending"]))
.await?;
Self::parse_hex_u64(&result)
}
async fn get_transaction_status(&self, tx_hash: &Hash) -> Result<TxStatus> {
let hash_hex = format!("0x{}", hex::encode(tx_hash.as_bytes()));
let result = self
.call("eth_getTransactionReceipt", json!([hash_hex]))
.await?;
if result.is_null() {
return Ok(TxStatus::Pending);
}
let status = result
.get("status")
.and_then(|s| s.as_str())
.unwrap_or("0x0");
if status == "0x1" || status == "0x01" {
Ok(TxStatus::Confirmed)
} else {
Ok(TxStatus::Failed)
}
}
async fn get_block_height(&self) -> Result<u64> {
let result = self.call("eth_blockNumber", json!([])).await?;
Self::parse_hex_u64(&result)
}
async fn submit_signed_transaction(
&self,
tx: &Transaction,
classical_sig: &Signature,
pq_sig: &[u8],
) -> Result<Hash> {
let from_hex = Self::addr_hex(&tx.from);
let to_hex = Self::addr_hex(&tx.to);
let amount_dec = match &tx.tx_type {
tenzro_types::TransactionType::Transfer { amount } => amount.to_string(),
_ => "0".to_string(),
};
let params = json!({
"from": from_hex,
"to": to_hex,
"value": amount_dec,
"nonce": tx.nonce.0,
"gas_limit": tx.gas_limit,
"gas_price": tx.gas_price,
"chain_id": tx.chain_id.0,
"timestamp": tx.timestamp.0,
"tx_type": tx.tx_type,
"signature": format!("0x{}", hex::encode(&classical_sig.bytes)),
"public_key": format!("0x{}", hex::encode(&classical_sig.public_key)),
"pq_signature": format!("0x{}", hex::encode(pq_sig)),
"pq_public_key": format!("0x{}", hex::encode(&tx.pq_public_key)),
});
let result = self
.call("eth_sendRawTransaction", params)
.await?;
let hash_str = result
.as_str()
.ok_or_else(|| WalletError::Other("eth_sendRawTransaction: expected hash string".into()))?;
let trimmed = hash_str.strip_prefix("0x").unwrap_or(hash_str);
let bytes = hex::decode(trimmed)
.map_err(|e| WalletError::Other(format!("invalid tx hash hex: {}", e)))?;
if bytes.len() != 32 {
return Err(WalletError::Other(format!(
"tx hash wrong length: got {}, want 32",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(Hash::new(arr))
}
}
pub const CROSS_VM_BRIDGE_ADDRESS: [u8; 20] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, 0x03,
];
pub const CROSS_VM_TRANSFER_SELECTOR: [u8; 4] = [0x3e, 0xaa, 0xf8, 0x6b];
pub const GET_VM_BALANCE_SELECTOR: [u8; 4] = [0xa3, 0xc5, 0x73, 0xeb];
pub const GET_SUPPORTED_VMS_SELECTOR: [u8; 4] = [0x12, 0x06, 0x5f, 0xe0];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum DestVm {
Native = 0,
Evm = 1,
Svm = 2,
Daml = 3,
}
pub fn cross_vm_transfer_calldata(
token_id: [u8; 32],
amount: [u8; 32],
dest_vm: DestVm,
caller: [u8; 20],
dest_address: &[u8],
) -> Vec<u8> {
let len = dest_address.len();
let padded_len = len.div_ceil(32) * 32;
let mut calldata = Vec::with_capacity(4 + 5 * 32 + 32 + padded_len);
calldata.extend_from_slice(&CROSS_VM_TRANSFER_SELECTOR);
calldata.extend_from_slice(&token_id);
calldata.extend_from_slice(&amount);
let mut dest_vm_padded = [0u8; 32];
dest_vm_padded[31] = dest_vm as u8;
calldata.extend_from_slice(&dest_vm_padded);
let mut offset_padded = [0u8; 32];
offset_padded[31] = 0xa0;
calldata.extend_from_slice(&offset_padded);
let mut caller_padded = [0u8; 32];
caller_padded[12..].copy_from_slice(&caller);
calldata.extend_from_slice(&caller_padded);
let mut length_padded = [0u8; 32];
length_padded[24..].copy_from_slice(&(len as u64).to_be_bytes());
calldata.extend_from_slice(&length_padded);
calldata.extend_from_slice(dest_address);
if padded_len > len {
calldata.extend(std::iter::repeat_n(0u8, padded_len - len));
}
calldata
}
pub fn get_vm_balance_calldata(
vm_type: DestVm,
address: [u8; 32],
token_id: [u8; 32],
) -> Vec<u8> {
let mut calldata = Vec::with_capacity(4 + 3 * 32);
calldata.extend_from_slice(&GET_VM_BALANCE_SELECTOR);
let mut vm_padded = [0u8; 32];
vm_padded[31] = vm_type as u8;
calldata.extend_from_slice(&vm_padded);
calldata.extend_from_slice(&address);
calldata.extend_from_slice(&token_id);
calldata
}
pub fn get_supported_vms_calldata() -> Vec<u8> {
GET_SUPPORTED_VMS_SELECTOR.to_vec()
}
pub fn u128_to_abi_amount(amount: u128) -> [u8; 32] {
let mut out = [0u8; 32];
out[16..].copy_from_slice(&amount.to_be_bytes());
out
}
pub fn pad_evm_address(addr: [u8; 20]) -> [u8; 32] {
let mut out = [0u8; 32];
out[12..].copy_from_slice(&addr);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cross_vm_transfer_calldata_layout() {
let token_id = [0xaau8; 32];
let amount = u128_to_abi_amount(1_000_000_000_000_000_000u128); let caller: [u8; 20] = [0x77; 20];
let dest_address: [u8; 20] = [0x42; 20];
let calldata =
cross_vm_transfer_calldata(token_id, amount, DestVm::Evm, caller, &dest_address);
assert_eq!(calldata.len(), 4 + 160 + 32 + 32);
assert_eq!(&calldata[0..4], &CROSS_VM_TRANSFER_SELECTOR);
let args = &calldata[4..];
assert_eq!(&args[0..32], &token_id);
assert_eq!(&args[32..64], &amount);
assert!(args[64..95].iter().all(|&b| b == 0));
assert_eq!(args[95], 0x01);
assert!(args[96..126].iter().all(|&b| b == 0));
assert_eq!(args[126], 0x00);
assert_eq!(args[127], 0xa0);
assert!(args[128..140].iter().all(|&b| b == 0));
assert_eq!(&args[140..160], &caller);
assert!(args[160..184].iter().all(|&b| b == 0));
assert_eq!(&args[184..192], &20u64.to_be_bytes());
assert_eq!(&args[192..212], &dest_address);
assert!(args[212..224].iter().all(|&b| b == 0));
}
#[test]
fn cross_vm_transfer_calldata_round_trips_through_precompile_decode() {
let token_id = {
let mut t = [0u8; 32];
t[31] = 0xCC; t
};
let amount_u128 = 7_500_000_000_000_000_000u128;
let amount = u128_to_abi_amount(amount_u128);
let caller: [u8; 20] = [0xEE; 20];
let dest_address: [u8; 20] = [0x99; 20];
let wallet_calldata =
cross_vm_transfer_calldata(token_id, amount, DestVm::Svm, caller, &dest_address);
let args = &wallet_calldata[4..];
let mut decoded_token_id = [0u8; 32];
decoded_token_id.copy_from_slice(&args[0..32]);
assert_eq!(decoded_token_id, token_id);
let mut amount_bytes = [0u8; 16];
amount_bytes.copy_from_slice(&args[48..64]);
assert_eq!(u128::from_be_bytes(amount_bytes), amount_u128);
assert_eq!(args[95], DestVm::Svm as u8);
let mut decoded_caller = [0u8; 20];
decoded_caller.copy_from_slice(&args[140..160]);
assert_eq!(decoded_caller, caller);
let offset_bytes = &args[96..128];
let mut offset_low = [0u8; 8];
offset_low.copy_from_slice(&offset_bytes[24..32]);
let offset = u64::from_be_bytes(offset_low) as usize;
assert_eq!(offset, 0xa0);
let mut length_low = [0u8; 8];
length_low.copy_from_slice(&args[offset + 24..offset + 32]);
let length = u64::from_be_bytes(length_low) as usize;
assert_eq!(length, 20);
let payload = &args[offset + 32..offset + 32 + length];
assert_eq!(payload, &dest_address);
}
#[test]
fn cross_vm_transfer_calldata_pads_dynamic_section() {
let token_id = [0u8; 32];
let amount = u128_to_abi_amount(1u128);
let caller: [u8; 20] = [0x33; 20];
let cd20 = cross_vm_transfer_calldata(token_id, amount, DestVm::Evm, caller, &[0x11; 20]);
assert_eq!(cd20.len(), 4 + 160 + 32 + 32);
let cd32 = cross_vm_transfer_calldata(token_id, amount, DestVm::Svm, caller, &[0x22; 32]);
assert_eq!(cd32.len(), 4 + 160 + 32 + 32);
let cd33 = cross_vm_transfer_calldata(token_id, amount, DestVm::Daml, caller, &[0x33; 33]);
assert_eq!(cd33.len(), 4 + 160 + 32 + 64);
}
#[test]
fn pad_evm_address_left_zeroes() {
let addr = [0xee; 20];
let padded = pad_evm_address(addr);
assert!(padded[..12].iter().all(|&b| b == 0));
assert_eq!(&padded[12..], &addr);
}
#[test]
fn u128_amount_big_endian() {
let abi = u128_to_abi_amount(0x0102030405060708090a0b0c0d0e0f10u128);
assert!(abi[..16].iter().all(|&b| b == 0));
assert_eq!(
&abi[16..],
&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10],
);
}
#[test]
fn get_supported_vms_calldata_is_selector_only() {
let calldata = get_supported_vms_calldata();
assert_eq!(calldata, GET_SUPPORTED_VMS_SELECTOR);
}
#[test]
fn parse_hex_quantity_zero_and_value() {
assert_eq!(
TenzroRpcChainProvider::parse_hex_quantity(&Value::String("0x0".into())).unwrap(),
0
);
assert_eq!(
TenzroRpcChainProvider::parse_hex_quantity(&Value::String("0x2a".into())).unwrap(),
42
);
assert_eq!(
TenzroRpcChainProvider::parse_hex_quantity(&Value::String("0x".into())).unwrap(),
0
);
}
}