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 = "0x6c31c01e10C44f4813FffDC7D5e671c1b26Da30c";
pub const CHAIN_ID: u64 = 42431;
pub const BOOTSTRAP_FAUCET_ADDRESS: &str = "0x0000000000000000000000000000000000000000";
pub const LOCALHARNESS_TOKEN_ADDRESS: &str = "0x90B84c7234Aae89BadA7f69160B9901B9bc37B17";
#[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?;
if total <= 1 {
return Ok(Vec::new());
}
let owner_lower = owner_hex.to_lowercase();
let owner_calls: Vec<(&str, String)> = (1..total)
.map(|id| (REGISTRY_ADDRESS, call_uint("ownerOfId(uint256)", id)))
.collect();
let owners = eth_call_batch(&owner_calls).await?;
let my_ids: Vec<u64> = owners
.iter()
.enumerate()
.filter_map(|(i, res)| {
let addr = decode_address(res.as_ref().ok()?)?;
(addr == owner_lower).then_some((i as u64) + 1)
})
.collect();
if my_ids.is_empty() {
return Ok(Vec::new());
}
let name_calls: Vec<(&str, String)> = my_ids
.iter()
.map(|&id| (REGISTRY_ADDRESS, call_uint("nameOfId(uint256)", id)))
.collect();
let tba_calls: Vec<(&str, String)> = my_ids
.iter()
.map(|&id| (REGISTRY_ADDRESS, call_uint("tokenBoundAccount(uint256)", id)))
.collect();
let names = eth_call_batch(&name_calls).await?;
let tbas = eth_call_batch(&tba_calls).await?;
let mut out: Vec<OwnedToken> = Vec::with_capacity(my_ids.len());
for (k, &id) in my_ids.iter().enumerate() {
let name = names
.get(k)
.and_then(|r| r.as_ref().ok())
.and_then(|h| decode_string(h))
.unwrap_or_default();
if name.is_empty() {
continue;
}
let tba = tbas
.get(k)
.and_then(|r| r.as_ref().ok())
.and_then(|h| decode_address(h));
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)
}
pub async fn subdomain_count() -> Result<u64, String> {
Ok(next_id().await?.saturating_sub(1))
}
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 id_of_name(name: &str) -> Result<u64, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let calldata = encode_id_of_name(name);
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
decode_u256_as_u64(&result_hex)
}
pub async fn list_recent_agents(limit: u64) -> Result<Vec<(u64, String)>, String> {
let next = next_id().await?;
if next <= 1 {
return Ok(Vec::new());
}
let max_id = next - 1;
let start = max_id.saturating_sub(limit.saturating_sub(1)).max(1);
let mut out = Vec::new();
let mut id = max_id;
loop {
if let Ok(name) = name_of_id(id).await {
if !name.is_empty() {
out.push((id, name));
}
}
if id <= start {
break;
}
id -= 1;
}
Ok(out)
}
fn app_metadata_key() -> [u8; 32] {
use sha3::{Digest, Keccak256};
let digest = Keccak256::digest(b"localharness.app.wasm");
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
pub async fn app_wasm_of(token_id: u64) -> Result<Option<Vec<u8>>, String> {
let key = app_metadata_key();
let mut data = Vec::with_capacity(4 + 64);
data.extend_from_slice(&selector("metadata(uint256,bytes32)"));
data.extend_from_slice(&u256_be(token_id as u128));
data.extend_from_slice(&key);
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let bytes = hex_to_bytes(&result_hex)?;
if bytes.len() < 64 {
return Ok(None);
}
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&bytes[56..64]);
let len = u64::from_be_bytes(len_buf) as usize;
if len == 0 {
return Ok(None);
}
let payload = bytes
.get(64..64 + len)
.ok_or_else(|| "app wasm truncated".to_string())?;
Ok(Some(payload.to_vec()))
}
pub fn encode_set_app_wasm(token_id: u64, wasm: &[u8]) -> Vec<u8> {
let key = app_metadata_key();
let len = wasm.len();
let padded = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 96 + 32 + padded);
buf.extend_from_slice(&selector("setMetadata(uint256,bytes32,bytes)"));
buf.extend_from_slice(&u256_be(token_id as u128)); buf.extend_from_slice(&key); buf.extend_from_slice(&u256_be(0x60)); buf.extend_from_slice(&u256_be(len as u128)); buf.extend_from_slice(wasm);
buf.resize(4 + 96 + 32 + padded, 0); buf
}
fn gemini_key_metadata_key() -> [u8; 32] {
use sha3::{Digest, Keccak256};
let digest = Keccak256::digest(b"localharness.gemini_key.enc");
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
pub async fn gemini_key_of(token_id: u64) -> Result<Option<Vec<u8>>, String> {
let mut data = Vec::with_capacity(4 + 64);
data.extend_from_slice(&selector("metadata(uint256,bytes32)"));
data.extend_from_slice(&u256_be(token_id as u128));
data.extend_from_slice(&gemini_key_metadata_key());
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let bytes = hex_to_bytes(&result_hex)?;
if bytes.len() < 64 {
return Ok(None);
}
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&bytes[56..64]);
let len = u64::from_be_bytes(len_buf) as usize;
if len == 0 {
return Ok(None);
}
let payload = bytes
.get(64..64 + len)
.ok_or_else(|| "gemini key ciphertext truncated".to_string())?;
Ok(Some(payload.to_vec()))
}
pub fn encode_set_gemini_key(token_id: u64, ciphertext: &[u8]) -> Vec<u8> {
let key = gemini_key_metadata_key();
let len = ciphertext.len();
let padded = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 96 + 32 + padded);
buf.extend_from_slice(&selector("setMetadata(uint256,bytes32,bytes)"));
buf.extend_from_slice(&u256_be(token_id as u128));
buf.extend_from_slice(&key);
buf.extend_from_slice(&u256_be(0x60));
buf.extend_from_slice(&u256_be(len as u128));
buf.extend_from_slice(ciphertext);
buf.resize(4 + 96 + 32 + padded, 0);
buf
}
fn keccak_key(label: &[u8]) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let digest = Keccak256::digest(label);
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
async fn metadata_bytes_of(token_id: u64, key: [u8; 32]) -> Result<Option<Vec<u8>>, String> {
let mut data = Vec::with_capacity(4 + 64);
data.extend_from_slice(&selector("metadata(uint256,bytes32)"));
data.extend_from_slice(&u256_be(token_id as u128));
data.extend_from_slice(&key);
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let bytes = hex_to_bytes(&result_hex)?;
if bytes.len() < 64 {
return Ok(None);
}
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&bytes[56..64]);
let len = u64::from_be_bytes(len_buf) as usize;
if len == 0 {
return Ok(None);
}
let payload = bytes
.get(64..64 + len)
.ok_or_else(|| "metadata truncated".to_string())?;
Ok(Some(payload.to_vec()))
}
fn encode_set_metadata_bytes(token_id: u64, key: [u8; 32], payload: &[u8]) -> Vec<u8> {
let len = payload.len();
let padded = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 96 + 32 + padded);
buf.extend_from_slice(&selector("setMetadata(uint256,bytes32,bytes)"));
buf.extend_from_slice(&u256_be(token_id as u128));
buf.extend_from_slice(&key);
buf.extend_from_slice(&u256_be(0x60));
buf.extend_from_slice(&u256_be(len as u128));
buf.extend_from_slice(payload);
buf.resize(4 + 96 + 32 + padded, 0);
buf
}
const PUBLIC_FACE_LABEL: &[u8] = b"localharness.public_face";
const PUBLIC_HTML_LABEL: &[u8] = b"localharness.public.html";
pub async fn public_face_of(token_id: u64) -> Result<Option<String>, String> {
match metadata_bytes_of(token_id, keccak_key(PUBLIC_FACE_LABEL)).await? {
Some(b) => Ok(String::from_utf8(b)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())),
None => Ok(None),
}
}
pub fn encode_set_public_face(token_id: u64, choice: &str) -> Vec<u8> {
encode_set_metadata_bytes(token_id, keccak_key(PUBLIC_FACE_LABEL), choice.as_bytes())
}
pub async fn public_html_of(token_id: u64) -> Result<Option<Vec<u8>>, String> {
metadata_bytes_of(token_id, keccak_key(PUBLIC_HTML_LABEL)).await
}
pub fn encode_set_public_html(token_id: u64, html: &[u8]) -> Vec<u8> {
encode_set_metadata_bytes(token_id, keccak_key(PUBLIC_HTML_LABEL), html)
}
const PERSONA_LABEL: &[u8] = b"localharness.persona";
pub async fn persona_of(token_id: u64) -> Result<Option<String>, String> {
match metadata_bytes_of(token_id, keccak_key(PERSONA_LABEL)).await? {
Some(b) => Ok(String::from_utf8(b)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())),
None => Ok(None),
}
}
pub fn encode_set_persona(token_id: u64, persona: &str) -> Vec<u8> {
encode_set_metadata_bytes(token_id, keccak_key(PERSONA_LABEL), persona.as_bytes())
}
const CAPABILITY_LABEL: &[u8] = b"localharness.capability";
pub fn encode_set_capability(token_id: u64, payload: &[u8]) -> Vec<u8> {
let commitment = keccak_key(payload); encode_set_metadata_bytes(token_id, keccak_key(CAPABILITY_LABEL), &commitment)
}
pub async fn capability_descriptor_of(token_id: u64) -> Result<Option<[u8; 32]>, String> {
match metadata_bytes_of(token_id, keccak_key(CAPABILITY_LABEL)).await? {
Some(b) if b.len() == 32 => {
let mut out = [0u8; 32];
out.copy_from_slice(&b);
Ok(Some(out))
}
Some(_) => Err("capability commitment is not 32 bytes".to_string()),
None => Ok(None),
}
}
pub async fn verify_descriptor(token_id: u64, served_payload: &[u8]) -> Result<bool, String> {
match capability_descriptor_of(token_id).await? {
Some(commitment) => Ok(keccak_key(served_payload) == commitment),
None => Ok(false),
}
}
pub const CREDIT_PROXY_URL: &str = "https://proxy-tau-ten-15.vercel.app/";
pub fn proxy_auth_token(signer: &SigningKey, now_secs: u64) -> String {
let addr = format!("0x{}", bytes_to_hex(&crate::wallet::address(signer)));
let msg = format!("localharness-proxy:{addr}:{now_secs}");
let sig = crate::wallet::personal_sign(signer, msg.as_bytes());
format!("{addr}:{now_secs}:0x{}", bytes_to_hex(&sig))
}
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 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 mut calldata = Vec::with_capacity(36);
calldata.extend_from_slice(&selector("balanceOf(address)"));
calldata.extend_from_slice(&padded);
let calldata_hex = format!("0x{}", bytes_to_hex(&calldata));
let result = eth_call(token_hex, &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 tba_token_id_of(tba_hex: &str) -> Result<u64, String> {
let calldata = format!("0x{}", bytes_to_hex(&selector("token()")));
let result = eth_call(tba_hex, &calldata).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 96 {
return Err("token(): short response".into());
}
let mut buf = [0u8; 8];
buf.copy_from_slice(&bytes[88..96]); Ok(u64::from_be_bytes(buf))
}
pub async fn tba_execute_batch_sponsored(
signer: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
tba_hex: &str,
targets: &[([u8; 20], Vec<u8>)],
fee_token: &str,
gas_limit: u128,
) -> Result<String, String> {
let diamond = parse_eth_address(REGISTRY_ADDRESS)?;
let tba = parse_eth_address(tba_hex)?;
let mut calls = Vec::with_capacity(targets.len() + 1);
calls.push(crate::tempo_tx::TempoCall {
to: diamond,
value_wei: 0,
input: encode_create_tba(token_id),
});
for (target, data) in targets {
calls.push(crate::tempo_tx::TempoCall {
to: tba,
value_wei: 0,
input: encode_tba_execute(target, 0, data),
});
}
submit_tempo_sponsored(signer, fee_payer, calls, fee_token, gas_limit).await
}
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),
};
let link_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_link_device(token_id, &new_signer),
};
submit_tempo_sponsored(
sender,
fee_payer,
vec![create_call, add_call, link_call],
fee_token,
2_200_000,
)
.await
}
fn encode_link_device(main_id: u64, device: &[u8; 20]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 64);
out.extend_from_slice(&selector("linkDevice(uint256,address)"));
out.extend_from_slice(&u256_be(main_id as u128));
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(device);
out.extend_from_slice(&padded);
out
}
pub async fn devices_of(main_id: u64) -> Result<Vec<String>, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(Vec::new());
}
let mut calldata = selector("devicesOf(uint256)").to_vec();
calldata.extend_from_slice(&u256_be(main_id as u128));
let calldata_hex = format!("0x{}", bytes_to_hex(&calldata));
let result = eth_call(REGISTRY_ADDRESS, &calldata_hex).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 64 {
return Ok(Vec::new());
}
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&bytes[56..64]); let len = u64::from_be_bytes(len_buf) as usize;
let mut out = Vec::with_capacity(len);
for i in 0..len {
let start = 64 + i * 32;
if start + 32 > bytes.len() {
break;
}
out.push(format!("0x{}", bytes_to_hex(&bytes[start + 12..start + 32])));
}
Ok(out)
}
pub async fn is_device_linked(main_id: u64, addr_hex: &str) -> Result<bool, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(false);
}
let addr = parse_eth_address(addr_hex)?;
let mut calldata = selector("isDeviceLinked(uint256,address)").to_vec();
calldata.extend_from_slice(&u256_be(main_id as u128));
calldata.extend_from_slice(&addr_word(&addr));
let calldata_hex = format!("0x{}", bytes_to_hex(&calldata));
let result = eth_call(REGISTRY_ADDRESS, &calldata_hex).await?;
decode_u256_as_u64(&result).map(|v| v != 0)
}
fn encode_unlink_device(main_id: u64, device: &[u8; 20]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 64);
out.extend_from_slice(&selector("unlinkDevice(uint256,address)"));
out.extend_from_slice(&u256_be(main_id as u128));
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(device);
out.extend_from_slice(&padded);
out
}
fn encode_erc721_transfer_from(from: &[u8; 20], to: &[u8; 20], token_id: u64) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 96);
out.extend_from_slice(&selector("transferFrom(address,address,uint256)"));
out.extend_from_slice(&addr_word(from));
out.extend_from_slice(&addr_word(to));
out.extend_from_slice(&u256_be(token_id as u128));
out
}
pub async fn consolidate_into_main_sponsored(
owner: &SigningKey,
fee_payer: &SigningKey,
main_tba_hex: &str,
token_ids: &[u64],
fee_token: &str,
) -> Result<String, String> {
if token_ids.is_empty() {
return Err("no subdomains to consolidate".into());
}
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let to = parse_eth_address(main_tba_hex)?;
let from = wallet::address(owner);
let calls: Vec<_> = token_ids
.iter()
.map(|&tid| crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_erc721_transfer_from(&from, &to, tid),
})
.collect();
let gas = 300_000 + token_ids.len() as u128 * 90_000;
submit_tempo_sponsored(owner, fee_payer, calls, fee_token, gas).await
}
fn encode_release_name(token_id: u64) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 32);
out.extend_from_slice(&selector("releaseName(uint256)"));
out.extend_from_slice(&u256_be(token_id as u128));
out
}
pub fn release_name_calldata(token_id: u64) -> Vec<u8> {
encode_release_name(token_id)
}
pub fn register_calldata(name: &str) -> Vec<u8> {
hex_to_bytes(&encode_register(name)).unwrap_or_default()
}
pub async fn release_name_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
fee_token: &str,
) -> Result<String, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_release_name(token_id),
};
submit_tempo_sponsored(sender, fee_payer, vec![call], fee_token, 1_000_000).await
}
pub async fn release_names_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_ids: &[u64],
fee_token: &str,
) -> Result<String, String> {
if token_ids.is_empty() {
return Err("no subdomains to release".into());
}
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let calls: Vec<_> = token_ids
.iter()
.map(|&tid| crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_release_name(tid),
})
.collect();
let gas = 1_000_000 + (token_ids.len() as u128).saturating_sub(1) * 250_000;
submit_tempo_sponsored(sender, fee_payer, calls, fee_token, gas).await
}
pub async fn remove_signer_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
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 diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let remove_call = crate::tempo_tx::TempoCall {
to: tba_addr,
value_wei: 0,
input: encode_remove_signer(&signer_addr),
};
let unlink_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_unlink_device(token_id, &signer_addr),
};
submit_tempo_sponsored(sender, fee_payer, vec![remove_call, unlink_call], fee_token, 600_000)
.await
}
pub fn pairing_code_hash(code: &str) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let mut out = [0u8; 32];
out.copy_from_slice(&Keccak256::digest(code.trim().as_bytes()));
out
}
pub async fn announce_pairing_sponsored(
device: &SigningKey,
fee_payer: &SigningKey,
code_hash: &[u8; 32],
pubkey: &[u8],
fee_token: &str,
) -> Result<String, String> {
let padded = pubkey.len().div_ceil(32) * 32;
let mut input = Vec::with_capacity(4 + 32 + 32 + 32 + padded);
input.extend_from_slice(&selector("announcePairing(bytes32,bytes)"));
input.extend_from_slice(code_hash);
input.extend_from_slice(&u256_be(0x40));
input.extend_from_slice(&u256_be(pubkey.len() as u128));
input.extend_from_slice(pubkey);
input.resize(4 + 32 + 32 + 32 + padded, 0);
let call = crate::tempo_tx::TempoCall {
to: parse_eth_address(REGISTRY_ADDRESS)?,
value_wei: 0,
input,
};
submit_tempo_sponsored(device, fee_payer, vec![call], fee_token, 450_000).await
}
pub async fn find_pairing_device(
code_hash: &[u8; 32],
) -> Result<Option<(String, Vec<u8>)>, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(None);
}
use sha3::{Digest, Keccak256};
let topic0 = format!(
"0x{}",
bytes_to_hex(&Keccak256::digest(
b"PairingAnnounced(bytes32,address,bytes,uint256)"
))
);
let code_topic = format!("0x{}", bytes_to_hex(code_hash));
let latest_hex = rpc("eth_blockNumber", serde_json::json!([])).await?;
let latest = parse_hex_quantity(&latest_hex)? as u64;
let from = latest.saturating_sub(99_000);
let from_hex = format!("0x{from:x}");
let logs = eth_get_logs(
REGISTRY_ADDRESS,
vec![serde_json::json!(topic0), serde_json::json!(code_topic)],
&from_hex,
)
.await?;
for log in &logs {
let topics = log.get("topics").and_then(|t| t.as_array());
let device = topics
.and_then(|t| t.get(2))
.and_then(|t| t.as_str())
.map(|s| s.trim_start_matches("0x"))
.filter(|s| s.len() >= 64)
.map(|s| format!("0x{}", &s[24..]).to_lowercase());
let Some(device) = device else { continue };
let data_hex = log.get("data").and_then(|d| d.as_str()).unwrap_or("0x");
let data = hex_to_bytes(data_hex).unwrap_or_default();
let pubkey = if data.len() >= 96 {
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&data[88..96]);
let len = u64::from_be_bytes(len_buf) as usize;
data.get(96..96 + len).map(|s| s.to_vec()).unwrap_or_default()
} else {
Vec::new()
};
return Ok(Some((device, pubkey)));
}
Ok(None)
}
fn gemini_key_dev_metadata_key(device_addr: &[u8; 20]) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let mut hasher = Keccak256::new();
hasher.update(b"localharness.gemini_key.dev.");
hasher.update(device_addr);
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
pub async fn wrapped_device_key_of(
token_id: u64,
device_addr_hex: &str,
) -> Result<Option<Vec<u8>>, String> {
let device_addr = parse_eth_address(device_addr_hex)?;
let mut data = Vec::with_capacity(4 + 64);
data.extend_from_slice(&selector("metadata(uint256,bytes32)"));
data.extend_from_slice(&u256_be(token_id as u128));
data.extend_from_slice(&gemini_key_dev_metadata_key(&device_addr));
let calldata = format!("0x{}", bytes_to_hex(&data));
let result_hex = eth_call(REGISTRY_ADDRESS, &calldata).await?;
let bytes = hex_to_bytes(&result_hex)?;
if bytes.len() < 64 {
return Ok(None);
}
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&bytes[56..64]);
let len = u64::from_be_bytes(len_buf) as usize;
if len == 0 {
return Ok(None);
}
let payload = bytes
.get(64..64 + len)
.ok_or_else(|| "wrapped key truncated".to_string())?;
Ok(Some(payload.to_vec()))
}
pub async fn set_device_wrapped_key_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
device_addr_hex: &str,
blob: &[u8],
fee_token: &str,
) -> Result<String, String> {
let device_addr = parse_eth_address(device_addr_hex)?;
let key = gemini_key_dev_metadata_key(&device_addr);
let len = blob.len();
let padded = len.div_ceil(32) * 32;
let mut input = Vec::with_capacity(4 + 96 + 32 + padded);
input.extend_from_slice(&selector("setMetadata(uint256,bytes32,bytes)"));
input.extend_from_slice(&u256_be(token_id as u128));
input.extend_from_slice(&key);
input.extend_from_slice(&u256_be(0x60));
input.extend_from_slice(&u256_be(len as u128));
input.extend_from_slice(blob);
input.resize(4 + 96 + 32 + padded, 0);
let call = crate::tempo_tx::TempoCall {
to: parse_eth_address(REGISTRY_ADDRESS)?,
value_wei: 0,
input,
};
let words = (padded / 32 + 4) as u128;
submit_tempo_sponsored(sender, fee_payer, vec![call], fee_token, 1_200_000 + words * 40_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)
}
#[allow(clippy::too_many_arguments)]
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,
2_000_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_input = hex_to_bytes(&encode_register(name))?;
let register_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: register_input,
};
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)
}
fn encode_redeem(code: &str) -> Vec<u8> {
let sel = selector("redeem(string)");
let bytes = code.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);
buf
}
pub async fn redeem_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
code: &str,
fee_token: &str,
) -> Result<String, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_redeem(code),
};
submit_tempo_sponsored(sender, fee_payer, vec![call], fee_token, 2_000_000).await
}
pub async fn session_expiry_of(account_hex: &str) -> Result<u64, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let account = parse_eth_address(account_hex)?;
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(&account);
let mut calldata = Vec::with_capacity(4 + 32);
calldata.extend_from_slice(&selector("sessionExpiryOf(address)"));
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 session_price() -> Result<u128, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let calldata = format!("0x{}", bytes_to_hex(&selector("sessionPrice()")));
let result = eth_call(REGISTRY_ADDRESS, &calldata).await?;
decode_u256_as_u128(&result)
}
pub async fn open_session_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
fee_token: &str,
) -> Result<String, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let token_addr = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS)?;
let price = session_price().await.unwrap_or(0);
let open_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: selector("openSession()").to_vec(),
};
let calls = if price > 0 {
let approve_call = crate::tempo_tx::TempoCall {
to: token_addr,
value_wei: 0,
input: encode_approve(&diamond_addr, price),
};
vec![approve_call, open_call]
} else {
vec![open_call]
};
submit_tempo_sponsored(sender, fee_payer, calls, fee_token, 600_000).await
}
fn encode_deposit_credits(amount_wei: u128) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 32);
out.extend_from_slice(&selector("depositCredits(uint256)"));
out.extend_from_slice(&u256_be(amount_wei));
out
}
pub async fn credit_balance_of(account_hex: &str) -> Result<u128, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(0);
}
let account = parse_eth_address(account_hex)?;
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(&account);
let mut calldata = Vec::with_capacity(4 + 32);
calldata.extend_from_slice(&selector("creditOf(address)"));
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_u128(&result)
}
pub async fn deposit_credits_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
amount_wei: u128,
fee_token: &str,
) -> Result<String, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let token_addr = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS)?;
let approve_call = crate::tempo_tx::TempoCall {
to: token_addr,
value_wei: 0,
input: encode_approve(&diamond_addr, amount_wei),
};
let deposit_call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_deposit_credits(amount_wei),
};
submit_tempo_sponsored(sender, fee_payer, vec![approve_call, deposit_call], fee_token, 1_500_000)
.await
}
fn keccak32(data: &[u8]) -> [u8; 32] {
let mut h = Keccak256::new();
h.update(data);
let d = h.finalize();
let mut o = [0u8; 32];
o.copy_from_slice(&d);
o
}
fn addr_word(a: &[u8; 20]) -> [u8; 32] {
let mut w = [0u8; 32];
w[12..].copy_from_slice(a);
w
}
pub fn x402_domain_separator() -> Result<[u8; 32], String> {
let diamond = parse_eth_address(REGISTRY_ADDRESS)?;
let mut dom = Vec::with_capacity(160);
dom.extend_from_slice(&keccak32(
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
));
dom.extend_from_slice(&keccak32(b"localharness-x402"));
dom.extend_from_slice(&keccak32(b"1"));
dom.extend_from_slice(&u256_be(CHAIN_ID as u128));
dom.extend_from_slice(&addr_word(&diamond));
Ok(keccak32(&dom))
}
pub fn x402_digest(
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
) -> Result<[u8; 32], String> {
let mut st = Vec::with_capacity(224);
st.extend_from_slice(&keccak32(
b"PaymentAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)",
));
st.extend_from_slice(&addr_word(from));
st.extend_from_slice(&addr_word(to));
st.extend_from_slice(&u256_be(value_wei));
st.extend_from_slice(&u256_be(valid_after as u128));
st.extend_from_slice(&u256_be(valid_before as u128));
st.extend_from_slice(nonce);
let struct_hash = keccak32(&st);
let mut pre = Vec::with_capacity(66);
pre.extend_from_slice(&[0x19, 0x01]);
pre.extend_from_slice(&x402_domain_separator()?);
pre.extend_from_slice(&struct_hash);
Ok(keccak32(&pre))
}
pub fn sign_x402(
signer: &SigningKey,
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
) -> Result<[u8; 65], String> {
let digest = x402_digest(from, to, value_wei, valid_after, valid_before, nonce)?;
Ok(crate::wallet::sign_hash(signer, &digest))
}
fn encode_settle(
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
signature: &[u8; 65],
) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 32 * 9 + 96);
out.extend_from_slice(&selector(
"settle(address,address,uint256,uint256,uint256,bytes32,bytes)",
));
out.extend_from_slice(&addr_word(from));
out.extend_from_slice(&addr_word(to));
out.extend_from_slice(&u256_be(value_wei));
out.extend_from_slice(&u256_be(valid_after as u128));
out.extend_from_slice(&u256_be(valid_before as u128));
out.extend_from_slice(nonce);
out.extend_from_slice(&u256_be(7 * 32)); out.extend_from_slice(&u256_be(signature.len() as u128)); out.extend_from_slice(signature);
out.resize(out.len() + 31, 0); out
}
#[allow(clippy::too_many_arguments)]
pub async fn settle_x402_sponsored(
submitter: &SigningKey,
fee_payer: &SigningKey,
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
signature: &[u8; 65],
fee_token: &str,
) -> Result<String, String> {
let diamond_addr = parse_eth_address(REGISTRY_ADDRESS)?;
let call = crate::tempo_tx::TempoCall {
to: diamond_addr,
value_wei: 0,
input: encode_settle(from, to, value_wei, valid_after, valid_before, nonce, signature),
};
submit_tempo_sponsored(submitter, fee_payer, vec![call], fee_token, 400_000).await
}
pub async fn x402_authorization_state(
from_hex: &str,
nonce: &[u8; 32],
) -> Result<bool, String> {
if REGISTRY_ADDRESS == zero_address() {
return Ok(false);
}
let from = parse_eth_address(from_hex)?;
let mut calldata = Vec::with_capacity(4 + 64);
calldata.extend_from_slice(&selector("authorizationState(address,bytes32)"));
calldata.extend_from_slice(&addr_word(&from));
calldata.extend_from_slice(nonce);
let calldata_hex = format!("0x{}", bytes_to_hex(&calldata));
let result = eth_call(REGISTRY_ADDRESS, &calldata_hex).await?;
Ok(decode_u256_as_u64(&result).map(|v| v != 0).unwrap_or(false))
}
#[cfg(test)]
mod x402_tests {
use super::*;
#[test]
fn x402_domain_matches_live_facet() {
let expected =
"54530933a67f96286ac528dbff39d00c0ea49f4c6bd0f034343a0c78927f0b7a";
let got = x402_domain_separator().unwrap();
assert_eq!(bytes_to_hex(&got), expected);
}
#[test]
fn x402_sign_recovers_payer() {
let w = crate::wallet::generate();
let from = w.address;
let to = [0x11u8; 20];
let nonce = [0x22u8; 32];
let sig = sign_x402(&w.signer, &from, &to, 1_000, 0, 9_999_999_999, &nonce).unwrap();
let digest = x402_digest(&from, &to, 1_000, 0, 9_999_999_999, &nonce).unwrap();
let recovered = crate::wallet::recover_address(&sig, &digest).unwrap();
assert_eq!(recovered, from);
}
#[test]
fn erc721_transfer_from_calldata_layout() {
let from = [0xAAu8; 20];
let to = [0xBBu8; 20];
let cd = encode_erc721_transfer_from(&from, &to, 0x1234);
assert_eq!(&cd[0..4], &[0x23, 0xb8, 0x72, 0xdd]);
assert_eq!(cd.len(), 4 + 96);
assert_eq!(&cd[4 + 12..4 + 32], &from); assert_eq!(&cd[4 + 44..4 + 64], &to); assert_eq!(u64::from_be_bytes(cd[4 + 88..4 + 96].try_into().unwrap()), 0x1234);
}
#[test]
fn release_name_calldata_layout() {
let cd = encode_release_name(7);
assert_eq!(&cd[0..4], &selector("releaseName(uint256)"));
assert_eq!(cd.len(), 36);
assert_eq!(u64::from_be_bytes(cd[28..36].try_into().unwrap()), 7);
}
#[test]
fn link_unlink_device_calldata_layout() {
let dev = [0xCDu8; 20];
let link = encode_link_device(3, &dev);
assert_eq!(&link[0..4], &selector("linkDevice(uint256,address)"));
assert_eq!(link.len(), 68);
assert_eq!(u64::from_be_bytes(link[28..36].try_into().unwrap()), 3); assert_eq!(&link[36 + 12..36 + 32], &dev); let unlink = encode_unlink_device(3, &dev);
assert_eq!(&unlink[0..4], &selector("unlinkDevice(uint256,address)"));
assert_eq!(unlink.len(), 68);
assert_eq!(&unlink[36 + 12..36 + 32], &dev);
}
#[test]
fn deposit_credits_calldata_layout() {
let cd = encode_deposit_credits(1_000_000_000_000_000_000);
assert_eq!(&cd[0..4], &selector("depositCredits(uint256)"));
assert_eq!(cd.len(), 36);
}
}
#[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<serde_json::Value>,
#[serde(default)]
error: Option<RpcError>,
}
#[derive(Deserialize)]
struct RpcError {
#[allow(dead_code)]
code: i64,
message: String,
}
async fn rpc_value(method: &str, params: serde_json::Value) -> Result<serde_json::Value, 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 rpc(method: &str, params: serde_json::Value) -> Result<String, String> {
let value = rpc_value(method, params).await?;
value
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| format!("{method}: expected string 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
}
fn call_uint(sig: &str, id: u64) -> String {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector(sig));
data.extend_from_slice(&u256_be(id as u128));
format!("0x{}", bytes_to_hex(&data))
}
fn decode_address(result_hex: &str) -> Option<String> {
let trimmed = result_hex.trim().trim_start_matches("0x");
if trimmed.len() < 64 {
return None;
}
let addr_hex = &trimmed[trimmed.len() - 40..];
if addr_hex.chars().all(|c| c == '0') {
return None;
}
Some(format!("0x{}", addr_hex.to_lowercase()))
}
fn decode_string(result_hex: &str) -> Option<String> {
let raw = hex_to_bytes(result_hex).ok()?;
if raw.len() < 64 {
return None;
}
let len = u64::from_be_bytes(raw[56..64].try_into().ok()?) as usize;
if raw.len() < 64 + len {
return None;
}
String::from_utf8(raw[64..64 + len].to_vec()).ok()
}
async fn eth_call_batch(calls: &[(&str, String)]) -> Result<Vec<Result<String, String>>, String> {
if calls.is_empty() {
return Ok(Vec::new());
}
let batch: Vec<serde_json::Value> = calls
.iter()
.enumerate()
.map(|(i, (to, data))| {
serde_json::json!({
"jsonrpc": "2.0",
"id": i,
"method": "eth_call",
"params": [{ "to": to, "data": data }, "latest"],
})
})
.collect();
let client = reqwest::Client::new();
let resp = client
.post(RPC_URL)
.json(&serde_json::Value::Array(batch))
.send()
.await
.map_err(|e| format!("eth_call batch send: {e}"))?;
let parsed: Vec<serde_json::Value> = resp
.json()
.await
.map_err(|e| format!("eth_call batch decode: {e}"))?;
let mut out: Vec<Result<String, String>> = (0..calls.len())
.map(|_| Err("missing batch response".to_string()))
.collect();
for item in parsed {
let Some(idx) = item.get("id").and_then(|v| v.as_u64()).map(|i| i as usize) else {
continue;
};
if idx >= out.len() {
continue;
}
if let Some(err) = item.get("error") {
let msg = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("rpc error");
out[idx] = Err(msg.to_string());
} else if let Some(result) = item.get("result").and_then(|r| r.as_str()) {
out[idx] = Ok(result.to_string());
}
}
Ok(out)
}
async fn eth_get_logs(
address: &str,
topics: Vec<serde_json::Value>,
from_block: &str,
) -> Result<Vec<serde_json::Value>, String> {
let result = rpc_value(
"eth_getLogs",
serde_json::json!([{
"address": address,
"topics": topics,
"fromBlock": from_block,
"toBlock": "latest"
}]),
)
.await?;
match result {
serde_json::Value::Array(logs) => Ok(logs),
_ => Ok(Vec::new()),
}
}
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())
}
#[derive(Debug, Clone)]
pub struct FeedbackEntry {
pub sender: String,
pub timestamp: u64,
pub text: String,
}
pub fn encode_submit_feedback(text: &str) -> Vec<u8> {
let bytes = text.as_bytes();
let len = bytes.len();
let padded = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 64 + padded);
buf.extend_from_slice(&selector("submitFeedback(string)"));
buf.extend_from_slice(&u256_be(0x20));
buf.extend_from_slice(&u256_be(len as u128));
buf.extend_from_slice(bytes);
buf.resize(4 + 64 + padded, 0);
buf
}
pub async fn submit_feedback_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
text: &str,
fee_token: &str,
) -> Result<String, String> {
let diamond = parse_eth_address(REGISTRY_ADDRESS)?;
let call = crate::tempo_tx::TempoCall {
to: diamond,
value_wei: 0,
input: encode_submit_feedback(text),
};
let gas = 1_500_000u128 + (text.len() as u128) * 9_000;
submit_tempo_sponsored(sender, fee_payer, vec![call], fee_token, gas).await
}
pub async fn list_feedback() -> Result<Vec<FeedbackEntry>, String> {
use sha3::{Digest, Keccak256};
let topic0 = format!(
"0x{}",
bytes_to_hex(&Keccak256::digest(b"FeedbackSubmitted(address,uint256,string)"))
);
let latest_hex = rpc("eth_blockNumber", serde_json::json!([])).await?;
let latest = parse_hex_quantity(&latest_hex)? as u64;
let from = latest.saturating_sub(99_000);
let from_hex = format!("0x{from:x}");
let logs = eth_get_logs(REGISTRY_ADDRESS, vec![serde_json::json!(topic0)], &from_hex).await?;
let mut out = Vec::new();
for log in &logs {
let sender = log
.get("topics")
.and_then(|t| t.as_array())
.and_then(|t| t.get(1))
.and_then(|t| t.as_str())
.map(|t| format!("0x{}", &t.trim_start_matches("0x")[24..]).to_lowercase())
.unwrap_or_default();
let Some(data_hex) = log.get("data").and_then(|d| d.as_str()) else {
continue;
};
let Ok(bytes) = hex_to_bytes(data_hex) else { continue };
if let Some(entry) = decode_feedback_data(&bytes, sender) {
out.push(entry);
}
}
out.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
Ok(out)
}
fn decode_feedback_data(bytes: &[u8], sender: String) -> Option<FeedbackEntry> {
if bytes.len() < 96 {
return None;
}
let mut ts = [0u8; 8];
ts.copy_from_slice(&bytes[24..32]); let timestamp = u64::from_be_bytes(ts);
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&bytes[88..96]); let len = u64::from_be_bytes(len_buf) as usize;
let text_bytes = bytes.get(96..96 + len)?;
let text = String::from_utf8_lossy(text_bytes).into_owned();
Some(FeedbackEntry { sender, timestamp, text })
}
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"))]
pub async fn sleep_ms(ms: u32) {
tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
}
#[cfg(target_arch = "wasm32")]
pub 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 proxy_auth_token_format_and_recovers_signer() {
let w = crate::wallet::generate();
let token = proxy_auth_token(&w.signer, 1_700_000_000);
let parts: Vec<&str> = token.split(':').collect();
assert_eq!(parts.len(), 3, "token is address:timestamp:signature");
let addr = format!("0x{}", bytes_to_hex(&crate::wallet::address(&w.signer)));
assert_eq!(parts[0], addr, "first field is the 0x address");
assert_eq!(parts[1], "1700000000", "second field is the unix timestamp");
assert!(parts[2].starts_with("0x"));
assert_eq!(parts[2].len(), 2 + 130, "signature is 65 bytes");
let msg = format!("localharness-proxy:{}:{}", parts[0], parts[1]);
let digest = crate::wallet::personal_sign_digest(msg.as_bytes());
let sig: [u8; 65] = hex_to_bytes(parts[2]).unwrap().try_into().unwrap();
let recovered = crate::wallet::recover_address(&sig, &digest).unwrap();
assert_eq!(format!("0x{}", bytes_to_hex(&recovered)), addr);
}
#[test]
fn encode_submit_feedback_abi_layout() {
let cd = encode_submit_feedback("hi");
assert_eq!(&cd[0..4], &selector("submitFeedback(string)"));
assert_eq!(&cd[4..36], &u256_be(0x20), "string offset");
assert_eq!(&cd[36..68], &u256_be(2), "string length");
assert_eq!(&cd[68..70], b"hi");
assert_eq!(cd.len(), 4 + 64 + 32, "selector + offset + len + padded payload");
assert_eq!(encode_submit_feedback(&"x".repeat(32)).len(), 4 + 64 + 32);
assert_eq!(encode_submit_feedback(&"x".repeat(33)).len(), 4 + 64 + 64);
}
#[test]
fn encode_set_persona_abi_layout() {
let cd = encode_set_persona(7, "hi");
assert_eq!(&cd[0..4], &selector("setMetadata(uint256,bytes32,bytes)"));
assert_eq!(&cd[4..36], &u256_be(7));
assert_eq!(&cd[36..68], &keccak_key(PERSONA_LABEL));
assert_eq!(&cd[68..100], &u256_be(0x60), "bytes offset");
assert_eq!(&cd[100..132], &u256_be(2), "payload length");
assert_eq!(&cd[132..134], b"hi");
assert_eq!(
cd.len(),
4 + 96 + 32 + 32,
"selector + 3 words + len + padded payload"
);
}
#[test]
fn encode_set_capability_commits_to_hash_not_payload() {
let payload = b"price=10;payee=0xabc;service=qa";
let cd = encode_set_capability(7, payload);
assert_eq!(&cd[0..4], &selector("setMetadata(uint256,bytes32,bytes)"));
assert_eq!(&cd[4..36], &u256_be(7));
assert_eq!(&cd[36..68], &keccak_key(CAPABILITY_LABEL));
assert_eq!(&cd[68..100], &u256_be(0x60), "bytes offset");
assert_eq!(&cd[100..132], &u256_be(32), "commitment is 32 bytes");
assert_eq!(&cd[132..164], &keccak_key(payload));
assert_ne!(&cd[132..164], &payload[..32.min(payload.len())]);
assert_eq!(cd.len(), 4 + 96 + 32 + 32);
}
#[test]
fn capability_key_distinct_from_other_metadata_keys() {
let cap = keccak_key(CAPABILITY_LABEL);
assert_ne!(cap, keccak_key(PERSONA_LABEL));
assert_ne!(cap, keccak_key(PUBLIC_FACE_LABEL));
assert_ne!(cap, keccak_key(PUBLIC_HTML_LABEL));
assert_ne!(cap, app_metadata_key());
}
#[test]
fn persona_key_distinct_from_other_metadata_keys() {
let persona = keccak_key(PERSONA_LABEL);
assert_ne!(persona, keccak_key(PUBLIC_FACE_LABEL));
assert_ne!(persona, keccak_key(PUBLIC_HTML_LABEL));
assert_ne!(persona, app_metadata_key());
}
#[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());
}
}