use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::error::{IdentityError, Result};
pub type PackageId = String;
pub type PartyId = String;
pub trait OnChainAgentDamlRegistry: Send + Sync {
fn mirror_register_agent(&self, did: &str, metadata_uri: &str) -> Result<()>;
fn lookup_agent_id_by_did(&self, did: &str) -> Option<u64>;
}
#[async_trait::async_trait]
pub trait Erc8004DamlTransport: Send + Sync {
async fn submit_and_wait(&self, command_json: Value) -> Result<String>;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DamlPackageIds {
pub tenzro_erc8004: PackageId,
}
impl DamlPackageIds {
pub fn new_single(package_id: impl Into<PackageId>) -> Self {
Self {
tenzro_erc8004: package_id.into(),
}
}
}
pub mod templates {
pub const IDENTITY_MODULE: &str = "Tenzro.Erc8004.Identity";
pub const REPUTATION_MODULE: &str = "Tenzro.Erc8004.Reputation";
pub const VALIDATION_MODULE: &str = "Tenzro.Erc8004.Validation";
pub const REGISTRY_ADMIN: &str = "RegistryAdmin";
pub const REGISTERED: &str = "Registered";
pub const RETIRED: &str = "Retired";
pub const FEEDBACK: &str = "Feedback";
pub const REPUTATION_ADMIN: &str = "ReputationAdmin";
pub const VALIDATION_REQUEST: &str = "ValidationRequest";
pub const VALIDATION_RESPONSE: &str = "ValidationResponse";
}
pub mod choices {
pub const REGISTER: &str = "Register";
pub const BUMP_COUNTER: &str = "BumpCounter";
pub const SET_AGENT_URI: &str = "SetAgentURI";
pub const SET_AGENT_WALLET: &str = "SetAgentWallet";
pub const UNSET_AGENT_WALLET: &str = "UnsetAgentWallet";
pub const SUBMIT_FEEDBACK: &str = "SubmitFeedback";
pub const REVOKE: &str = "Revoke";
pub const APPEND_RESPONSE: &str = "AppendResponse";
pub const RESPOND: &str = "Respond";
}
pub fn template_id(package_ids: &DamlPackageIds, module: &str, entity: &str) -> String {
format!("{}:{}:{}", package_ids.tenzro_erc8004, module, entity)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DamlAgentRecord {
pub agent_id: u64,
pub admin: PartyId,
pub controller: PartyId,
pub metadata_uri: String,
}
pub mod commands {
use super::*;
pub const APPLICATION_ID: &str = "tenzro-erc8004-mirror";
pub fn command_id(prefix: &str, did: &str) -> String {
format!("{}-{}", prefix, did)
}
pub fn build_register(
package_ids: &DamlPackageIds,
admin_contract_id: &str,
admin_party: &str,
controller_party: &str,
metadata_uri: &str,
did_for_command_id: &str,
) -> Value {
let tid = template_id(package_ids, templates::IDENTITY_MODULE, templates::REGISTRY_ADMIN);
let choice_argument = json!({
"controller": controller_party,
"metadataUri": metadata_uri,
});
build_submit_and_wait(
&tid,
admin_contract_id,
choices::REGISTER,
choice_argument,
&[admin_party, controller_party],
&command_id(APPLICATION_ID, did_for_command_id),
)
}
pub fn build_set_agent_uri(
package_ids: &DamlPackageIds,
registered_contract_id: &str,
controller_party: &str,
new_uri: &str,
did_for_command_id: &str,
) -> Value {
let tid = template_id(package_ids, templates::IDENTITY_MODULE, templates::REGISTERED);
let choice_argument = json!({ "newUri": new_uri });
build_submit_and_wait(
&tid,
registered_contract_id,
choices::SET_AGENT_URI,
choice_argument,
&[controller_party],
&command_id(&format!("{}-set-uri", APPLICATION_ID), did_for_command_id),
)
}
pub fn build_set_agent_wallet(
package_ids: &DamlPackageIds,
registered_contract_id: &str,
current_controller_party: &str,
new_controller_party: &str,
did_for_command_id: &str,
) -> Value {
let tid = template_id(package_ids, templates::IDENTITY_MODULE, templates::REGISTERED);
let choice_argument = json!({ "newController": new_controller_party });
build_submit_and_wait(
&tid,
registered_contract_id,
choices::SET_AGENT_WALLET,
choice_argument,
&[current_controller_party, new_controller_party],
&command_id(&format!("{}-set-wallet", APPLICATION_ID), did_for_command_id),
)
}
pub fn build_submit_feedback(
package_ids: &DamlPackageIds,
admin_contract_id: &str,
admin_party: &str,
rater_party: &str,
subject_agent_id: u64,
rating: i8,
context_uri: &str,
feedback_id_hex32: &str,
) -> Value {
let tid = template_id(
package_ids,
templates::REPUTATION_MODULE,
templates::REPUTATION_ADMIN,
);
let choice_argument = json!({
"rater": rater_party,
"subjectAgentId": subject_agent_id,
"rating": rating as i64,
"contextUri": context_uri,
"feedbackId": feedback_id_hex32,
});
build_submit_and_wait(
&tid,
admin_contract_id,
choices::SUBMIT_FEEDBACK,
choice_argument,
&[admin_party, rater_party],
&command_id(
&format!("{}-submit-feedback", APPLICATION_ID),
feedback_id_hex32,
),
)
}
pub fn build_submit_and_wait(
template_id: &str,
contract_id: &str,
choice: &str,
choice_argument: Value,
act_as: &[&str],
command_id: &str,
) -> Value {
json!({
"commands": [{
"ExerciseCommand": {
"templateId": template_id,
"contractId": contract_id,
"choice": choice,
"choiceArgument": choice_argument,
}
}],
"actAs": act_as,
"commandId": command_id,
"applicationId": APPLICATION_ID,
})
}
}
pub fn require_agent_id(did: &str, registry: &dyn OnChainAgentDamlRegistry) -> Result<u64> {
registry.lookup_agent_id_by_did(did).ok_or_else(|| {
IdentityError::VerificationFailed(format!(
"no ERC-8004 DAML agentId mirrored for DID '{}' yet \
(mirror command may still be pending — retry after a few seconds)",
did
))
})
}
#[cfg(test)]
mod tests {
use super::*;
fn pkg() -> DamlPackageIds {
DamlPackageIds::new_single(
"1122334455667788112233445566778811223344556677881122334455667788",
)
}
#[test]
fn template_id_format_is_three_part_colon() {
let id = template_id(&pkg(), templates::IDENTITY_MODULE, templates::REGISTRY_ADMIN);
let parts: Vec<&str> = id.split(':').collect();
assert_eq!(parts.len(), 3);
assert_eq!(parts[0].len(), 64); assert_eq!(parts[1], "Tenzro.Erc8004.Identity");
assert_eq!(parts[2], "RegistryAdmin");
}
#[test]
fn command_id_is_deterministic_per_did() {
let a = commands::command_id(commands::APPLICATION_ID, "did:tenzro:machine:abc");
let b = commands::command_id(commands::APPLICATION_ID, "did:tenzro:machine:abc");
assert_eq!(a, b);
assert_ne!(
a,
commands::command_id(commands::APPLICATION_ID, "did:tenzro:machine:xyz")
);
}
#[test]
fn build_register_emits_canton_v2_shape() {
let cmd = commands::build_register(
&pkg(),
"00abcd...cid",
"TenzroAdmin::123",
"Controller::456",
"did:tenzro:machine:test",
"did:tenzro:machine:test",
);
let commands_arr = cmd.get("commands").and_then(|v| v.as_array()).unwrap();
assert_eq!(commands_arr.len(), 1);
let ex = commands_arr[0].get("ExerciseCommand").unwrap();
assert_eq!(ex.get("choice").and_then(|v| v.as_str()), Some("Register"));
assert_eq!(
ex.get("contractId").and_then(|v| v.as_str()),
Some("00abcd...cid")
);
let tid = ex.get("templateId").and_then(|v| v.as_str()).unwrap();
assert!(tid.ends_with(":Tenzro.Erc8004.Identity:RegistryAdmin"));
let arg = ex.get("choiceArgument").unwrap();
assert_eq!(
arg.get("controller").and_then(|v| v.as_str()),
Some("Controller::456")
);
assert_eq!(
arg.get("metadataUri").and_then(|v| v.as_str()),
Some("did:tenzro:machine:test")
);
let act_as = cmd.get("actAs").and_then(|v| v.as_array()).unwrap();
assert_eq!(act_as.len(), 2);
assert_eq!(act_as[0].as_str(), Some("TenzroAdmin::123"));
assert_eq!(act_as[1].as_str(), Some("Controller::456"));
assert_eq!(
cmd.get("applicationId").and_then(|v| v.as_str()),
Some(commands::APPLICATION_ID)
);
}
#[test]
fn build_set_agent_uri_targets_registered_template() {
let cmd = commands::build_set_agent_uri(
&pkg(),
"00cid",
"Controller::1",
"did:tenzro:machine:new-uri",
"did:tenzro:machine:abc",
);
let tid = cmd["commands"][0]["ExerciseCommand"]["templateId"]
.as_str()
.unwrap();
assert!(tid.ends_with(":Tenzro.Erc8004.Identity:Registered"));
assert_eq!(
cmd["commands"][0]["ExerciseCommand"]["choice"]
.as_str()
.unwrap(),
"SetAgentURI"
);
}
#[test]
fn build_set_agent_wallet_requires_both_signatures() {
let cmd = commands::build_set_agent_wallet(
&pkg(),
"00cid",
"Old::1",
"New::2",
"did:tenzro:machine:abc",
);
let act_as: Vec<&str> = cmd["actAs"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(act_as, vec!["Old::1", "New::2"]);
}
#[test]
fn build_submit_feedback_widens_i8_rating_to_i64() {
let cmd = commands::build_submit_feedback(
&pkg(),
"00cid",
"Admin::1",
"Rater::2",
42,
-7,
"ipfs://bafy...",
"0000000000000000000000000000000000000000000000000000000000000abc",
);
let arg = &cmd["commands"][0]["ExerciseCommand"]["choiceArgument"];
assert_eq!(arg["rating"].as_i64(), Some(-7));
assert_eq!(arg["subjectAgentId"].as_u64(), Some(42));
assert_eq!(arg["rater"].as_str(), Some("Rater::2"));
}
struct StubRegistry;
impl OnChainAgentDamlRegistry for StubRegistry {
fn mirror_register_agent(&self, _did: &str, _uri: &str) -> Result<()> {
Ok(())
}
fn lookup_agent_id_by_did(&self, _did: &str) -> Option<u64> {
None
}
}
#[test]
fn require_agent_id_returns_typed_error_when_unindexed() {
let reg = StubRegistry;
let err = require_agent_id("did:tenzro:machine:x", ®).unwrap_err();
let msg = format!("{}", err);
assert!(msg.contains("did:tenzro:machine:x"));
assert!(msg.contains("DAML"));
}
}