use k256::ecdsa::SigningKey;
use super::*;
#[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> {
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)
}
pub(crate) async fn next_id() -> Result<u64, String> {
let result_hex = read_view(selector("nextId()"), &[]).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 result_hex = read_view(selector("nameOfId(uint256)"), &[u256_be(id as u128)]).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;
let end = len
.checked_add(64)
.filter(|&end| end <= raw.len())
.ok_or_else(|| format!("nameOfId: truncated body (len {}, have {})", len, raw.len()))?;
String::from_utf8(raw[64..end].to_vec()).map_err(|e| e.to_string())
}
pub async fn tba_of_token_id(token_id: u64) -> Result<Option<String>, String> {
let result_hex = match read_view(
selector("tokenBoundAccount(uint256)"),
&[u256_be(token_id as u128)],
)
.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> {
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> {
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())))
}
pub(crate) fn encode_owner_of_name(name: &str) -> String {
encode_string_call("ownerOfName(string)", name)
}
pub(crate) 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> {
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> {
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 ids: Vec<u64> = (start..=max_id).rev().collect();
let sel = selector("nameOfId(uint256)");
let calls: Vec<(&str, String)> = ids
.iter()
.map(|&id| (REGISTRY_ADDRESS, encode_call_hex(sel, &[u256_be(id as u128)])))
.collect();
let results = eth_call_batch(&calls).await?;
let out = ids
.iter()
.zip(results.iter())
.filter_map(|(&id, r)| {
r.as_ref()
.ok()
.and_then(|hex| decode_metadata_bytes(hex))
.and_then(|b| String::from_utf8(b).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.map(|name| (id, name))
})
.collect();
Ok(out)
}
pub fn rank_agent_matches(agents: &[(String, String)], query: &str) -> Vec<(String, String)> {
let q = query.trim().to_lowercase();
if q.is_empty() {
return agents.to_vec();
}
let tokens: Vec<&str> = q.split_whitespace().collect();
let overlap = |text_lower: &str| tokens.iter().filter(|t| text_lower.contains(**t)).count();
let mut name_hits: Vec<(usize, usize, (String, String))> = Vec::new();
let mut persona_hits: Vec<(usize, usize, (String, String))> = Vec::new();
for (order, (name, persona)) in agents.iter().enumerate() {
let name_lower = name.to_lowercase();
let persona_lower = persona.to_lowercase();
let name_overlap = overlap(&name_lower);
if name_lower.contains(&q) || name_overlap > 0 {
let score = if name_lower.contains(&q) { 100 } else { 0 } + name_overlap;
name_hits.push((score, order, (name.clone(), persona.clone())));
} else {
let persona_overlap = overlap(&persona_lower);
if persona_lower.contains(&q) || persona_overlap > 0 {
let score = if persona_lower.contains(&q) { 100 } else { 0 } + persona_overlap;
persona_hits.push((score, order, (name.clone(), persona.clone())));
}
}
}
name_hits.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
persona_hits.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
name_hits
.into_iter()
.chain(persona_hits)
.map(|(_, _, pair)| pair)
.collect()
}
pub async fn discover_agents(query: &str, scan: u64) -> Result<Vec<(String, String)>, String> {
let agents = list_recent_agents(scan).await?;
if agents.is_empty() {
return Ok(Vec::new());
}
let ids: Vec<u64> = agents.iter().map(|(id, _)| *id).collect();
let personas = personas_of(&ids).await;
let pairs: Vec<(String, String)> = agents
.into_iter()
.zip(personas)
.map(|((_, name), persona)| (name, persona.unwrap_or_default()))
.collect();
Ok(rank_agent_matches(&pairs, query))
}
pub(crate) 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 result_hex = read_view(
selector("metadata(uint256,bytes32)"),
&[u256_be(token_id as u128), key],
)
.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 = len
.checked_add(64)
.and_then(|end| bytes.get(64..end))
.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
}
pub(crate) 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 result_hex = read_view(
selector("metadata(uint256,bytes32)"),
&[u256_be(token_id as u128), gemini_key_metadata_key()],
)
.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 = len
.checked_add(64)
.and_then(|end| bytes.get(64..end))
.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
}
pub(crate) 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
}
pub(crate) async fn metadata_bytes_of(token_id: u64, key: [u8; 32]) -> Result<Option<Vec<u8>>, String> {
let result_hex = read_view(
selector("metadata(uint256,bytes32)"),
&[u256_be(token_id as u128), key],
)
.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 = len
.checked_add(64)
.and_then(|end| bytes.get(64..end))
.ok_or_else(|| "metadata truncated".to_string())?;
Ok(Some(payload.to_vec()))
}
pub(crate) 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
}
pub(crate) const PUBLIC_FACE_LABEL: &[u8] = b"localharness.public_face";
pub(crate) 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)
}
pub(crate) const PUSH_SUB_LABEL: &[u8] = b"localharness.push_sub";
pub async fn push_sub_of(token_id: u64) -> Result<Option<String>, String> {
match metadata_bytes_of(token_id, keccak_key(PUSH_SUB_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_push_sub(token_id: u64, sub_json: &[u8]) -> Vec<u8> {
encode_set_metadata_bytes(token_id, keccak_key(PUSH_SUB_LABEL), sub_json)
}
pub(crate) 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())
}
pub(crate) const LESSONS_LABEL: &[u8] = b"localharness.lessons";
pub async fn lessons_of(token_id: u64) -> Result<Option<String>, String> {
match metadata_bytes_of(token_id, keccak_key(LESSONS_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_lessons(token_id: u64, lessons: &str) -> Vec<u8> {
encode_set_metadata_bytes(token_id, keccak_key(LESSONS_LABEL), lessons.as_bytes())
}
pub async fn personas_of(token_ids: &[u64]) -> Vec<Option<String>> {
if token_ids.is_empty() {
return Vec::new();
}
let key = keccak_key(PERSONA_LABEL);
let calls: Vec<(&str, String)> = token_ids
.iter()
.map(|&id| (REGISTRY_ADDRESS, call_metadata(id, key)))
.collect();
match eth_call_batch(&calls).await {
Ok(results) => results
.iter()
.map(|r| {
r.as_ref()
.ok()
.and_then(|hex| decode_metadata_bytes(hex))
.and_then(|b| String::from_utf8(b).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.collect(),
Err(_) => token_ids.iter().map(|_| None).collect(),
}
}
pub(crate) fn call_metadata(token_id: u64, key: [u8; 32]) -> String {
encode_call_hex(
selector("metadata(uint256,bytes32)"),
&[u256_be(token_id as u128), key],
)
}
pub(crate) fn decode_metadata_bytes(result_hex: &str) -> Option<Vec<u8>> {
let bytes = hex_to_bytes(result_hex).ok()?;
if bytes.len() < 64 {
return 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 None;
}
len.checked_add(64)
.and_then(|end| bytes.get(64..end))
.map(|s| s.to_vec())
}
pub(crate) 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))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rank_agent_matches_hostile_inputs() {
assert!(rank_agent_matches(&[], "anything").is_empty());
assert!(rank_agent_matches(&[], "").is_empty());
let agents = vec![
("auditor".to_string(), "reviews code".to_string()),
("bob".to_string(), "I AUDIT contracts".to_string()),
("carol".to_string(), "unrelated".to_string()),
];
let hits = rank_agent_matches(&agents, "audit");
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].0, "auditor"); assert_eq!(hits[1].0, "bob"); assert_eq!(rank_agent_matches(&agents, " AUDIT ").len(), 2);
let all = rank_agent_matches(&agents, "\t \n");
assert_eq!(all.len(), 3);
assert_eq!(all[0].0, "auditor");
let dual = vec![("audit".to_string(), "audit audit".to_string())];
assert_eq!(rank_agent_matches(&dual, "audit").len(), 1);
}
#[test]
fn rank_agent_matches_filters_and_ranks() {
let agents = vec![
("alice".to_string(), "A friendly chatbot".to_string()),
("solidity-bob".to_string(), "general assistant".to_string()),
(
"carol".to_string(),
"An expert SOLIDITY auditor + security reviewer".to_string(),
),
("dave".to_string(), "writes haikus".to_string()),
];
let hits = rank_agent_matches(&agents, "solidity");
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].0, "solidity-bob");
assert_eq!(hits[1].0, "carol");
assert!(rank_agent_matches(&agents, "nonexistent").is_empty());
assert_eq!(rank_agent_matches(&agents, "").len(), 4);
assert_eq!(rank_agent_matches(&agents, " ").len(), 4);
}
#[test]
fn rank_agent_matches_multi_keyword() {
let agents = vec![
("chess-game".to_string(), "plays chess".to_string()),
("toolsmith".to_string(), "builds developer tools".to_string()),
(
"arcade".to_string(),
"a game tool for retro arcade fun".to_string(),
),
("dave".to_string(), "writes haikus".to_string()),
];
let hits = rank_agent_matches(&agents, "game tool");
assert_eq!(hits.len(), 3);
assert_eq!(hits[0].0, "chess-game"); assert_eq!(hits[1].0, "toolsmith"); assert_eq!(hits[2].0, "arcade");
let personas = vec![
("a".to_string(), "tool things".to_string()),
("b".to_string(), "a game tool combo".to_string()),
];
let hits = rank_agent_matches(&personas, "game tool");
assert_eq!(hits[0].0, "b"); assert_eq!(hits[1].0, "a");
let hits = rank_agent_matches(&agents, "game");
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].0, "chess-game");
assert_eq!(hits[1].0, "arcade");
}
#[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_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 encode_set_lessons_abi_layout() {
let cd = encode_set_lessons(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(LESSONS_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 lessons_key_distinct_from_other_metadata_keys() {
let lessons = keccak_key(LESSONS_LABEL);
assert_ne!(lessons, keccak_key(PERSONA_LABEL));
assert_ne!(lessons, keccak_key(PUBLIC_FACE_LABEL));
assert_ne!(lessons, keccak_key(PUBLIC_HTML_LABEL));
assert_ne!(lessons, app_metadata_key());
let hex: String = lessons.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(
hex,
"08564cae936ec460c48a23578c7df5665bad18fe42f3c5dbde517ad67a9d9c89"
);
}
#[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 metadata_bytes_edge_cases() {
assert_eq!(decode_metadata_bytes("0x"), None);
assert_eq!(decode_metadata_bytes(&format!("0x{Z}")), None);
let zero_len = format!("0x{}{}", word_usize(0x20), Z);
assert_eq!(decode_metadata_bytes(&zero_len), None);
let huge = format!("0x{}{}", word_usize(0x20), word_u64_max());
assert_eq!(decode_metadata_bytes(&huge), None);
let trunc = format!("0x{}{}", word_usize(0x20), word_usize(10)); assert_eq!(decode_metadata_bytes(&trunc), None);
let ok = format!(
"0x{}{}{}",
word_usize(0x20),
word_usize(3),
"aabbcc0000000000000000000000000000000000000000000000000000000000"
);
assert_eq!(decode_metadata_bytes(&ok), Some(vec![0xAA, 0xBB, 0xCC]));
}
}