Skip to main content

self_agent_sdk/
registration.rs

1// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
2// SPDX-License-Identifier: BUSL-1.1
3// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
4
5//! Registration helpers for building `userDefinedData` strings and signing
6//! registration challenges.
7//!
8//! These functions produce the ASCII-encoded `userDefinedData` payloads that the
9//! Self Protocol Hub V2 expects when registering or deregistering an agent.
10//! They are used by the CLI binary and can also be called directly from
11//! library consumers.
12
13use alloy::primitives::{keccak256, Address, U256};
14use alloy::signers::local::PrivateKeySigner;
15use alloy::signers::Signer;
16use alloy::sol_types::SolValue;
17use serde::{Deserialize, Serialize};
18
19/// Disclosure flags that map to one of 6 verification configurations on-chain.
20///
21/// | `minimum_age` | `ofac`  | Config index |
22/// |---------------|---------|--------------|
23/// | 0             | false   | 0            |
24/// | 18            | false   | 1            |
25/// | 21            | false   | 2            |
26/// | 0             | true    | 3            |
27/// | 18            | true    | 4            |
28/// | 21            | true    | 5            |
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct RegistrationDisclosures {
31    pub minimum_age: u8,
32    pub ofac: bool,
33}
34
35/// The r, s, v components of an ECDSA signature.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct SignatureParts {
38    /// 0x-prefixed hex, 64 hex chars after prefix
39    pub r: String,
40    /// 0x-prefixed hex, 64 hex chars after prefix
41    pub s: String,
42    /// Recovery id — 27 or 28
43    pub v: u64,
44}
45
46/// Result of signing a registration challenge.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct SignedRegistrationChallenge {
49    /// 0x-prefixed keccak256 hash of the packed challenge
50    pub message_hash: String,
51    /// Signature components
52    pub parts: SignatureParts,
53    /// 0x-prefixed checksummed agent address derived from the signing key
54    pub agent_address: String,
55}
56
57/// Map disclosure flags to a config index (0–5).
58pub fn get_registration_config_index(disclosures: &RegistrationDisclosures) -> u8 {
59    match (disclosures.minimum_age, disclosures.ofac) {
60        (18, true) => 4,
61        (21, true) => 5,
62        (18, false) => 1,
63        (21, false) => 2,
64        (0, true) => 3,
65        _ => 0,
66    }
67}
68
69/// Compute the keccak256 hash of the registration challenge.
70///
71/// The challenge is `abi.encodePacked("self-agent-id:register:", humanIdentifier, chainId, registryAddress, nonce)`.
72///
73/// The `nonce` parameter is the agent's current registration nonce from `agentNonces(agent)`.
74/// Use 0 for first-time registrations.
75pub fn compute_registration_challenge_hash(
76    human_identifier: Address,
77    chain_id: u64,
78    registry_address: Address,
79    nonce: u64,
80) -> [u8; 32] {
81    let packed = (
82        "self-agent-id:register:".to_string(),
83        human_identifier,
84        U256::from(chain_id),
85        registry_address,
86        U256::from(nonce),
87    )
88        .abi_encode_packed();
89    keccak256(packed).into()
90}
91
92/// Sign the registration challenge with the agent's private key (EIP-191 personal sign).
93///
94/// Returns a [`SignedRegistrationChallenge`] containing the message hash,
95/// signature components, and derived agent address.
96///
97/// The `nonce` parameter is the agent's current registration nonce from `agentNonces(agent)`.
98/// Use 0 for first-time registrations.
99pub async fn sign_registration_challenge(
100    private_key: &str,
101    human_identifier: Address,
102    chain_id: u64,
103    registry_address: Address,
104    nonce: u64,
105) -> Result<SignedRegistrationChallenge, crate::Error> {
106    let signer: PrivateKeySigner = private_key
107        .parse::<PrivateKeySigner>()
108        .map_err(|_| crate::Error::InvalidPrivateKey)?;
109    let hash = compute_registration_challenge_hash(human_identifier, chain_id, registry_address, nonce);
110    let sig = signer
111        .sign_message(&hash)
112        .await
113        .map_err(|e| crate::Error::SigningError(e.to_string()))?;
114    let bytes = sig.as_bytes();
115    if bytes.len() != 65 {
116        return Err(crate::Error::InvalidSignature);
117    }
118    let mut v = bytes[64] as u64;
119    if v == 0 || v == 1 {
120        v += 27;
121    }
122
123    Ok(SignedRegistrationChallenge {
124        message_hash: format!("0x{}", hex::encode(hash)),
125        parts: SignatureParts {
126            r: format!("0x{}", hex::encode(&bytes[0..32])),
127            s: format!("0x{}", hex::encode(&bytes[32..64])),
128            v,
129        },
130        agent_address: format!("{:#x}", signer.address()),
131    })
132}
133
134/// Build `userDefinedData` for **simple (self-custody) registration**.
135///
136/// Format: `"R{config_index}"` — e.g. `"R0"`, `"R4"`.
137pub fn build_simple_register_user_data_ascii(disclosures: &RegistrationDisclosures) -> String {
138    format!("R{}", get_registration_config_index(disclosures))
139}
140
141/// Build `userDefinedData` for **simple (self-custody) deregistration**.
142///
143/// Format: `"D{config_index}"` — e.g. `"D0"`, `"D4"`.
144pub fn build_simple_deregister_user_data_ascii(disclosures: &RegistrationDisclosures) -> String {
145    format!("D{}", get_registration_config_index(disclosures))
146}
147
148/// Build `userDefinedData` for **advanced (linked) registration**.
149///
150/// Format: `"K{cfg}{addr40}{r64}{s64}{v2}"` where all hex is lowercase, no `0x` prefix.
151pub fn build_advanced_register_user_data_ascii(
152    agent_address: &str,
153    sig: &SignatureParts,
154    disclosures: &RegistrationDisclosures,
155) -> String {
156    let cfg = get_registration_config_index(disclosures);
157    let addr_hex = agent_address.trim_start_matches("0x").to_lowercase();
158    let r_hex = sig.r.trim_start_matches("0x").to_lowercase();
159    let s_hex = sig.s.trim_start_matches("0x").to_lowercase();
160    let v_hex = format!("{:02x}", sig.v);
161    format!("K{cfg}{addr_hex}{r_hex}{s_hex}{v_hex}")
162}
163
164/// Build `userDefinedData` for **advanced (linked) deregistration**.
165///
166/// Format: `"X{cfg}{addr40}"`.
167pub fn build_advanced_deregister_user_data_ascii(
168    agent_address: &str,
169    disclosures: &RegistrationDisclosures,
170) -> String {
171    let cfg = get_registration_config_index(disclosures);
172    let addr_hex = agent_address.trim_start_matches("0x").to_lowercase();
173    format!("X{cfg}{addr_hex}")
174}
175
176/// Build `userDefinedData` for **wallet-free registration** (agent-as-guardian).
177///
178/// Format: `"W{cfg}{addr40}{guardian40}{r64}{s64}{v2}"`.
179pub fn build_wallet_free_register_user_data_ascii(
180    agent_address: &str,
181    guardian_address: &str,
182    sig: &SignatureParts,
183    disclosures: &RegistrationDisclosures,
184) -> String {
185    let cfg = get_registration_config_index(disclosures);
186    let addr_hex = agent_address.trim_start_matches("0x").to_lowercase();
187    let guardian_hex = guardian_address.trim_start_matches("0x").to_lowercase();
188    let r_hex = sig.r.trim_start_matches("0x").to_lowercase();
189    let s_hex = sig.s.trim_start_matches("0x").to_lowercase();
190    let v_hex = format!("{:02x}", sig.v);
191    format!("W{cfg}{addr_hex}{guardian_hex}{r_hex}{s_hex}{v_hex}")
192}