use tiny_keccak::{Hasher, Keccak};
pub const CONTENTHASH_SELECTOR: [u8; 4] = [0xbc, 0x1c, 0x58, 0xd1];
pub const fn hex_addr(s: &str) -> [u8; 20] {
let b = s.as_bytes();
assert!(b.len() == 40, "address must be 40 hex chars");
let mut out = [0u8; 20];
let mut i = 0;
while i < 20 {
out[i] = (hex_nibble(b[i * 2]) << 4) | hex_nibble(b[i * 2 + 1]);
i += 1;
}
out
}
pub const fn hex_nibble(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => panic!("invalid hex"),
}
}
pub fn keccak256(data: &[u8]) -> [u8; 32] {
let mut hasher = Keccak::v256();
hasher.update(data);
let mut out = [0u8; 32];
hasher.finalize(&mut out);
out
}
pub fn namehash(domain: &str) -> [u8; 32] {
if domain.is_empty() {
return [0u8; 32];
}
let labels: Vec<&str> = domain.split('.').collect();
let mut node = [0u8; 32];
for label in labels.into_iter().rev() {
let label_hash = keccak256(label.as_bytes());
let mut buf = [0u8; 64];
buf[..32].copy_from_slice(&node);
buf[32..].copy_from_slice(&label_hash);
node = keccak256(&buf);
}
node
}
pub fn encode_contenthash_call(node: &[u8; 32]) -> Vec<u8> {
let mut data = Vec::with_capacity(36);
data.extend_from_slice(&CONTENTHASH_SELECTOR);
data.extend_from_slice(node);
data
}
pub fn scale_encode_revive_call(dest: &[u8; 20], input_data: &[u8]) -> Result<Vec<u8>, String> {
let mut buf = Vec::with_capacity(128 + input_data.len());
buf.extend_from_slice(&[
0xd4, 0x35, 0x93, 0xc7, 0x15, 0xfd, 0xd3, 0x1c, 0x61, 0x14, 0x1a, 0xbd, 0x04, 0xa9, 0x9f,
0xd6, 0x82, 0x2c, 0x85, 0x58, 0x85, 0x4c, 0xcd, 0xe3, 0x9a, 0x56, 0x84, 0xe7, 0xa5, 0x6d,
0xa2, 0x7d,
]);
buf.extend_from_slice(dest);
buf.extend_from_slice(&0u128.to_le_bytes());
buf.push(0x01); scale_compact_u64(&mut buf, u64::MAX); scale_compact_u64(&mut buf, u64::MAX);
buf.push(0x01); buf.extend_from_slice(&(u64::MAX as u128).to_le_bytes());
scale_compact_len(&mut buf, input_data.len())?;
buf.extend_from_slice(input_data);
Ok(buf)
}
pub fn scale_compact_u64(buf: &mut Vec<u8>, val: u64) {
if val < 64 {
buf.push((val as u8) << 2);
} else if val < 0x4000 {
let v = ((val as u16) << 2) | 1;
buf.extend_from_slice(&v.to_le_bytes());
} else if val < 0x4000_0000 {
let v = ((val as u32) << 2) | 2;
buf.extend_from_slice(&v.to_le_bytes());
} else {
let bytes = val.to_le_bytes();
let len = 8 - (val.leading_zeros() / 8) as usize;
let len = len.max(4); let prefix = (((len - 4) as u8) << 2) | 3;
buf.push(prefix);
buf.extend_from_slice(&bytes[..len]);
}
}
pub fn scale_compact_len(buf: &mut Vec<u8>, n: usize) -> Result<(), String> {
if n < 64 {
buf.push((n as u8) << 2);
} else if n < 16384 {
let v = ((n as u16) << 2) | 1;
buf.extend_from_slice(&v.to_le_bytes());
} else if n < 1_073_741_824 {
let v = ((n as u32) << 2) | 2;
buf.extend_from_slice(&v.to_le_bytes());
} else {
return Err(format!(
"compact encoding: value {n} is too large (max 1_073_741_823)"
));
}
Ok(())
}
pub use crate::{hex_decode, hex_encode};
pub fn decode_contract_result(response: &[u8]) -> Result<Vec<u8>, String> {
let mut pos = 0;
let (_, n) = decode_scale_compact(&response[pos..])?;
pos += n;
let (_, n) = decode_scale_compact(&response[pos..])?;
pos += n;
let (_, n) = decode_scale_compact(&response[pos..])?;
pos += n;
let (_, n) = decode_scale_compact(&response[pos..])?;
pos += n;
if pos + 17 > response.len() {
return Err("response too short (storage_deposit)".into());
}
pos += 1 + 16;
if pos >= response.len() {
return Err("response too short (extra fields)".into());
}
let opt_tag = response[pos];
pos += 1; if opt_tag == 1 {
if pos + 16 > response.len() {
return Err("response too short (option balance)".into());
}
pos += 16; }
if pos + 16 > response.len() {
return Err("response too short (plain balance)".into());
}
pos += 16;
let (msg_len, bytes_read) = decode_scale_compact(&response[pos..])?;
pos += bytes_read + msg_len;
if pos + 4 > response.len() {
return Err("response too short (flags)".into());
}
pos += 4;
let (data_len, bytes_read) = decode_scale_compact(&response[pos..])?;
pos += bytes_read;
if pos + data_len > response.len() {
return Err(format!(
"data extends beyond response (pos={pos}, data_len={data_len}, total={})",
response.len()
));
}
Ok(response[pos..pos + data_len].to_vec())
}
pub fn decode_scale_compact(data: &[u8]) -> Result<(usize, usize), String> {
if data.is_empty() {
return Err("empty data for compact decode".into());
}
let mode = data[0] & 0b11;
match mode {
0 => Ok(((data[0] >> 2) as usize, 1)),
1 => {
if data.len() < 2 {
return Err("compact: need 2 bytes".into());
}
let v = u16::from_le_bytes([data[0], data[1]]) >> 2;
Ok((v as usize, 2))
}
2 => {
if data.len() < 4 {
return Err("compact: need 4 bytes".into());
}
let v = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) >> 2;
Ok((v as usize, 4))
}
3 => {
let bytes_needed = (data[0] >> 2) as usize + 4;
if data.len() < 1 + bytes_needed {
return Err("compact: big mode insufficient data".into());
}
let mut val: usize = 0;
for i in (0..bytes_needed).rev() {
val = (val << 8) | data[1 + i] as usize;
}
Ok((val, 1 + bytes_needed))
}
_ => unreachable!(),
}
}
pub fn decode_abi_bytes(data: &[u8]) -> Result<Vec<u8>, String> {
if data.len() < 64 {
return Err(format!("ABI bytes too short: {} bytes", data.len()));
}
let len = u32::from_be_bytes([data[60], data[61], data[62], data[63]]) as usize;
if 64 + len > data.len() {
return Err(format!("ABI bytes: length {len} exceeds data"));
}
Ok(data[64..64 + len].to_vec())
}
pub fn contenthash_to_cid(data: &[u8]) -> Result<String, String> {
if data.is_empty() {
return Err("empty contenthash".into());
}
let (codec, varint_len) = decode_unsigned_varint(data);
match codec {
0xe3 => {
let cid_bytes = &data[varint_len..];
Ok(format!("b{}", base32_encode(cid_bytes)))
}
0xe5 => Err("Swarm contenthash not supported".into()),
_ => Err(format!(
"contenthash_to_cid: unrecognized codec varint 0x{codec:02x}; only IPFS dag-pb (0x70) is supported"
)),
}
}
pub fn decode_unsigned_varint(data: &[u8]) -> (u64, usize) {
let mut value: u64 = 0;
let mut shift = 0;
for (i, &byte) in data.iter().enumerate() {
if shift >= 64 {
break;
}
value |= ((byte & 0x7f) as u64) << shift;
if byte & 0x80 == 0 {
return (value, i + 1);
}
shift += 7;
}
(value, data.len())
}
pub fn base32_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
let mut result = String::new();
let mut bits: u32 = 0;
let mut num_bits: u32 = 0;
for &byte in data {
bits = (bits << 8) | byte as u32;
num_bits += 8;
while num_bits >= 5 {
num_bits -= 5;
result.push(ALPHABET[((bits >> num_bits) & 0x1f) as usize] as char);
}
}
if num_bits > 0 {
result.push(ALPHABET[((bits << (5 - num_bits)) & 0x1f) as usize] as char);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_nibble_digits() {
assert_eq!(hex_nibble(b'0'), 0);
assert_eq!(hex_nibble(b'9'), 9);
assert_eq!(hex_nibble(b'a'), 10);
assert_eq!(hex_nibble(b'f'), 15);
assert_eq!(hex_nibble(b'A'), 10);
assert_eq!(hex_nibble(b'F'), 15);
}
#[test]
fn test_hex_addr_roundtrip() {
let addr = hex_addr("7756DF72CBc7f062e7403cD59e45fBc78bed1cD7");
assert_eq!(addr[0], 0x77);
assert_eq!(addr[19], 0xd7);
}
#[test]
fn test_keccak256_empty() {
let result = keccak256(b"");
assert_eq!(result[0], 0xc5);
assert_eq!(result[1], 0xd2);
}
#[test]
fn test_namehash_empty_domain() {
assert_eq!(namehash(""), [0u8; 32]);
}
#[test]
fn test_encode_contenthash_call_length() {
let node = [0u8; 32];
let call = encode_contenthash_call(&node);
assert_eq!(call.len(), 36);
assert_eq!(&call[..4], &CONTENTHASH_SELECTOR);
}
#[test]
fn test_scale_compact_u64_single_byte() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0);
assert_eq!(buf, vec![0x00]);
buf.clear();
scale_compact_u64(&mut buf, 63);
assert_eq!(buf, vec![0xfc]);
}
#[test]
fn test_scale_compact_len_too_large_returns_error() {
let mut buf = Vec::new();
let result = scale_compact_len(&mut buf, 1_073_741_824);
assert!(result.is_err());
}
#[test]
fn test_hex_encode_decode_roundtrip() {
let original = vec![0xde, 0xad, 0xbe, 0xef];
let encoded = hex_encode(&original);
assert_eq!(encoded, "0xdeadbeef");
let decoded = hex_decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_hex_decode_rejects_odd_length() {
assert!(hex_decode("0xabc").is_none());
}
#[test]
fn test_decode_scale_compact_single_byte() {
let (val, consumed) = decode_scale_compact(&[0x04]).unwrap();
assert_eq!(val, 1);
assert_eq!(consumed, 1);
}
#[test]
fn test_decode_scale_compact_empty_returns_error() {
assert!(decode_scale_compact(&[]).is_err());
}
#[test]
fn test_decode_abi_bytes_too_short_returns_error() {
assert!(decode_abi_bytes(&[0u8; 32]).is_err());
}
#[test]
fn test_contenthash_to_cid_empty_returns_error() {
assert!(contenthash_to_cid(&[]).is_err());
}
#[test]
fn test_contenthash_to_cid_swarm_returns_error() {
assert!(contenthash_to_cid(&[0xe5, 0x01, 0x00]).is_err());
}
#[test]
fn test_contenthash_to_cid_unknown_codec_returns_error() {
let result = contenthash_to_cid(&[0x01, 0x02, 0x03]);
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("unrecognized codec varint"));
}
#[test]
fn test_base32_encode_known_vector() {
assert_eq!(base32_encode(b""), "");
assert_eq!(base32_encode(b"f"), "my");
}
#[test]
fn test_decode_unsigned_varint_single_byte() {
let (val, consumed) = decode_unsigned_varint(&[0x05]);
assert_eq!(val, 5);
assert_eq!(consumed, 1);
}
#[test]
fn test_decode_unsigned_varint_multi_byte() {
let (val, consumed) = decode_unsigned_varint(&[0xac, 0x02]);
assert_eq!(val, 300);
assert_eq!(consumed, 2);
}
#[test]
fn test_namehash_mytestapp_dot_pinned() {
let node = namehash("mytestapp.dot");
let hex = hex_encode(&node);
assert_ne!(node, [0u8; 32]);
let expected = hex_decode(&hex).unwrap();
assert_eq!(node.to_vec(), expected);
assert_eq!(
hex,
"0xea3cb49a7f22581a2b768fdfd30be01a398514934d65b60e158ee9ee93c20894"
);
}
#[test]
fn test_scale_encode_revive_call_pinned() {
let content_resolver: [u8; 20] = hex_addr("7756DF72CBc7f062e7403cD59e45fBc78bed1cD7");
let node = namehash("mytestapp.dot");
let call_data = encode_contenthash_call(&node);
let params = scale_encode_revive_call(&content_resolver, &call_data).unwrap();
let expected = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d7756df72cbc7f062e7403cd59e45fbc78bed1cd7000000000000000000000000000000000113ffffffffffffffff13ffffffffffffffff01ffffffffffffffff000000000000000090bc1c58d1ea3cb49a7f22581a2b768fdfd30be01a398514934d65b60e158ee9ee93c20894";
assert_eq!(hex_encode(¶ms), expected);
}
#[test]
fn test_contenthash_to_cid_ipfs_pinned() {
let digest =
hex_decode("0xc3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a")
.unwrap();
let mut contenthash = vec![0xe3, 0x01, 0x01, 0x70, 0x12, 0x20];
contenthash.extend_from_slice(&digest);
let cid = contenthash_to_cid(&contenthash).unwrap();
assert!(cid.starts_with("bafybei"));
}
#[test]
fn test_scale_compact_u64_two_byte_mode() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 64);
assert_eq!(buf, vec![0x01, 0x01]);
}
#[test]
fn test_scale_compact_u64_four_byte_mode() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0x4000);
let expected_val: u32 = (0x4000u32 << 2) | 2;
assert_eq!(buf, expected_val.to_le_bytes());
}
#[test]
fn test_scale_compact_u64_big_mode_u64_max() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, u64::MAX);
assert_eq!(buf[0], 0x13);
assert_eq!(buf.len(), 9);
assert_eq!(&buf[1..], &u64::MAX.to_le_bytes());
}
#[test]
fn test_scale_compact_len_single_byte_boundary() {
let mut buf = Vec::new();
scale_compact_len(&mut buf, 0).unwrap();
assert_eq!(buf, vec![0x00]);
buf.clear();
scale_compact_len(&mut buf, 63).unwrap();
assert_eq!(buf, vec![0xfc]); }
#[test]
fn test_scale_compact_len_two_byte_mode() {
let mut buf = Vec::new();
scale_compact_len(&mut buf, 64).unwrap();
assert_eq!(buf, vec![0x01, 0x01]);
}
#[test]
fn test_scale_compact_len_four_byte_mode() {
let mut buf = Vec::new();
scale_compact_len(&mut buf, 16384).unwrap();
let expected: u32 = (16384u32 << 2) | 2;
assert_eq!(buf, expected.to_le_bytes());
}
#[test]
fn test_decode_scale_compact_two_byte_mode() {
let (val, consumed) = decode_scale_compact(&[0x01, 0x01]).unwrap();
assert_eq!(val, 64);
assert_eq!(consumed, 2);
}
#[test]
fn test_decode_scale_compact_two_byte_truncated_returns_error() {
assert!(decode_scale_compact(&[0x01]).is_err());
}
#[test]
fn test_decode_scale_compact_four_byte_mode() {
let v: u32 = (16384u32 << 2) | 2;
let input = v.to_le_bytes();
let (val, consumed) = decode_scale_compact(&input).unwrap();
assert_eq!(val, 16384);
assert_eq!(consumed, 4);
}
#[test]
fn test_decode_scale_compact_four_byte_truncated_returns_error() {
assert!(decode_scale_compact(&[0x02, 0x00, 0x00]).is_err());
}
#[test]
fn test_decode_scale_compact_big_mode() {
let input = [0x03u8, 0x04, 0x03, 0x02, 0x01];
let (val, consumed) = decode_scale_compact(&input).unwrap();
assert_eq!(val, 0x01020304);
assert_eq!(consumed, 5); }
#[test]
fn test_decode_scale_compact_big_mode_insufficient_data_returns_error() {
let input = [0x03u8, 0x04, 0x03, 0x02];
assert!(decode_scale_compact(&input).is_err());
}
#[test]
fn test_decode_abi_bytes_valid() {
let mut data = vec![0u8; 64];
data[31] = 0x20;
data[63] = 5;
data.extend_from_slice(b"hello");
let result = decode_abi_bytes(&data).unwrap();
assert_eq!(result, b"hello");
}
#[test]
fn test_decode_abi_bytes_zero_length() {
let mut data = vec![0u8; 64];
data[31] = 0x20;
let result = decode_abi_bytes(&data).unwrap();
assert_eq!(result, b"");
}
#[test]
fn test_decode_abi_bytes_length_exceeds_data_returns_error() {
let mut data = vec![0u8; 64];
data[31] = 0x20;
data[63] = 100; assert!(decode_abi_bytes(&data).is_err());
}
#[test]
fn test_hex_decode_rejects_invalid_chars() {
assert!(hex_decode("0xgg").is_none());
assert!(hex_decode("zz").is_none());
}
#[test]
fn test_namehash_single_label() {
let node = namehash("dot");
assert_ne!(node, [0u8; 32]);
assert_eq!(node, namehash("dot"));
}
fn build_contract_result(payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0); scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
buf.push(0x00); buf.extend_from_slice(&0u128.to_le_bytes());
buf.push(0x00);
buf.extend_from_slice(&0u128.to_le_bytes());
scale_compact_len(&mut buf, 0).unwrap();
buf.extend_from_slice(&0u32.to_le_bytes()); scale_compact_len(&mut buf, payload.len()).unwrap();
buf.extend_from_slice(payload);
buf
}
#[test]
fn test_decode_contract_result_empty_payload() {
let buf = build_contract_result(b"");
let result = decode_contract_result(&buf).unwrap();
assert_eq!(result, b"");
}
#[test]
fn test_decode_contract_result_with_payload() {
let payload = b"\x01\x02\x03\x04";
let buf = build_contract_result(payload);
let result = decode_contract_result(&buf).unwrap();
assert_eq!(result, payload);
}
#[test]
fn test_decode_contract_result_with_option_some() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0); scale_compact_u64(&mut buf, 0); scale_compact_u64(&mut buf, 0); scale_compact_u64(&mut buf, 0); buf.push(0x00); buf.extend_from_slice(&0u128.to_le_bytes()); buf.push(0x01); buf.extend_from_slice(&42u128.to_le_bytes()); buf.extend_from_slice(&0u128.to_le_bytes()); scale_compact_len(&mut buf, 0).unwrap(); buf.extend_from_slice(&0u32.to_le_bytes()); scale_compact_len(&mut buf, 3).unwrap(); buf.extend_from_slice(b"abc");
let result = decode_contract_result(&buf).unwrap();
assert_eq!(result, b"abc");
}
#[test]
fn test_decode_contract_result_truncated_at_storage_deposit_returns_error() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00]);
assert!(decode_contract_result(&buf).is_err());
}
#[test]
fn test_decode_contract_result_truncated_plain_balance_returns_error() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
buf.push(0x00); buf.extend_from_slice(&0u128.to_le_bytes()); buf.push(0x00); buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert!(decode_contract_result(&buf).is_err());
}
#[test]
fn test_decode_contract_result_truncated_option_some_balance_returns_error() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
buf.push(0x00); buf.extend_from_slice(&0u128.to_le_bytes()); buf.push(0x01); buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert!(decode_contract_result(&buf).is_err());
}
#[test]
fn test_decode_contract_result_truncated_at_flags_returns_error() {
let mut buf = build_contract_result(b"payload");
let len = buf.len();
buf.truncate(len - 12);
assert!(decode_contract_result(&buf).is_err());
}
#[test]
fn test_decode_contract_result_data_extends_beyond_response_returns_error() {
let mut buf = build_contract_result(b"");
let len = buf.len();
buf[len - 1] = 100u8 << 2; assert!(decode_contract_result(&buf).is_err());
}
#[test]
fn test_decode_contract_result_with_debug_message() {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
buf.push(0x00);
buf.extend_from_slice(&0u128.to_le_bytes());
buf.push(0x00); buf.extend_from_slice(&0u128.to_le_bytes());
scale_compact_len(&mut buf, 5).unwrap();
buf.extend_from_slice(b"debug");
buf.extend_from_slice(&0u32.to_le_bytes()); scale_compact_len(&mut buf, 4).unwrap();
buf.extend_from_slice(b"data");
let result = decode_contract_result(&buf).unwrap();
assert_eq!(result, b"data");
}
#[test]
fn test_scale_encode_revive_call_rejects_oversized_input() {
let mut buf = Vec::new();
let result = scale_compact_len(&mut buf, 1_073_741_824);
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("too large"));
}
}