use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::{IdentityError, Result};
pub type SolPubkey = [u8; 32];
pub trait OnChainAgentSvmRegistry: Send + Sync {
fn mirror_register_agent(&self, did: &str, metadata_uri: &str) -> Result<()>;
fn lookup_asset_by_did(&self, did: &str) -> Option<SolPubkey>;
}
#[async_trait]
pub trait Erc8004SvmTransport: Send + Sync {
async fn send_signed_transaction(&self, signed_tx: &[u8]) -> Result<String>;
async fn get_account(&self, pubkey: &SolPubkey) -> Result<Option<Vec<u8>>>;
}
pub mod addresses {
use super::SolPubkey;
const fn b58_to_pubkey(_b58: &str, bytes: [u8; 32]) -> SolPubkey {
bytes
}
pub const AGENT_REGISTRY_PROGRAM_ID_MAINNET_BETA: SolPubkey = b58_to_pubkey(
"8oo4J9tBB3Hna1jRQ3rWvJjojqM5DYTDJo5cejUuJy3C",
[
0x73, 0xfe, 0x9a, 0x31, 0x81, 0x81, 0x30, 0x59, 0x3a, 0x08, 0xa7, 0xc2, 0x7a, 0x08,
0xc2, 0xdc, 0xf5, 0xd5, 0xcc, 0xc3, 0x68, 0x86, 0x27, 0xf3, 0x0a, 0x87, 0x0d, 0x44,
0x14, 0xd4, 0x02, 0x27,
],
);
pub const ATOM_ENGINE_PROGRAM_ID: SolPubkey = b58_to_pubkey(
"AToMufS4QD6hEXvcvBDg9m1AHeCLpmZQsyfYa5h9MwAF",
[
0x8c, 0x96, 0xb4, 0x0d, 0xef, 0x5e, 0xdc, 0x22, 0xcb, 0xcd, 0xeb, 0x94, 0xc1, 0xd1,
0x19, 0x3c, 0x05, 0x8c, 0x1c, 0x6d, 0x76, 0xb3, 0x27, 0xe4, 0x5a, 0x40, 0xe7, 0x87,
0x8d, 0x9b, 0xea, 0xd0,
],
);
pub const MPL_CORE_PROGRAM_ID: SolPubkey = b58_to_pubkey(
"CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d",
[
0xaf, 0x54, 0xab, 0x10, 0xbd, 0x97, 0xa5, 0x42, 0xa0, 0x9e, 0xf7, 0xb3, 0x98, 0x89,
0xdd, 0x0c, 0xd3, 0x94, 0xa4, 0xcc, 0xe9, 0xdf, 0xa6, 0xcd, 0xc9, 0x7e, 0xbe, 0x2d,
0x23, 0x5b, 0xa7, 0x48,
],
);
pub const SYSTEM_PROGRAM_ID: SolPubkey = [0u8; 32];
}
pub mod discriminators {
pub const REGISTER: [u8; 8] = [0xd3, 0x7c, 0x43, 0x0f, 0xd3, 0xc2, 0xb2, 0xf0];
pub const REGISTER_WITH_OPTIONS: [u8; 8] =
[0xb1, 0xaf, 0x60, 0x29, 0x3b, 0xa6, 0x0d, 0x06];
pub const SET_AGENT_URI: [u8; 8] = [0x2b, 0xfe, 0xa8, 0x68, 0xc0, 0x33, 0x27, 0x2e];
pub const SET_AGENT_WALLET: [u8; 8] = [0x9a, 0x57, 0xfb, 0x17, 0x33, 0x0c, 0x04, 0x96];
pub const SET_METADATA_PDA: [u8; 8] = [0xec, 0x3c, 0x17, 0x30, 0x8a, 0x45, 0xc4, 0x99];
pub const DELETE_METADATA_PDA: [u8; 8] = [0xe4, 0xbe, 0xc3, 0xff, 0x3d, 0xdd, 0x1a, 0x98];
pub const GIVE_FEEDBACK: [u8; 8] = [0x91, 0x88, 0x7b, 0x03, 0xd7, 0xa5, 0x62, 0x29];
pub const REVOKE_FEEDBACK: [u8; 8] = [0xd3, 0x25, 0xe6, 0x52, 0x76, 0xd8, 0x89, 0xce];
pub const APPEND_RESPONSE: [u8; 8] = [0xa2, 0xd2, 0xba, 0x32, 0xb4, 0x04, 0x2f, 0x68];
pub const OWNER_OF: [u8; 8] = [0xa5, 0x55, 0x2e, 0xf9, 0x64, 0x3d, 0xf9, 0x70];
pub const SYNC_OWNER: [u8; 8] = [0x2e, 0x05, 0xe8, 0xc6, 0x3b, 0x9e, 0xa0, 0x77];
}
pub mod event_discriminators {
pub const AGENT_REGISTERED: [u8; 8] = [0xbf, 0x4e, 0xd9, 0x36, 0xe8, 0x64, 0xbd, 0x55];
pub const URI_UPDATED: [u8; 8] = [0xaa, 0xc7, 0x4e, 0xa7, 0x31, 0x54, 0x66, 0x0b];
pub const WALLET_UPDATED: [u8; 8] = [0xd7, 0x22, 0x0a, 0x3b, 0x18, 0x72, 0xc9, 0x81];
pub const METADATA_SET: [u8; 8] = [0xbe, 0x7d, 0x47, 0x77, 0x0e, 0x1f, 0x1a, 0xc5];
pub const NEW_FEEDBACK: [u8; 8] = [0x0e, 0xa2, 0x3a, 0xc2, 0x83, 0x2a, 0x0b, 0x95];
}
pub mod borsh_ix {
pub fn encode_register(agent_uri: &str) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 4 + agent_uri.len());
data.extend_from_slice(&super::discriminators::REGISTER);
encode_string(&mut data, agent_uri);
data
}
pub fn encode_register_with_options(agent_uri: &str, atom_enabled: bool) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 4 + agent_uri.len() + 1);
data.extend_from_slice(&super::discriminators::REGISTER_WITH_OPTIONS);
encode_string(&mut data, agent_uri);
data.push(atom_enabled as u8);
data
}
pub fn encode_set_agent_uri(new_uri: &str) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 4 + new_uri.len());
data.extend_from_slice(&super::discriminators::SET_AGENT_URI);
encode_string(&mut data, new_uri);
data
}
pub fn encode_set_agent_wallet(new_wallet: &super::SolPubkey, deadline: i64) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 32 + 8);
data.extend_from_slice(&super::discriminators::SET_AGENT_WALLET);
data.extend_from_slice(new_wallet);
data.extend_from_slice(&deadline.to_le_bytes());
data
}
pub fn encode_set_metadata_pda(
key_hash: &[u8; 16],
key: &str,
value: &[u8],
immutable: bool,
) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 16 + 4 + key.len() + 4 + value.len() + 1);
data.extend_from_slice(&super::discriminators::SET_METADATA_PDA);
data.extend_from_slice(key_hash);
encode_string(&mut data, key);
encode_bytes(&mut data, value);
data.push(immutable as u8);
data
}
pub fn encode_delete_metadata_pda(key_hash: &[u8; 16]) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 16);
data.extend_from_slice(&super::discriminators::DELETE_METADATA_PDA);
data.extend_from_slice(key_hash);
data
}
#[allow(clippy::too_many_arguments)]
pub fn encode_give_feedback(
value: i128,
value_decimals: u8,
score: Option<u8>,
feedback_file_hash: Option<[u8; 32]>,
tag1: &str,
tag2: &str,
endpoint: &str,
feedback_uri: &str,
) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&super::discriminators::GIVE_FEEDBACK);
data.extend_from_slice(&value.to_le_bytes());
data.push(value_decimals);
encode_option_u8(&mut data, score);
encode_option_bytes32(&mut data, feedback_file_hash);
encode_string(&mut data, tag1);
encode_string(&mut data, tag2);
encode_string(&mut data, endpoint);
encode_string(&mut data, feedback_uri);
data
}
pub fn encode_revoke_feedback(feedback_index: u64, seal_hash: &[u8; 32]) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 8 + 32);
data.extend_from_slice(&super::discriminators::REVOKE_FEEDBACK);
data.extend_from_slice(&feedback_index.to_le_bytes());
data.extend_from_slice(seal_hash);
data
}
pub fn encode_append_response(
client_address: &super::SolPubkey,
feedback_index: u64,
response_uri: &str,
response_hash: &[u8; 32],
seal_hash: &[u8; 32],
) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 32 + 8 + 4 + response_uri.len() + 32 + 32);
data.extend_from_slice(&super::discriminators::APPEND_RESPONSE);
data.extend_from_slice(client_address);
data.extend_from_slice(&feedback_index.to_le_bytes());
encode_string(&mut data, response_uri);
data.extend_from_slice(response_hash);
data.extend_from_slice(seal_hash);
data
}
fn encode_string(out: &mut Vec<u8>, s: &str) {
let bytes = s.as_bytes();
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
out.extend_from_slice(bytes);
}
fn encode_bytes(out: &mut Vec<u8>, b: &[u8]) {
out.extend_from_slice(&(b.len() as u32).to_le_bytes());
out.extend_from_slice(b);
}
fn encode_option_u8(out: &mut Vec<u8>, v: Option<u8>) {
match v {
None => out.push(0),
Some(x) => {
out.push(1);
out.push(x);
}
}
}
fn encode_option_bytes32(out: &mut Vec<u8>, v: Option<[u8; 32]>) {
match v {
None => out.push(0),
Some(arr) => {
out.push(1);
out.extend_from_slice(&arr);
}
}
}
}
pub mod borsh_event {
use super::SolPubkey;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentRegistered {
pub asset: SolPubkey,
pub collection: SolPubkey,
pub owner: SolPubkey,
pub atom_enabled: bool,
pub agent_uri: String,
}
pub fn decode_agent_registered(body: &[u8]) -> Option<AgentRegistered> {
let mut p = body;
let asset = take_pubkey(&mut p)?;
let collection = take_pubkey(&mut p)?;
let owner = take_pubkey(&mut p)?;
let atom_enabled = take_bool(&mut p)?;
let agent_uri = take_string(&mut p)?;
Some(AgentRegistered {
asset,
collection,
owner,
atom_enabled,
agent_uri,
})
}
fn take_pubkey(p: &mut &[u8]) -> Option<SolPubkey> {
if p.len() < 32 {
return None;
}
let mut out = [0u8; 32];
out.copy_from_slice(&p[..32]);
*p = &p[32..];
Some(out)
}
fn take_bool(p: &mut &[u8]) -> Option<bool> {
if p.is_empty() {
return None;
}
let b = p[0];
*p = &p[1..];
match b {
0 => Some(false),
1 => Some(true),
_ => None,
}
}
fn take_string(p: &mut &[u8]) -> Option<String> {
if p.len() < 4 {
return None;
}
let len = u32::from_le_bytes([p[0], p[1], p[2], p[3]]) as usize;
*p = &p[4..];
if p.len() < len {
return None;
}
let s = std::str::from_utf8(&p[..len]).ok()?.to_string();
*p = &p[len..];
Some(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SvmAgentRecord {
pub asset: SolPubkey,
pub owner: SolPubkey,
pub agent_uri: String,
pub agent_wallet: Option<SolPubkey>,
pub atom_enabled: bool,
}
pub fn base58_to_pubkey(b58: &str) -> Result<SolPubkey> {
let bytes = bs58_decode(b58)
.ok_or_else(|| IdentityError::VerificationFailed(format!("invalid base58: {}", b58)))?;
if bytes.len() != 32 {
return Err(IdentityError::VerificationFailed(format!(
"pubkey must be 32 bytes, got {} (input '{}')",
bytes.len(),
b58
)));
}
let mut out = [0u8; 32];
out.copy_from_slice(&bytes);
Ok(out)
}
pub fn pubkey_to_base58(pk: &SolPubkey) -> String {
bs58_encode(pk)
}
const B58_ALPHABET: &[u8; 58] =
b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
fn bs58_encode(bytes: &[u8]) -> String {
let mut leading_zeros = 0;
for &b in bytes {
if b == 0 {
leading_zeros += 1;
} else {
break;
}
}
let mut num: Vec<u8> = bytes.to_vec();
let mut out: Vec<u8> = Vec::new();
while !num.iter().all(|&b| b == 0) {
let mut remainder = 0u16;
for byte in num.iter_mut() {
let cur = remainder * 256 + (*byte as u16);
*byte = (cur / 58) as u8;
remainder = cur % 58;
}
out.push(B58_ALPHABET[remainder as usize]);
}
for _ in 0..leading_zeros {
out.push(b'1');
}
out.reverse();
String::from_utf8(out).expect("base58 alphabet is ASCII")
}
fn bs58_decode(s: &str) -> Option<Vec<u8>> {
let mut num: Vec<u8> = Vec::new();
for ch in s.chars() {
let digit = B58_ALPHABET.iter().position(|&c| c == ch as u8)? as u32;
let mut carry = digit;
for byte in num.iter_mut() {
carry += (*byte as u32) * 58;
*byte = (carry & 0xff) as u8;
carry >>= 8;
}
while carry > 0 {
num.push((carry & 0xff) as u8);
carry >>= 8;
}
}
let leading_ones = s.chars().take_while(|&c| c == '1').count();
let mut out = vec![0u8; leading_ones];
out.extend(num.iter().rev());
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discriminators_match_idl() {
assert_eq!(
discriminators::REGISTER,
[0xd3, 0x7c, 0x43, 0x0f, 0xd3, 0xc2, 0xb2, 0xf0]
);
assert_eq!(
discriminators::SET_AGENT_URI,
[0x2b, 0xfe, 0xa8, 0x68, 0xc0, 0x33, 0x27, 0x2e]
);
assert_eq!(
event_discriminators::AGENT_REGISTERED,
[0xbf, 0x4e, 0xd9, 0x36, 0xe8, 0x64, 0xbd, 0x55]
);
}
#[test]
fn encode_register_shape() {
let data = borsh_ix::encode_register("did:tenzro:machine:abc");
assert_eq!(data.len(), 8 + 4 + 22);
assert_eq!(&data[..8], &discriminators::REGISTER);
assert_eq!(&data[8..12], &22u32.to_le_bytes());
assert_eq!(&data[12..], b"did:tenzro:machine:abc");
}
#[test]
fn encode_register_with_options_shape() {
let data = borsh_ix::encode_register_with_options("uri", true);
assert_eq!(data.len(), 8 + 4 + 3 + 1);
assert_eq!(&data[..8], &discriminators::REGISTER_WITH_OPTIONS);
assert_eq!(data[data.len() - 1], 1);
}
#[test]
fn encode_set_agent_wallet_shape() {
let pk = [0x11u8; 32];
let data = borsh_ix::encode_set_agent_wallet(&pk, 1717171717);
assert_eq!(data.len(), 8 + 32 + 8);
assert_eq!(&data[..8], &discriminators::SET_AGENT_WALLET);
assert_eq!(&data[8..40], &pk);
assert_eq!(&data[40..48], &1717171717i64.to_le_bytes());
}
#[test]
fn encode_set_metadata_pda_shape() {
let kh = [0xAAu8; 16];
let data = borsh_ix::encode_set_metadata_pda(&kh, "k", b"vv", true);
assert_eq!(data.len(), 8 + 16 + 4 + 1 + 4 + 2 + 1);
assert_eq!(data[data.len() - 1], 1);
}
#[test]
fn encode_revoke_feedback_shape() {
let seal = [0xFFu8; 32];
let data = borsh_ix::encode_revoke_feedback(7, &seal);
assert_eq!(data.len(), 8 + 8 + 32);
assert_eq!(&data[8..16], &7u64.to_le_bytes());
}
#[test]
fn base58_roundtrip() {
let b58 = "8oo4J9tBB3Hna1jRQ3rWvJjojqM5DYTDJo5cejUuJy3C";
let bytes = base58_to_pubkey(b58).expect("decode");
assert_eq!(bytes.len(), 32);
let round = pubkey_to_base58(&bytes);
assert_eq!(round, b58);
}
#[test]
fn base58_decodes_to_constant() {
let decoded =
base58_to_pubkey("8oo4J9tBB3Hna1jRQ3rWvJjojqM5DYTDJo5cejUuJy3C").expect("decode");
assert_eq!(decoded, addresses::AGENT_REGISTRY_PROGRAM_ID_MAINNET_BETA);
let atom =
base58_to_pubkey("AToMufS4QD6hEXvcvBDg9m1AHeCLpmZQsyfYa5h9MwAF").expect("decode");
assert_eq!(atom, addresses::ATOM_ENGINE_PROGRAM_ID);
let mpl = base58_to_pubkey("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d")
.expect("decode");
assert_eq!(mpl, addresses::MPL_CORE_PROGRAM_ID);
}
#[test]
fn decode_agent_registered_roundtrip() {
let asset = [0x01u8; 32];
let coll = [0x02u8; 32];
let owner = [0x03u8; 32];
let uri = "did:tenzro:machine:42";
let mut body = Vec::new();
body.extend_from_slice(&asset);
body.extend_from_slice(&coll);
body.extend_from_slice(&owner);
body.push(1); body.extend_from_slice(&(uri.len() as u32).to_le_bytes());
body.extend_from_slice(uri.as_bytes());
let ev = borsh_event::decode_agent_registered(&body).expect("decode");
assert_eq!(ev.asset, asset);
assert_eq!(ev.collection, coll);
assert_eq!(ev.owner, owner);
assert!(ev.atom_enabled);
assert_eq!(ev.agent_uri, uri);
}
#[test]
fn decode_agent_registered_short_body_returns_none() {
assert!(borsh_event::decode_agent_registered(&[0u8; 10]).is_none());
}
}