use k256::ecdsa::SigningKey;
use serde::{Deserialize, Serialize};
use sha3::{Digest, Keccak256};
use crate::wallet;
pub const RPC_URL: &str = "https://rpc.moderato.tempo.xyz";
pub const REGISTRY_ADDRESS: &str = "0x6f2858b4b10bf8d4ea372a446e69bea8fbce2930";
pub const CHAIN_ID: u64 = 42431;
pub const BOOTSTRAP_FAUCET_ADDRESS: &str = "0x0000000000000000000000000000000000000000";
pub const LOCALHARNESS_TOKEN_ADDRESS: &str = "0xC1FC0452670049953ED64f2B177beBed4090A5bc";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Status {
Unknown,
Available,
Taken { agent_id: u64 },
}
#[derive(Debug, Clone)]
pub struct OwnedToken {
pub token_id: u64,
pub name: String,
pub tba: Option<String>,
}
pub async fn list_owned_tokens(owner_hex: &str) -> Result<Vec<OwnedToken>, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(Vec::new());
}
let total = next_id().await?;
let owner_lower = owner_hex.to_lowercase();
let mut out: Vec<OwnedToken> = Vec::new();
for id in 1..total {
let owner = match owner_of_id(id).await {
Ok(Some(addr)) => addr,
_ => continue,
};
if owner.to_lowercase() != owner_lower {
continue;
}
let name = name_of_id(id).await.unwrap_or_default();
if name.is_empty() {
continue;
}
let tba = tba_of_name(&name).await.ok().flatten();
out.push(OwnedToken {
token_id: id,
name,
tba,
});
}
out.reverse();
Ok(out)
}
async fn next_id() -> Result<u64, String> {
let calldata = format!("0x{}", bytes_to_hex(&selector("nextId()")));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
decode_u256_as_u64(&result_hex)
}
async fn owner_of_id(id: u64) -> Result<Option<String>, String> {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("ownerOfId(uint256)"));
data.extend_from_slice(&u256_be(id as u128));
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let trimmed = result_hex.trim().trim_start_matches("0x");
if trimmed.len() < 64 {
return Ok(None);
}
let addr_hex = &trimmed[trimmed.len() - 40..];
if addr_hex.chars().all(|c| c == '0') {
return Ok(None);
}
Ok(Some(format!("0x{}", addr_hex.to_lowercase())))
}
pub async fn name_of_id(id: u64) -> Result<String, String> {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("nameOfId(uint256)"));
data.extend_from_slice(&u256_be(id as u128));
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let raw = hex_to_bytes(&result_hex)?;
if raw.len() < 64 {
return Err(format!("nameOfId: short response {} bytes", raw.len()));
}
let len = u64::from_be_bytes(raw[56..64].try_into().map_err(|e: std::array::TryFromSliceError| e.to_string())?) as usize;
if raw.len() < 64 + len {
return Err(format!("nameOfId: truncated body (need {} bytes, have {})", 64 + len, raw.len()));
}
String::from_utf8(raw[64..64 + len].to_vec()).map_err(|e| e.to_string())
}
pub async fn tba_of_token_id(token_id: u64) -> Result<Option<String>, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(None);
}
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("tokenBoundAccount(uint256)"));
data.extend_from_slice(&u256_be(token_id as u128));
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = match eth_call(REGISTRY_ADDRESS, &calldata).await {
Ok(h) => h,
Err(err) => {
if err.contains("nonexistent token") || err.contains("registry unset") {
return Ok(None);
}
return Err(err);
}
};
let trimmed = result_hex.trim().trim_start_matches("0x");
if trimmed.len() < 64 {
return Err(format!("tokenBoundAccount: short response {trimmed}"));
}
let addr_hex = &trimmed[trimmed.len() - 40..];
if addr_hex.chars().all(|c| c == '0') {
return Ok(None);
}
Ok(Some(format!("0x{}", addr_hex.to_lowercase())))
}
pub async fn tba_of_name(name: &str) -> Result<Option<String>, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(None);
}
let calldata = encode_string_call("tokenBoundAccountByName(string)", name);
let result_hex = match eth_call(REGISTRY_ADDRESS, &calldata).await {
Ok(h) => h,
Err(err) => {
if err.contains("name unregistered") || err.contains("nonexistent token") {
return Ok(None);
}
return Err(err);
}
};
let trimmed = result_hex.trim().trim_start_matches("0x");
if trimmed.len() < 64 {
return Err(format!("tokenBoundAccountByName: short response {trimmed}"));
}
let addr_hex = &trimmed[trimmed.len() - 40..];
if addr_hex.chars().all(|c| c == '0') {
return Ok(None);
}
Ok(Some(format!("0x{}", addr_hex.to_lowercase())))
}
pub async fn owner_of_name(name: &str) -> Result<Option<String>, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(None);
}
let calldata = encode_owner_of_name(name);
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let trimmed = result_hex.trim().trim_start_matches("0x");
if trimmed.len() < 64 {
return Err(format!("ownerOfName: short response {trimmed}"));
}
let addr_hex = &trimmed[trimmed.len() - 40..];
if addr_hex.chars().all(|c| c == '0') {
return Ok(None);
}
Ok(Some(format!("0x{}", addr_hex.to_lowercase())))
}
fn encode_owner_of_name(name: &str) -> String {
encode_string_call("ownerOfName(string)", name)
}
fn encode_string_call(signature: &str, value: &str) -> String {
let sel = selector(signature);
let bytes = value.as_bytes();
let len = bytes.len();
let padded_len = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 32 + 32 + padded_len);
buf.extend_from_slice(&sel);
buf.extend_from_slice(&u256_be(0x20));
buf.extend_from_slice(&u256_be(len as u128));
buf.extend_from_slice(bytes);
buf.resize(4 + 32 + 32 + padded_len, 0);
let mut out = String::with_capacity(2 + buf.len() * 2);
out.push_str("0x");
for b in &buf {
out.push_str(&format!("{b:02x}"));
}
out
}
pub async fn check_name(name: &str) -> Result<Status, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(Status::Unknown);
}
let calldata = encode_id_of_name(name);
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let id = decode_u256_as_u64(&result_hex)?;
Ok(if id == 0 {
Status::Available
} else {
Status::Taken { agent_id: id }
})
}
pub async fn claim_name(signer: &SigningKey, name: &str) -> Result<String, String> {
if REGISTRY_ADDRESS == zero_address() {
return Err("registry not deployed".into());
}
let from = wallet::address(signer);
let from_hex = address_to_hex(&from);
let nonce = eth_get_transaction_count(&from_hex).await?;
let gas_price = eth_gas_price().await?;
let calldata_hex = encode_register(name);
let gas_limit = eth_estimate_gas(&from_hex, REGISTRY_ADDRESS, &calldata_hex).await?;
let calldata_bytes = hex_to_bytes(&calldata_hex)?;
let unsigned = rlp_legacy_unsigned(
nonce,
gas_price,
gas_limit,
REGISTRY_ADDRESS,
0,
&calldata_bytes,
CHAIN_ID,
)?;
let mut hasher = Keccak256::new();
hasher.update(&unsigned);
let mut prehash = [0u8; 32];
prehash.copy_from_slice(&hasher.finalize());
let sig = wallet::sign_hash(signer, &prehash);
let r = &sig[..32];
let s = &sig[32..64];
let rec_id = (sig[64] - 27) as u64;
let v = CHAIN_ID * 2 + 35 + rec_id;
let signed = rlp_legacy_signed(
nonce,
gas_price,
gas_limit,
REGISTRY_ADDRESS,
0,
&calldata_bytes,
v,
r,
s,
)?;
let raw_hex = format!("0x{}", bytes_to_hex(&signed));
let tx_hash = eth_send_raw_transaction(&raw_hex).await?;
wait_for_receipt(&tx_hash).await?;
Ok(tx_hash)
}
pub async fn request_faucet_funds(address_hex: &str) -> Result<(), String> {
let body = RpcRequest {
jsonrpc: "2.0",
id: 1,
method: "tempo_fundAddress",
params: serde_json::json!([address_hex]),
};
let client = reqwest::Client::new();
let resp = client
.post(RPC_URL)
.json(&body)
.send()
.await
.map_err(|e| format!("faucet send: {e}"))?;
let parsed: RpcResponse = resp
.json()
.await
.map_err(|e| format!("faucet decode: {e}"))?;
if let Some(err) = parsed.error {
return Err(format!("faucet: {}", err.message));
}
Ok(())
}
fn selector(signature: &str) -> [u8; 4] {
let mut h = Keccak256::new();
h.update(signature.as_bytes());
let digest = h.finalize();
let mut out = [0u8; 4];
out.copy_from_slice(&digest[..4]);
out
}
fn encode_id_of_name(name: &str) -> String {
let sel = selector("idOfName(string)");
let bytes = name.as_bytes();
let len = bytes.len();
let padded_len = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 32 + 32 + padded_len);
buf.extend_from_slice(&sel);
buf.extend_from_slice(&u256_be(0x20));
buf.extend_from_slice(&u256_be(len as u128));
buf.extend_from_slice(bytes);
buf.resize(4 + 32 + 32 + padded_len, 0);
let mut out = String::with_capacity(2 + buf.len() * 2);
out.push_str("0x");
for b in &buf {
out.push_str(&format!("{b:02x}"));
}
out
}
fn encode_register(name: &str) -> String {
let sel = selector("register(string)");
let bytes = name.as_bytes();
let len = bytes.len();
let padded_len = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 32 + 32 + padded_len);
buf.extend_from_slice(&sel);
buf.extend_from_slice(&u256_be(0x20));
buf.extend_from_slice(&u256_be(len as u128));
buf.extend_from_slice(bytes);
buf.resize(4 + 32 + 32 + padded_len, 0);
let mut out = String::with_capacity(2 + buf.len() * 2);
out.push_str("0x");
for b in &buf {
out.push_str(&format!("{b:02x}"));
}
out
}
fn u256_be(value: u128) -> [u8; 32] {
let mut out = [0u8; 32];
out[16..].copy_from_slice(&value.to_be_bytes());
out
}
fn decode_u256_as_u64(hex: &str) -> Result<u64, String> {
let stripped = hex.trim().trim_start_matches("0x");
if stripped.is_empty() {
return Ok(0);
}
if stripped.len() > 64 {
return Err(format!("u256 hex too long: {}", stripped.len()));
}
let high_end = stripped.len().saturating_sub(16);
if stripped[..high_end].chars().any(|c| c != '0') {
return Err("u256 exceeds u64 range".into());
}
let tail = &stripped[high_end..];
u64::from_str_radix(tail, 16).map_err(|e| e.to_string())
}
fn zero_address() -> &'static str {
"0x0000000000000000000000000000000000000000"
}
fn address_to_hex(addr: &[u8; 20]) -> String {
let mut s = String::with_capacity(42);
s.push_str("0x");
for b in addr {
s.push_str(&format!("{b:02x}"));
}
s
}
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
let trimmed = hex.trim().trim_start_matches("0x").trim_start_matches("0X");
if trimmed.len() % 2 != 0 {
return Err("hex odd length".into());
}
let mut out = Vec::with_capacity(trimmed.len() / 2);
let bytes = trimmed.as_bytes();
let mut i = 0;
while i < bytes.len() {
let hi = nibble_value(bytes[i])?;
let lo = nibble_value(bytes[i + 1])?;
out.push((hi << 4) | lo);
i += 2;
}
Ok(out)
}
fn nibble_value(b: u8) -> Result<u8, String> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(format!("non-hex byte {b}")),
}
}
fn bytes_to_hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn parse_hex_quantity(hex: &str) -> Result<u128, String> {
let trimmed = hex.trim().trim_start_matches("0x");
if trimmed.is_empty() {
return Ok(0);
}
u128::from_str_radix(trimmed, 16).map_err(|e| e.to_string())
}
pub async fn next_nonce(address_hex: &str) -> Result<u128, String> {
eth_get_transaction_count(address_hex).await
}
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 fn rlp_native_transfer_unsigned(
to_hex: &str,
value_wei: u128,
nonce: u128,
gas_price: u128,
gas_limit: u128,
) -> Result<Vec<u8>, String> {
rlp_legacy_unsigned(nonce, gas_price, gas_limit, to_hex, value_wei, &[], CHAIN_ID)
}
pub fn rlp_native_transfer_signed(
to_hex: &str,
value_wei: u128,
nonce: u128,
gas_price: u128,
gas_limit: u128,
sig_65: &[u8; 65],
) -> Result<String, String> {
let rec_id = (sig_65[64] - 27) as u64;
let v = CHAIN_ID * 2 + 35 + rec_id;
let signed = rlp_legacy_signed(
nonce,
gas_price,
gas_limit,
to_hex,
value_wei,
&[],
v,
&sig_65[..32],
&sig_65[32..64],
)?;
Ok(format!("0x{}", bytes_to_hex(&signed)))
}
pub const NATIVE_TRANSFER_GAS_LIMIT: u128 = 21_000;
pub async fn balance_of(address_hex: &str) -> Result<u128, String> {
let hex = rpc(
"eth_getBalance",
serde_json::json!([address_hex, "latest"]),
)
.await?;
parse_hex_quantity(&hex)
}
pub async fn wait_for_min_balance(
address_hex: &str,
min_wei: u128,
max_attempts: u32,
) -> Result<u128, String> {
for _ in 0..max_attempts {
let bal = balance_of(address_hex).await?;
if bal >= min_wei {
return Ok(bal);
}
sleep_ms(1000).await;
}
Err(format!(
"balance for {address_hex} did not reach {min_wei} wei within {max_attempts}s"
))
}
pub async fn token_balance_of(holder_hex: &str) -> Result<u128, String> {
if LOCALHARNESS_TOKEN_ADDRESS == zero_address() {
return Err("localharness token not deployed".into());
}
let selector = selector("balanceOf(address)");
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 mut calldata = Vec::with_capacity(36);
calldata.extend_from_slice(&selector);
calldata.extend_from_slice(&padded);
let calldata_hex = format!("0x{}", bytes_to_hex(&calldata));
let result = eth_call(LOCALHARNESS_TOKEN_ADDRESS, &calldata_hex).await?;
decode_u256_as_u128(&result)
}
pub async fn token_transfer(
signer: &SigningKey,
to_hex: &str,
amount_token_wei: u128,
) -> Result<String, String> {
let to_bytes = hex_to_bytes(to_hex)?;
if to_bytes.len() != 20 {
return Err(format!("to must be 20 bytes, got {}", to_bytes.len()));
}
let selector = selector("transfer(address,uint256)");
let mut to_padded = [0u8; 32];
to_padded[12..].copy_from_slice(&to_bytes);
let amount_bytes = u256_be(amount_token_wei);
let mut calldata = Vec::with_capacity(4 + 32 + 32);
calldata.extend_from_slice(&selector);
calldata.extend_from_slice(&to_padded);
calldata.extend_from_slice(&amount_bytes);
sign_and_submit_call(signer, LOCALHARNESS_TOKEN_ADDRESS, 0, &calldata).await
}
async fn sign_and_submit_call(
signer: &SigningKey,
to_hex: &str,
value_wei: u128,
calldata: &[u8],
) -> Result<String, String> {
if to_hex == zero_address() {
return Err("target contract address is zero".into());
}
let from_bytes = wallet::address(signer);
let from_hex = address_to_hex(&from_bytes);
let nonce = eth_get_transaction_count(&from_hex).await?;
let gas_price = eth_gas_price().await?;
let calldata_hex = format!("0x{}", bytes_to_hex(calldata));
let gas_limit = eth_estimate_gas(&from_hex, to_hex, &calldata_hex).await?;
let unsigned = rlp_legacy_unsigned(
nonce, gas_price, gas_limit, to_hex, value_wei, calldata, CHAIN_ID,
)?;
let mut hasher = Keccak256::new();
hasher.update(&unsigned);
let mut prehash = [0u8; 32];
prehash.copy_from_slice(&hasher.finalize());
let sig = wallet::sign_hash(signer, &prehash);
let rec_id = (sig[64] - 27) as u64;
let v = CHAIN_ID * 2 + 35 + rec_id;
let signed = rlp_legacy_signed(
nonce, gas_price, gas_limit, to_hex, value_wei, calldata,
v, &sig[..32], &sig[32..64],
)?;
let raw_hex = format!("0x{}", bytes_to_hex(&signed));
let tx_hash = eth_send_raw_transaction(&raw_hex).await?;
wait_for_receipt(&tx_hash).await?;
Ok(tx_hash)
}
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)
}
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 async fn main_of(holder_hex: &str) -> Result<u64, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let selector = selector("mainOf(address)");
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 mut calldata = Vec::with_capacity(36);
calldata.extend_from_slice(&selector);
calldata.extend_from_slice(&padded);
let calldata_hex = format!("0x{}", bytes_to_hex(&calldata));
let result = eth_call(REGISTRY_ADDRESS, &calldata_hex).await?;
decode_u256_as_u64(&result)
}
pub async fn register_main(signer: &SigningKey, token_id: u64) -> Result<String, String> {
sign_and_submit_call(signer, REGISTRY_ADDRESS, 0, &encode_register_main(token_id)).await
}
pub async fn register_main_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
fee_token: &str,
) -> Result<String, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let token_addr = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS)?;
let cost = main_cost().await.unwrap_or(0);
let main_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_register_main(token_id),
};
let calls = if cost > 0 {
let approve_call = crate::tempo_tx::TempoCall {
to: token_addr,
value_wei: 0,
input: encode_approve(&diamond_addr, cost),
};
vec![approve_call, main_call]
} else {
vec![main_call]
};
submit_tempo_sponsored(sender, fee_payer, calls, fee_token, 700_000).await
}
fn encode_register_main(token_id: u64) -> Vec<u8> {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("registerMain(uint256)"));
data.extend_from_slice(&u256_be(token_id as u128));
data
}
pub async fn is_authorized_signer(tba_address: &str, signer_hex: &str) -> Result<bool, String> {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("isAuthorizedSigner(address)"));
let signer_bytes = hex_to_bytes(signer_hex)?;
if signer_bytes.len() != 20 {
return Err(format!("signer must be 20 bytes, got {}", signer_bytes.len()));
}
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(&signer_bytes);
data.extend_from_slice(&padded);
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(tba_address, &calldata).await?;
let trimmed = result_hex.trim().trim_start_matches("0x");
Ok(trimmed.chars().last().map(|c| c == '1').unwrap_or(false))
}
pub async fn add_signer_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
tba_address: &str,
new_signer_hex: &str,
fee_token: &str,
) -> Result<String, String> {
let new_signer = parse_eth_address(new_signer_hex)?;
let tba_addr = parse_eth_address(tba_address)?;
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let create_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_create_tba(token_id),
};
let add_call = crate::tempo_tx::TempoCall {
to: tba_addr,
value_wei: 0,
input: encode_add_signer(&new_signer),
};
submit_tempo_sponsored(
sender,
fee_payer,
vec![create_call, add_call],
fee_token,
1_000_000,
)
.await
}
pub async fn remove_signer_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
tba_address: &str,
signer_hex: &str,
fee_token: &str,
) -> Result<String, String> {
let signer_addr = parse_eth_address(signer_hex)?;
let tba_addr = parse_eth_address(tba_address)?;
let call = crate::tempo_tx::TempoCall {
to: tba_addr,
value_wei: 0,
input: encode_remove_signer(&signer_addr),
};
submit_tempo_sponsored(sender, fee_payer, vec![call], fee_token, 500_000).await
}
pub async fn main_cost() -> Result<u128, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let calldata = format!("0x{}", bytes_to_hex(&selector("mainCost()")));
let result = eth_call(REGISTRY_ADDRESS, &calldata).await?;
decode_u256_as_u128(&result)
}
pub async fn treasury_balance() -> Result<u128, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let calldata = format!("0x{}", bytes_to_hex(&selector("treasuryBalance()")));
let result = eth_call(REGISTRY_ADDRESS, &calldata).await?;
decode_u256_as_u128(&result)
}
pub async fn registration_cost() -> Result<u128, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let calldata = format!("0x{}", bytes_to_hex(&selector("registrationCost()")));
let result = eth_call(REGISTRY_ADDRESS, &calldata).await?;
decode_u256_as_u128(&result)
}
fn encode_approve(spender: &[u8; 20], amount_wei: u128) -> Vec<u8> {
let sel = selector("approve(address,uint256)");
let mut spender_padded = [0u8; 32];
spender_padded[12..].copy_from_slice(spender);
let amount_padded = u256_be(amount_wei);
let mut out = Vec::with_capacity(4 + 32 + 32);
out.extend_from_slice(&sel);
out.extend_from_slice(&spender_padded);
out.extend_from_slice(&amount_padded);
out
}
pub async fn claim_daily_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
fee_token: &str,
) -> Result<String, String> {
let call = crate::tempo_tx::TempoCall {
to: parse_eth_address(REGISTRY_ADDRESS)?,
value_wei: 0,
input: selector("claimDaily()").to_vec(),
};
submit_tempo_sponsored(sender, fee_payer, vec![call], fee_token, 600_000).await
}
pub async fn can_claim_credits(account_hex: &str) -> Result<bool, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(false);
}
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("canClaim(address)"));
let account_bytes = hex_to_bytes(account_hex)?;
if account_bytes.len() != 20 {
return Err(format!("account must be 20 bytes, got {}", account_bytes.len()));
}
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(&account_bytes);
data.extend_from_slice(&padded);
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let trimmed = result_hex.trim().trim_start_matches("0x");
Ok(trimmed.chars().last().map(|c| c == '1').unwrap_or(false))
}
pub async fn daily_allowance() -> Result<u128, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let calldata = format!("0x{}", bytes_to_hex(&selector("dailyAllowance()")));
let result = eth_call(REGISTRY_ADDRESS, &calldata).await?;
decode_u256_as_u128(&result)
}
pub async fn last_claim_day(account_hex: &str) -> Result<u64, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("lastClaimDay(address)"));
let account_bytes = hex_to_bytes(account_hex)?;
if account_bytes.len() != 20 {
return Err(format!("account must be 20 bytes, got {}", account_bytes.len()));
}
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(&account_bytes);
data.extend_from_slice(&padded);
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let val = decode_u256_as_u128(&result_hex)?;
Ok(val as u64)
}
pub async fn tba_execute_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
tba_address: &str,
target_hex: &str,
value_wei: u128,
inner_data: Vec<u8>,
fee_token: &str,
gas_limit: u128,
) -> Result<String, String> {
let tba_addr = parse_eth_address(tba_address)?;
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let target = parse_eth_address(target_hex)?;
let create_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_create_tba(token_id),
};
let execute_call = crate::tempo_tx::TempoCall {
to: tba_addr,
value_wei: 0,
input: encode_tba_execute(&target, value_wei, &inner_data),
};
submit_tempo_sponsored(
sender,
fee_payer,
vec![create_call, execute_call],
fee_token,
gas_limit,
)
.await
}
pub async fn tba_transfer_lh_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
tba_address: &str,
recipient_hex: &str,
amount_wei: u128,
fee_token: &str,
) -> Result<String, String> {
let recipient = parse_eth_address(recipient_hex)?;
let mut transfer_data = Vec::with_capacity(4 + 32 + 32);
transfer_data.extend_from_slice(&selector("transfer(address,uint256)"));
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(&recipient);
transfer_data.extend_from_slice(&padded);
transfer_data.extend_from_slice(&u256_be(amount_wei));
tba_execute_sponsored(
sender,
fee_payer,
token_id,
tba_address,
LOCALHARNESS_TOKEN_ADDRESS,
0,
transfer_data,
fee_token,
800_000,
)
.await
}
fn encode_tba_execute(target: &[u8; 20], value_wei: u128, data: &[u8]) -> Vec<u8> {
let sel = selector("execute(address,uint256,bytes,uint8)");
let mut target_padded = [0u8; 32];
target_padded[12..].copy_from_slice(target);
let data_len = data.len();
let padded_len = data_len.div_ceil(32) * 32;
let data_offset: u128 = 0x80;
let mut out = Vec::with_capacity(4 + 128 + 32 + padded_len);
out.extend_from_slice(&sel);
out.extend_from_slice(&target_padded);
out.extend_from_slice(&u256_be(value_wei));
out.extend_from_slice(&u256_be(data_offset));
out.extend_from_slice(&u256_be(0)); out.extend_from_slice(&u256_be(data_len as u128));
out.extend_from_slice(data);
out.resize(out.len() + (padded_len - data_len), 0);
out
}
fn encode_create_tba(token_id: u64) -> Vec<u8> {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector("createTokenBoundAccount(uint256)"));
data.extend_from_slice(&u256_be(token_id as u128));
data
}
fn encode_add_signer(addr: &[u8; 20]) -> Vec<u8> {
let sel = selector("addSigner(address)");
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(addr);
let mut out = Vec::with_capacity(4 + 32);
out.extend_from_slice(&sel);
out.extend_from_slice(&padded);
out
}
fn encode_remove_signer(addr: &[u8; 20]) -> Vec<u8> {
let sel = selector("removeSigner(address)");
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(addr);
let mut out = Vec::with_capacity(4 + 32);
out.extend_from_slice(&sel);
out.extend_from_slice(&padded);
out
}
pub async fn claim_and_maybe_set_main(
signer: &SigningKey,
name: &str,
) -> Result<String, String> {
let tx_hash = claim_name(signer, name).await?;
let addr_hex = address_to_hex(&wallet::address(signer));
match main_of(&addr_hex).await {
Ok(0) => {
if let Ok(Status::Taken { agent_id }) = check_name(name).await {
if let Err(err) = register_main(signer, agent_id).await {
log_main_warning(&err);
}
}
}
Ok(_) => {} Err(err) => log_main_warning(&err),
}
Ok(tx_hash)
}
pub async fn claim_and_maybe_set_main_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
name: &str,
fee_token: &str,
) -> Result<String, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let token_addr = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS)?;
let cost = registration_cost().await.unwrap_or(0);
let register_calldata = hex_to_bytes(&encode_register(name))?;
let register_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: register_calldata,
};
let calls = if cost > 0 {
let approve_call = crate::tempo_tx::TempoCall {
to: token_addr,
value_wei: 0,
input: encode_approve(&diamond_addr, cost),
};
vec![approve_call, register_call]
} else {
vec![register_call]
};
let tx_hash = submit_tempo_sponsored(
sender,
fee_payer,
calls,
fee_token,
2_200_000,
)
.await?;
let sender_addr = address_to_hex(&wallet::address(sender));
if let Ok(0) = main_of(&sender_addr).await {
if let Ok(Status::Taken { agent_id }) = check_name(name).await {
if let Err(err) =
register_main_sponsored(sender, fee_payer, agent_id, fee_token).await
{
log_main_warning(&err);
}
}
}
Ok(tx_hash)
}
#[cfg(target_arch = "wasm32")]
fn log_main_warning(err: &str) {
use wasm_bindgen::JsValue;
web_sys::console::warn_1(&JsValue::from_str(&format!("auto-set MAIN: {err}")));
}
#[cfg(not(target_arch = "wasm32"))]
fn log_main_warning(_err: &str) {
}
pub fn rlp_call_unsigned(
to_hex: &str,
value_wei: u128,
data: &[u8],
nonce: u128,
gas_price: u128,
gas_limit: u128,
) -> Result<Vec<u8>, String> {
rlp_legacy_unsigned(nonce, gas_price, gas_limit, to_hex, value_wei, data, CHAIN_ID)
}
pub fn rlp_call_signed(
to_hex: &str,
value_wei: u128,
data: &[u8],
nonce: u128,
gas_price: u128,
gas_limit: u128,
sig_65: &[u8; 65],
) -> Result<String, String> {
let rec_id = (sig_65[64] - 27) as u64;
let v = CHAIN_ID * 2 + 35 + rec_id;
let signed = rlp_legacy_signed(
nonce, gas_price, gas_limit, to_hex, value_wei, data,
v, &sig_65[..32], &sig_65[32..64],
)?;
Ok(format!("0x{}", bytes_to_hex(&signed)))
}
#[allow(clippy::too_many_arguments)]
fn rlp_legacy_unsigned(
nonce: u128,
gas_price: u128,
gas_limit: u128,
to_hex: &str,
value: u128,
data: &[u8],
chain_id: u64,
) -> Result<Vec<u8>, String> {
let to_bytes = hex_to_bytes(to_hex)?;
let items = vec![
wallet::rlp_uint(nonce),
wallet::rlp_uint(gas_price),
wallet::rlp_uint(gas_limit),
wallet::rlp_bytes(&to_bytes),
wallet::rlp_uint(value),
wallet::rlp_bytes(data),
wallet::rlp_uint(chain_id as u128),
wallet::rlp_uint(0),
wallet::rlp_uint(0),
];
Ok(wallet::rlp_list(&items))
}
#[allow(clippy::too_many_arguments)]
fn rlp_legacy_signed(
nonce: u128,
gas_price: u128,
gas_limit: u128,
to_hex: &str,
value: u128,
data: &[u8],
v: u64,
r: &[u8],
s: &[u8],
) -> Result<Vec<u8>, String> {
let to_bytes = hex_to_bytes(to_hex)?;
let r_min = strip_leading_zeros(r);
let s_min = strip_leading_zeros(s);
let items = vec![
wallet::rlp_uint(nonce),
wallet::rlp_uint(gas_price),
wallet::rlp_uint(gas_limit),
wallet::rlp_bytes(&to_bytes),
wallet::rlp_uint(value),
wallet::rlp_bytes(data),
wallet::rlp_uint(v as u128),
wallet::rlp_bytes(r_min),
wallet::rlp_bytes(s_min),
];
Ok(wallet::rlp_list(&items))
}
fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
let first_nz = bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len() - 1);
&bytes[first_nz..]
}
#[derive(Serialize)]
struct RpcRequest<'a> {
jsonrpc: &'a str,
id: u32,
method: &'a str,
params: serde_json::Value,
}
#[derive(Deserialize)]
struct RpcResponse {
#[serde(default)]
result: Option<String>,
#[serde(default)]
error: Option<RpcError>,
}
#[derive(Deserialize)]
struct RpcError {
#[allow(dead_code)]
code: i64,
message: String,
}
async fn rpc(method: &str, params: serde_json::Value) -> Result<String, String> {
let body = RpcRequest {
jsonrpc: "2.0",
id: 1,
method,
params,
};
let client = reqwest::Client::new();
let resp = client
.post(RPC_URL)
.json(&body)
.send()
.await
.map_err(|e| format!("{method} send: {e}"))?;
let parsed: RpcResponse = resp
.json()
.await
.map_err(|e| format!("{method} decode: {e}"))?;
if let Some(err) = parsed.error {
return Err(format!("{method}: {}", err.message));
}
parsed
.result
.ok_or_else(|| format!("{method} returned no result"))
}
async fn eth_call(to: &str, data_hex: &str) -> Result<String, String> {
rpc(
"eth_call",
serde_json::json!([{ "to": to, "data": data_hex }, "latest"]),
)
.await
}
async fn eth_get_logs(
address: &str,
topics: Vec<serde_json::Value>,
from_block: &str,
) -> Result<Vec<serde_json::Value>, String> {
let result = rpc(
"eth_getLogs",
serde_json::json!([{
"address": address,
"topics": topics,
"fromBlock": from_block,
"toBlock": "latest"
}]),
)
.await?;
let parsed: Vec<serde_json::Value> = serde_json::from_str(
&serde_json::to_string(&result).unwrap_or_default()
).unwrap_or_default();
Ok(parsed)
}
pub async fn tba_signers(tba_hex: &str) -> Result<Vec<String>, String> {
use sha3::{Digest, Keccak256};
let added_topic = format!("0x{}", bytes_to_hex(
&Keccak256::digest(b"SignerAdded(address,address)")
));
let removed_topic = format!("0x{}", bytes_to_hex(
&Keccak256::digest(b"SignerRemoved(address,address)")
));
let added_logs = eth_get_logs(
tba_hex,
vec![serde_json::json!(added_topic)],
"0x0",
).await.unwrap_or_default();
let removed_logs = eth_get_logs(
tba_hex,
vec![serde_json::json!(removed_topic)],
"0x0",
).await.unwrap_or_default();
let mut signers = std::collections::HashSet::new();
for log in &added_logs {
if let Some(topics) = log.get("topics").and_then(|t| t.as_array()) {
if let Some(topic) = topics.get(1).and_then(|t| t.as_str()) {
let addr = format!("0x{}", &topic.trim_start_matches("0x")[24..]);
signers.insert(addr.to_lowercase());
}
}
}
for log in &removed_logs {
if let Some(topics) = log.get("topics").and_then(|t| t.as_array()) {
if let Some(topic) = topics.get(1).and_then(|t| t.as_str()) {
let addr = format!("0x{}", &topic.trim_start_matches("0x")[24..]);
signers.remove(&addr.to_lowercase());
}
}
}
Ok(signers.into_iter().collect())
}
async fn eth_get_transaction_count(addr: &str) -> Result<u128, String> {
let hex = rpc(
"eth_getTransactionCount",
serde_json::json!([addr, "pending"]),
)
.await?;
parse_hex_quantity(&hex)
}
async fn eth_gas_price() -> Result<u128, String> {
let hex = rpc("eth_gasPrice", serde_json::json!([])).await?;
parse_hex_quantity(&hex)
}
async fn eth_estimate_gas(from: &str, to: &str, data_hex: &str) -> Result<u128, String> {
let hex = rpc(
"eth_estimateGas",
serde_json::json!([{ "from": from, "to": to, "data": data_hex }]),
)
.await?;
let estimate = parse_hex_quantity(&hex)?;
Ok(estimate + estimate / 4)
}
async fn eth_send_raw_transaction(raw_hex: &str) -> Result<String, String> {
match rpc("eth_sendRawTransaction", serde_json::json!([raw_hex])).await {
Ok(hash) => Ok(hash),
Err(err) => {
let lower = err.to_lowercase();
let is_duplicate = lower.contains("already known")
|| lower.contains("already exists")
|| lower.contains("nonce too low");
if is_duplicate {
let bytes = hex_to_bytes(raw_hex)?;
let mut hasher = Keccak256::new();
hasher.update(&bytes);
let digest = hasher.finalize();
Ok(format!("0x{}", bytes_to_hex(&digest)))
} else {
Err(err)
}
}
}
}
async fn wait_for_receipt(tx_hash: &str) -> Result<(), String> {
for _ in 0..30 {
let body = RpcRequest {
jsonrpc: "2.0",
id: 1,
method: "eth_getTransactionReceipt",
params: serde_json::json!([tx_hash]),
};
let client = reqwest::Client::new();
let resp = client
.post(RPC_URL)
.json(&body)
.send()
.await
.map_err(|e| format!("receipt poll: {e}"))?;
let json: serde_json::Value = resp
.json()
.await
.map_err(|e| format!("receipt parse: {e}"))?;
if let Some(receipt) = json.get("result").filter(|v| !v.is_null()) {
let status = receipt
.get("status")
.and_then(|s| s.as_str())
.unwrap_or("");
if status == "0x1" {
return Ok(());
} else if status == "0x0" {
return Err(format!("tx reverted: {tx_hash}"));
}
}
sleep_ms(1000).await;
}
Err(format!("receipt timeout for {tx_hash}"))
}
#[cfg(not(target_arch = "wasm32"))]
async fn sleep_ms(ms: u32) {
tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
}
#[cfg(target_arch = "wasm32")]
async fn sleep_ms(ms: u32) {
use wasm_bindgen_futures::JsFuture;
let promise = js_sys::Promise::new(&mut |resolve, _| {
if let Some(window) = web_sys::window() {
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
&resolve,
ms as i32,
);
}
});
let _ = JsFuture::from(promise).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selector_matches_known_value() {
let sel = selector("idOfName(string)");
let hex: String = sel.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(hex, "127c388a");
}
#[test]
fn encode_short_name_layout() {
let cd = encode_id_of_name("abc");
assert!(cd.starts_with("0x127c388a"));
assert_eq!(cd.len(), 2 + (4 + 32 + 32 + 32) * 2);
}
#[test]
fn decode_zero_means_available() {
let z = format!("0x{}", "0".repeat(64));
assert_eq!(decode_u256_as_u64(&z).unwrap(), 0);
}
#[test]
fn decode_normal_id() {
let mut s = "0".repeat(63);
s.push('7');
let hex = format!("0x{s}");
assert_eq!(decode_u256_as_u64(&hex).unwrap(), 7);
}
#[test]
fn decode_oversize_errors() {
let mut s = String::from("1");
s.push_str(&"0".repeat(63));
let hex = format!("0x{s}");
assert!(decode_u256_as_u64(&hex).is_err());
}
}