use sha2::{Sha256, Digest};
pub const SCHEMA_VERSION_V4: u8 = 4;
pub const DOMAIN_SEPARATOR_V4: &[u8] = b"signedby.me:identity:v4";
pub fn hash_field(prefix: &str, value: &str) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(format!("{}:{}", prefix, value).as_bytes());
hasher.finalize().into()
}
pub fn compute_binding_hash_v4(
did_pubkey: &[u8], wallet_address: &str,
client_id: &str,
session_id: &str,
payment_hash: &[u8], amount_sats: u64,
expires_at: u64,
nonce: &[u8], ea_domain: &str,
purpose_id: u8,
root_id: &str,
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(&[SCHEMA_VERSION_V4]);
let mut domain_sep = [0u8; 24];
let sep_len = DOMAIN_SEPARATOR_V4.len().min(24);
domain_sep[..sep_len].copy_from_slice(&DOMAIN_SEPARATOR_V4[..sep_len]);
hasher.update(&domain_sep);
let mut did_padded = [0u8; 33];
let did_len = did_pubkey.len().min(33);
did_padded[..did_len].copy_from_slice(&did_pubkey[..did_len]);
hasher.update(&did_padded);
hasher.update(&hash_field("wallet", wallet_address));
hasher.update(&hash_field("client_id", client_id));
hasher.update(&hash_field("session_id", session_id));
let mut payment_padded = [0u8; 32];
let payment_len = payment_hash.len().min(32);
payment_padded[..payment_len].copy_from_slice(&payment_hash[..payment_len]);
hasher.update(&payment_padded);
hasher.update(&amount_sats.to_le_bytes());
hasher.update(&expires_at.to_le_bytes());
let mut nonce_padded = [0u8; 16];
let nonce_len = nonce.len().min(16);
nonce_padded[..nonce_len].copy_from_slice(&nonce[..nonce_len]);
hasher.update(&nonce_padded);
hasher.update(&hash_field("ea_domain", ea_domain));
hasher.update(&[purpose_id]);
if root_id.is_empty() {
hasher.update(&[0u8; 32]);
} else {
hasher.update(&hash_field("root_id", root_id));
}
hasher.finalize().into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_field() {
let result = hash_field("client_id", "acme_corp");
assert_eq!(result.len(), 32);
let result2 = hash_field("client_id", "acme_corp");
assert_eq!(result, result2);
}
#[test]
fn test_binding_hash_basic() {
let did_pubkey = [0x02u8; 33]; let payment_hash = [0xaau8; 32];
let nonce = [0xbbu8; 16];
let hash = compute_binding_hash_v4(
&did_pubkey,
"did:btcr:test",
"acme_corp",
"session123",
&payment_hash,
500,
1700000000,
&nonce,
"acme.com",
1, "allowlist-2026-Q1",
);
assert_eq!(hash.len(), 32);
}
#[test]
fn test_binding_hash_no_membership() {
let did_pubkey = [0x02u8; 33];
let payment_hash = [0xaau8; 32];
let nonce = [0xbbu8; 16];
let hash = compute_binding_hash_v4(
&did_pubkey,
"did:btcr:test",
"acme_corp",
"session123",
&payment_hash,
500,
1700000000,
&nonce,
"acme.com",
0, "", );
assert_eq!(hash.len(), 32);
}
#[test]
fn test_different_inputs_different_hashes() {
let did_pubkey = [0x02u8; 33];
let payment_hash = [0xaau8; 32];
let nonce = [0xbbu8; 16];
let hash1 = compute_binding_hash_v4(
&did_pubkey,
"did:btcr:test",
"acme_corp",
"session123",
&payment_hash,
500,
1700000000,
&nonce,
"acme.com",
1,
"root1",
);
let hash2 = compute_binding_hash_v4(
&did_pubkey,
"did:btcr:test",
"acme_corp",
"session456", &payment_hash,
500,
1700000000,
&nonce,
"acme.com",
1,
"root1",
);
assert_ne!(hash1, hash2, "Different sessions should produce different hashes");
}
}