use k256::ecdsa::SigningKey;
use crate::wallet;
use super::*;
pub async fn main_of(holder_hex: &str) -> Result<u64, 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 result = read_view(selector("mainOf(address)"), &[padded]).await?;
decode_u256_as_u64(&result)
}
pub async fn register_main_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
fee_token: &str,
) -> Result<String, String> {
let cost = main_cost().await.unwrap_or(0);
let input = encode_register_main(token_id);
if cost > 0 {
sponsored_escrow_diamond_call(sender, fee_payer, cost, input, fee_token, 700_000).await
} else {
sponsored_diamond_call(sender, fee_payer, input, fee_token, 700_000).await
}
}
pub(crate) 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 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);
let calldata = encode_call_hex(selector("isAuthorizedSigner(address)"), &[padded]);
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 = encode_call_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 devices_of(main_id: u64) -> Result<Vec<String>, String> {
let result = read_view(selector("devicesOf(uint256)"), &[u256_be(main_id as u128)]).await?;
let bytes = hex_to_bytes(&result)?;
Ok(decode_address_array(&bytes))
}
pub(crate) 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
}
pub(crate) 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
}
pub(crate) 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 fn approve_credits_call(amount_wei: u128) -> Result<crate::tempo_tx::TempoCall, String> {
let diamond = parse_eth_address(REGISTRY_ADDRESS)?;
let token = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS)?;
Ok(crate::tempo_tx::TempoCall {
to: token,
value_wei: 0,
input: encode_approve(&diamond, amount_wei),
})
}
pub async fn release_name_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(sender, fee_payer, encode_release_name(token_id), 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 async fn main_cost() -> Result<u128, String> {
let result = read_view(selector("mainCost()"), &[]).await?;
decode_u256_as_u128(&result)
}
pub async fn treasury_balance() -> Result<u128, String> {
let result = read_view(selector("treasuryBalance()"), &[]).await?;
decode_u256_as_u128(&result)
}
pub async fn registration_cost() -> Result<u128, String> {
let result = read_view(selector("registrationCost()"), &[]).await?;
decode_u256_as_u128(&result)
}
pub(crate) 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(crate) fn encode_transfer(to: &[u8; 20], amount_wei: u128) -> Vec<u8> {
let sel = selector("transfer(address,uint256)");
let mut to_padded = [0u8; 32];
to_padded[12..].copy_from_slice(to);
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(&to_padded);
out.extend_from_slice(&amount_padded);
out
}
#[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 fn tba_send_lh_calls(
token_id: u64,
tba_hex: &str,
recipient_hex: &str,
amount_wei: u128,
) -> Result<Vec<crate::tempo_tx::TempoCall>, String> {
if amount_wei == 0 {
return Err("amount must be greater than 0".into());
}
let diamond = parse_eth_address(REGISTRY_ADDRESS)?;
let tba = parse_eth_address(tba_hex)?;
let recipient = parse_eth_address(recipient_hex)?;
let token = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS)?;
let transfer_data = encode_erc20_transfer(&recipient, amount_wei);
Ok(vec![
crate::tempo_tx::TempoCall {
to: diamond,
value_wei: 0,
input: encode_create_tba(token_id),
},
crate::tempo_tx::TempoCall {
to: tba,
value_wei: 0,
input: encode_tba_execute(&token, 0, &transfer_data),
},
])
}
pub const TBA_SEND_LH_GAS: u128 = 2_000_000;
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 calls = tba_send_lh_calls(token_id, tba_address, recipient_hex, amount_wei)?;
submit_tempo_sponsored(sender, fee_payer, calls, fee_token, TBA_SEND_LH_GAS).await
}
#[allow(clippy::too_many_arguments)]
pub async fn tba_execute_call_sponsored(
owner_signer: &SigningKey,
fee_payer: &SigningKey,
tba_addr: &str,
to: &str,
value_wei: u128,
data: &[u8],
fee_token: &str,
) -> Result<String, String> {
let target = parse_eth_address(to)?;
sponsored_call_to(
owner_signer,
fee_payer,
tba_addr,
encode_tba_execute(&target, value_wei, data),
fee_token,
2_000_000,
)
.await
}
pub async fn create_token_bound_account_sponsored(
owner_signer: &SigningKey,
fee_payer: &SigningKey,
token_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
owner_signer,
fee_payer,
encode_create_tba(token_id),
fee_token,
1_200_000,
)
.await
}
pub async fn tba_send_lh_sponsored(
owner_signer: &SigningKey,
fee_payer: &SigningKey,
tba_addr: &str,
recipient_hex: &str,
amount_wei: u128,
fee_token: &str,
) -> Result<String, String> {
let recipient = parse_eth_address(recipient_hex)?;
let transfer_data = encode_erc20_transfer(&recipient, amount_wei);
tba_execute_call_sponsored(
owner_signer,
fee_payer,
tba_addr,
LOCALHARNESS_TOKEN_ADDRESS,
0,
&transfer_data,
fee_token,
)
.await
}
pub(crate) fn encode_erc20_transfer(recipient: &[u8; 20], amount_wei: u128) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 32 + 32);
out.extend_from_slice(&selector("transfer(address,uint256)"));
let mut padded = [0u8; 32];
padded[12..].copy_from_slice(recipient);
out.extend_from_slice(&padded);
out.extend_from_slice(&u256_be(amount_wei));
out
}
pub(crate) 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
}
pub(crate) 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
}
pub(crate) 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_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
name: &str,
fee_token: &str,
) -> Result<String, String> {
let cost = registration_cost().await.unwrap_or(0);
let register_input = hex_to_bytes(&encode_register(name))?;
let tx_hash = if cost > 0 {
sponsored_escrow_diamond_call(sender, fee_payer, cost, register_input, fee_token, 2_200_000)
.await?
} else {
sponsored_diamond_call(sender, fee_payer, register_input, 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(test)]
mod tests {
use super::*;
#[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 unlink_device_calldata_layout() {
let dev = [0xCDu8; 20];
let unlink = encode_unlink_device(3, &dev);
assert_eq!(&unlink[0..4], &selector("unlinkDevice(uint256,address)"));
assert_eq!(unlink.len(), 68);
assert_eq!(u64::from_be_bytes(unlink[28..36].try_into().unwrap()), 3); assert_eq!(&unlink[36 + 12..36 + 32], &dev); }
#[test]
fn transfer_calldata_layout() {
let to = [0xFFu8; 20];
let amount = 1_500_000_000_000_000_000u128; let cd = encode_transfer(&to, amount);
assert_eq!(&cd[0..4], &[0xa9, 0x05, 0x9c, 0xbb]);
assert_eq!(cd.len(), 4 + 64);
assert_eq!(&cd[4..4 + 12], &[0u8; 12]);
assert_eq!(&cd[4 + 12..4 + 32], &to);
assert_eq!(&cd[4 + 32..4 + 48], &[0u8; 16]);
assert_eq!(
u128::from_be_bytes(cd[4 + 48..4 + 64].try_into().unwrap()),
amount
);
}
#[test]
fn approve_calldata_layout_max_amount() {
let spender = [0xABu8; 20];
let cd = encode_approve(&spender, u128::MAX);
assert_eq!(&cd[0..4], &[0x09, 0x5e, 0xa7, 0xb3]);
assert_eq!(cd.len(), 4 + 64);
assert_eq!(&cd[4 + 12..4 + 32], &spender);
assert_eq!(&cd[4 + 32..4 + 48], &[0u8; 16]);
assert_eq!(&cd[4 + 48..4 + 64], &[0xFFu8; 16]);
}
#[test]
fn tba_execute_calldata_layout() {
let target = [0xABu8; 20];
let data = [0x01, 0x02, 0x03, 0x04, 0x05];
let value: u128 = 0x1234;
let cd = encode_tba_execute(&target, value, &data);
assert_eq!(&cd[0..4], &selector("execute(address,uint256,bytes,uint8)"));
assert!(cd[4..16].iter().all(|&b| b == 0)); assert_eq!(&cd[16..36], &target); assert_eq!(&cd[36..68], &u256_be(value));
assert_eq!(&cd[68..100], &u256_be(0x80));
assert!(cd[100..132].iter().all(|&b| b == 0));
assert_eq!(&cd[132..164], &u256_be(data.len() as u128));
assert_eq!(&cd[164..164 + data.len()], &data);
assert_eq!(cd.len(), 164 + 32); assert!(cd[164 + data.len()..].iter().all(|&b| b == 0));
let empty = encode_tba_execute(&target, 0, &[]);
assert_eq!(empty.len(), 4 + 128 + 32); assert_eq!(&empty[132..164], &u256_be(0));
}
#[test]
fn tba_transfer_lh_calldata_layout() {
let recipient = [0xCDu8; 20];
let amount: u128 = 1_000_000_000_000_000_000;
let inner = encode_erc20_transfer(&recipient, amount);
assert_eq!(&inner[0..4], &selector("transfer(address,uint256)"));
assert_eq!(&inner[16..36], &recipient); assert_eq!(&inner[36..68], &u256_be(amount));
assert_eq!(inner.len(), 4 + 32 + 32);
let token = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS).unwrap();
let cd = encode_tba_execute(&token, 0, &inner);
assert_eq!(&cd[0..4], &selector("execute(address,uint256,bytes,uint8)"));
assert_eq!(&cd[16..36], &token); assert_eq!(&cd[36..68], &u256_be(0)); assert_eq!(&cd[68..100], &u256_be(0x80)); assert!(cd[100..132].iter().all(|&b| b == 0)); assert_eq!(&cd[132..164], &u256_be(inner.len() as u128));
assert_eq!(&cd[164..164 + inner.len()], inner.as_slice());
assert_eq!(cd.len(), 4 + 128 + 32 + 96);
}
#[test]
fn tba_send_lh_calls_batch_layout() {
let tba_hex = format!("0x{}", "aa".repeat(20));
let recipient_hex = format!("0x{}", "cd".repeat(20));
let amount: u128 = 250_000_000_000_000_000; let calls = tba_send_lh_calls(42, &tba_hex, &recipient_hex, amount).unwrap();
assert_eq!(calls.len(), 2);
let diamond = parse_eth_address(REGISTRY_ADDRESS).unwrap();
assert_eq!(calls[0].to, diamond);
assert_eq!(calls[0].value_wei, 0);
assert_eq!(calls[0].input, encode_create_tba(42));
assert_eq!(
u64::from_be_bytes(calls[0].input[28..36].try_into().unwrap()),
42
);
assert_eq!(calls[1].to, [0xAAu8; 20]);
assert_eq!(calls[1].value_wei, 0);
let token = parse_eth_address(LOCALHARNESS_TOKEN_ADDRESS).unwrap();
let inner = encode_erc20_transfer(&[0xCDu8; 20], amount);
assert_eq!(calls[1].input, encode_tba_execute(&token, 0, &inner));
assert_eq!(&calls[1].input[16..36], &token);
assert_eq!(&calls[1].input[164..164 + inner.len()], inner.as_slice());
}
#[test]
fn tba_send_lh_calls_rejects_bad_inputs() {
let tba = format!("0x{}", "aa".repeat(20));
let to = format!("0x{}", "cd".repeat(20));
assert!(tba_send_lh_calls(1, &tba, &to, 0).is_err()); assert!(tba_send_lh_calls(1, "0x1234", &to, 1).is_err()); assert!(tba_send_lh_calls(1, &tba, "not-an-address", 1).is_err());
assert!(tba_send_lh_calls(1, &tba, "", 1).is_err());
}
}