use k256::ecdsa::SigningKey;
use crate::wallet;
use super::*;
pub async fn next_nonce(address_hex: &str) -> Result<u128, String> {
eth_get_transaction_count(address_hex).await
}
pub fn set_metadata_gas(byte_len: usize) -> u128 {
1_200_000 + byte_len as u128 * 8_500
}
pub async fn current_gas_price() -> Result<u128, String> {
eth_gas_price().await
}
pub async fn submit_and_wait_receipt(raw_hex: &str) -> Result<String, String> {
let tx_hash = eth_send_raw_transaction(raw_hex).await?;
wait_for_receipt(&tx_hash).await?;
Ok(tx_hash)
}
pub async fn token_balance_of(holder_hex: &str) -> Result<u128, String> {
erc20_balance_of(LOCALHARNESS_TOKEN_ADDRESS, holder_hex).await
}
pub async fn erc20_balance_of(token_hex: &str, holder_hex: &str) -> Result<u128, String> {
let holder_bytes = hex_to_bytes(holder_hex)?;
if holder_bytes.len() != 20 {
return Err(format!("holder must be 20 bytes, got {}", holder_bytes.len()));
}
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(&holder_bytes);
let calldata_hex = encode_call_hex(selector("balanceOf(address)"), &[padded]);
let result = eth_call(token_hex, &calldata_hex).await?;
decode_u256_as_u128(&result)
}
pub(crate) fn decode_u256_as_u128(hex: &str) -> Result<u128, String> {
let trimmed = hex.trim_start_matches("0x");
if trimmed.is_empty() {
return Ok(0);
}
let tail = if trimmed.len() <= 32 {
trimmed
} else {
&trimmed[trimmed.len() - 32..]
};
u128::from_str_radix(tail, 16).map_err(|e| e.to_string())
}
pub const ALPHA_USD_ADDRESS: &str = "0x20c0000000000000000000000000000000000001";
pub async fn submit_tempo_self_paid(
sender: &SigningKey,
calls: Vec<crate::tempo_tx::TempoCall>,
fee_token: Option<&str>,
gas_limit: u128,
) -> Result<String, String> {
use crate::tempo_tx::{sign_self_paid, TempoTxBuilder};
let sender_addr = wallet::address(sender);
let sender_hex = address_to_hex(&sender_addr);
let nonce = eth_get_transaction_count(&sender_hex).await?;
let gas_price = eth_gas_price().await?;
let mut builder = TempoTxBuilder::new(CHAIN_ID)
.max_priority_fee_per_gas(gas_price)
.max_fee_per_gas(gas_price)
.gas_limit(gas_limit)
.nonce(nonce)
.calls(calls);
if let Some(token) = fee_token {
builder = builder.fee_token(parse_eth_address(token)?);
}
let tx = builder.build();
let raw = sign_self_paid(tx, sender);
let raw_hex = format!("0x{}", bytes_to_hex(&raw));
let tx_hash = eth_send_raw_transaction(&raw_hex).await?;
wait_for_receipt(&tx_hash).await?;
Ok(tx_hash)
}
pub async fn submit_tempo_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
calls: Vec<crate::tempo_tx::TempoCall>,
fee_token: &str,
gas_limit: u128,
) -> Result<String, String> {
use crate::tempo_tx::{sign_sponsored, TempoTxBuilder};
let sender_addr = wallet::address(sender);
let sender_hex = address_to_hex(&sender_addr);
let nonce = eth_get_transaction_count(&sender_hex).await?;
let gas_price = eth_gas_price().await?;
let tx = TempoTxBuilder::new(CHAIN_ID)
.max_priority_fee_per_gas(gas_price)
.max_fee_per_gas(gas_price)
.gas_limit(gas_limit)
.nonce(nonce)
.calls(calls)
.fee_token(parse_eth_address(fee_token)?)
.sponsored()
.build();
let raw = sign_sponsored(tx, sender, fee_payer);
let raw_hex = format!("0x{}", bytes_to_hex(&raw));
let tx_hash = eth_send_raw_transaction(&raw_hex).await?;
wait_for_receipt(&tx_hash).await?;
Ok(tx_hash)
}
pub(crate) fn parse_eth_address(hex_str: &str) -> Result<[u8; 20], String> {
let bytes = hex_to_bytes(hex_str)?;
if bytes.len() != 20 {
return Err(format!("address must be 20 bytes, got {}", bytes.len()));
}
let mut out = [0u8; 20];
out.copy_from_slice(&bytes);
Ok(out)
}
pub(crate) async fn sponsored_call_to(
sender: &SigningKey,
fee_payer: &SigningKey,
to_hex: &str,
input: Vec<u8>,
fee_token: &str,
gas_limit: u128,
) -> Result<String, String> {
let call = crate::tempo_tx::TempoCall {
to: parse_eth_address(to_hex)?,
value_wei: 0,
input,
};
submit_tempo_sponsored(sender, fee_payer, vec![call], fee_token, gas_limit).await
}
pub(crate) async fn sponsored_diamond_call(
sender: &SigningKey,
fee_payer: &SigningKey,
input: Vec<u8>,
fee_token: &str,
gas_limit: u128,
) -> Result<String, String> {
sponsored_call_to(sender, fee_payer, REGISTRY_ADDRESS, input, fee_token, gas_limit).await
}
pub(crate) async fn sponsored_escrow_diamond_call(
sender: &SigningKey,
fee_payer: &SigningKey,
amount_wei: u128,
input: Vec<u8>,
fee_token: &str,
gas_limit: u128,
) -> Result<String, String> {
sponsored_escrow_diamond_call_bridged(sender, fee_payer, amount_wei, input, fee_token, gas_limit, 0)
.await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn sponsored_escrow_diamond_call_bridged(
sender: &SigningKey,
fee_payer: &SigningKey,
amount_wei: u128,
input: Vec<u8>,
fee_token: &str,
gas_limit: u128,
bridge_wei: u128,
) -> Result<String, String> {
let calls = escrow_call_batch(amount_wei, input, bridge_wei)?;
let gas = if bridge_wei > 0 { gas_limit + 150_000 } else { gas_limit };
submit_tempo_sponsored(sender, fee_payer, calls, fee_token, gas).await
}
pub(crate) fn escrow_call_batch(
amount_wei: u128,
input: Vec<u8>,
bridge_wei: u128,
) -> Result<Vec<crate::tempo_tx::TempoCall>, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let token_addr = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS)?;
let mut calls = Vec::with_capacity(3);
if bridge_wei > 0 {
calls.push(crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_withdraw_credits(bridge_wei),
});
}
calls.push(crate::tempo_tx::TempoCall {
to: token_addr,
value_wei: 0,
input: encode_approve(&diamond_addr, amount_wei),
});
calls.push(crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input,
});
Ok(calls)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escrow_call_batch_bridged_order_and_targets() {
let escrow_input = vec![0xAA, 0xBB, 0xCC, 0xDD];
let calls = escrow_call_batch(700, escrow_input.clone(), 250).unwrap();
assert_eq!(calls.len(), 3);
let diamond = parse_eth_address(REGISTRY_ADDRESS).unwrap();
let token = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS).unwrap();
assert_eq!(calls[0].to, diamond);
assert_eq!(calls[0].input, encode_withdraw_credits(250));
assert_eq!(calls[1].to, token);
assert_eq!(calls[1].input, encode_approve(&diamond, 700));
assert_eq!(calls[2].to, diamond);
assert_eq!(calls[2].input, escrow_input);
assert!(calls.iter().all(|c| c.value_wei == 0));
}
#[test]
fn escrow_call_batch_zero_bridge_matches_original_shape() {
let escrow_input = vec![0x01, 0x02, 0x03];
let unbridged = escrow_call_batch(42, escrow_input.clone(), 0).unwrap();
let bridged = escrow_call_batch(42, escrow_input, 7).unwrap();
assert_eq!(unbridged.len(), 2);
for (a, b) in unbridged.iter().zip(&bridged[1..]) {
assert_eq!(a.to, b.to);
assert_eq!(a.value_wei, b.value_wei);
assert_eq!(a.input, b.input);
}
}
#[test]
fn decode_u256_as_u128_truncation_and_empty() {
assert_eq!(decode_u256_as_u128("0x").unwrap(), 0);
assert_eq!(decode_u256_as_u128(&format!("0x{}", word_usize(42))).unwrap(), 42);
let max = format!("0x{}{}", "0".repeat(32), "f".repeat(32));
assert_eq!(decode_u256_as_u128(&max).unwrap(), u128::MAX);
}
}