extern crate alloc;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use sha3::{Digest, Keccak256};
use aethel_core::signing::purpose;
use aethel_core::signing::Identity;
use crate::error::VaultError;
use crate::policy::{Decision, HitlApproval, PolicyState, Rail, SpendPolicy, SpendRequest};
fn keccak256(data: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(data);
let out = hasher.finalize();
let mut arr = [0u8; 32];
arr.copy_from_slice(&out);
arr
}
fn pad_left_32(bytes: &[u8]) -> [u8; 32] {
debug_assert!(bytes.len() <= 32);
let mut out = [0u8; 32];
let start = 32 - bytes.len();
out[start..].copy_from_slice(bytes);
out
}
fn abi_address(addr: &[u8; 20]) -> [u8; 32] {
pad_left_32(addr)
}
fn abi_u128(v: u128) -> [u8; 32] {
pad_left_32(&v.to_be_bytes())
}
fn abi_u64_as_u256(v: u64) -> [u8; 32] {
pad_left_32(&v.to_be_bytes())
}
fn parse_hex_address(hex_str: &str) -> [u8; 20] {
let hex_str = hex_str.trim_start_matches("0x");
let mut out = [0u8; 20];
for i in 0..20 {
out[i] = u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16).expect("valid hex address");
}
out
}
fn to_hex_lower(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{:02x}", b));
}
s
}
fn domain_type_hash() -> [u8; 32] {
keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
}
fn transfer_with_authorization_type_hash() -> [u8; 32] {
keccak256(
b"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)",
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Chain {
Base,
Ethereum,
ArbitrumOne,
}
impl Chain {
pub fn chain_id(&self) -> u64 {
match self {
Chain::Base => 8453,
Chain::Ethereum => 1,
Chain::ArbitrumOne => 42161,
}
}
pub fn domain_name(&self) -> &'static str {
"USD Coin"
}
pub fn domain_version(&self) -> &'static str {
"2"
}
pub fn usdc_contract(&self) -> [u8; 20] {
let hex_str = match self {
Chain::Base => "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
Chain::Ethereum => "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
Chain::ArbitrumOne => "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
};
parse_hex_address(hex_str)
}
pub fn rail(&self) -> Rail {
match self {
Chain::Base => Rail::Eip3009Base,
Chain::Ethereum => Rail::Eip3009Ethereum,
Chain::ArbitrumOne => Rail::Eip3009Arbitrum,
}
}
pub fn domain_separator(&self) -> [u8; 32] {
let mut buf = Vec::with_capacity(32 * 5);
buf.extend_from_slice(&domain_type_hash());
buf.extend_from_slice(&keccak256(self.domain_name().as_bytes()));
buf.extend_from_slice(&keccak256(self.domain_version().as_bytes()));
buf.extend_from_slice(&abi_u64_as_u256(self.chain_id()));
buf.extend_from_slice(&abi_address(&self.usdc_contract()));
keccak256(&buf)
}
}
mod sig65 {
use alloc::vec::Vec;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(sig: &[u8; 65], s: S) -> Result<S::Ok, S::Error> {
sig.as_slice().serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 65], D::Error> {
let v: Vec<u8> = Vec::deserialize(d)?;
if v.len() != 65 {
return Err(serde::de::Error::custom("expected exactly 65 bytes"));
}
let mut out = [0u8; 65];
out.copy_from_slice(&v);
Ok(out)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransferWithAuthorization {
pub from: [u8; 20],
pub to: [u8; 20],
pub value: u128,
pub valid_after: u64,
pub valid_before: u64,
pub nonce: [u8; 32],
}
impl TransferWithAuthorization {
fn struct_hash(&self) -> [u8; 32] {
let mut buf = Vec::with_capacity(32 * 7);
buf.extend_from_slice(&transfer_with_authorization_type_hash());
buf.extend_from_slice(&abi_address(&self.from));
buf.extend_from_slice(&abi_address(&self.to));
buf.extend_from_slice(&abi_u128(self.value));
buf.extend_from_slice(&abi_u64_as_u256(self.valid_after));
buf.extend_from_slice(&abi_u64_as_u256(self.valid_before));
buf.extend_from_slice(&self.nonce);
keccak256(&buf)
}
pub fn eip712_digest(&self, chain: Chain) -> [u8; 32] {
let domain_separator = chain.domain_separator();
let struct_hash = self.struct_hash();
let mut buf = Vec::with_capacity(2 + 32 + 32);
buf.push(0x19);
buf.push(0x01);
buf.extend_from_slice(&domain_separator);
buf.extend_from_slice(&struct_hash);
keccak256(&buf)
}
}
pub fn wallet_bind_message(
agent_did: &str,
chain: Chain,
address: &[u8; 20],
nonce: &[u8; 32],
) -> String {
format!(
"SAGP wallet binding\nagent_did: {}\nchain: {}\naddress: 0x{}\nnonce: 0x{}",
agent_did,
chain.chain_id(),
to_hex_lower(address),
to_hex_lower(nonce)
)
}
pub fn eip191_wallet_bind_digest(
agent_did: &str,
chain: Chain,
address: &[u8; 20],
nonce: &[u8; 32],
) -> [u8; 32] {
let msg = wallet_bind_message(agent_did, chain, address, nonce);
let prefix = format!("\x19Ethereum Signed Message:\n{}", msg.len());
let mut buf = Vec::with_capacity(prefix.len() + msg.len());
buf.extend_from_slice(prefix.as_bytes());
buf.extend_from_slice(msg.as_bytes());
keccak256(&buf)
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
pub fn wallet_bind_canonical_json(
agent_did: &str,
chain: Chain,
address: &[u8; 20],
nonce: &[u8; 32],
) -> String {
format!(
"{{\"agent_did\":\"{}\",\"chain_id\":{},\"address\":\"0x{}\",\"nonce\":\"0x{}\"}}",
json_escape(agent_did),
chain.chain_id(),
to_hex_lower(address),
to_hex_lower(nonce)
)
}
pub fn sign_wallet_bind(
identity: &Identity,
agent_did: &str,
chain: Chain,
address: &[u8; 20],
nonce: &[u8; 32],
) -> Result<Vec<u8>, VaultError> {
let json = wallet_bind_canonical_json(agent_did, chain, address, nonce);
identity
.sign_with_purpose(purpose::VAULT_WALLET_BIND_V1, json.as_bytes())
.map_err(VaultError::from)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettlementError {
SignerRejected,
}
pub trait SettlementSigner {
fn sign_digest(&self, digest: &[u8; 32]) -> Result<[u8; 65], SettlementError>;
fn address(&self) -> [u8; 20];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpendIntent {
pub chain_id: u64,
pub from: [u8; 20],
pub to: [u8; 20],
pub value: u128,
pub valid_after: u64,
pub valid_before: u64,
pub nonce: [u8; 32],
pub eip712_digest: [u8; 32],
}
impl SpendIntent {
fn from_authorization(auth: &TransferWithAuthorization, chain: Chain) -> Self {
SpendIntent {
chain_id: chain.chain_id(),
from: auth.from,
to: auth.to,
value: auth.value,
valid_after: auth.valid_after,
valid_before: auth.valid_before,
nonce: auth.nonce,
eip712_digest: auth.eip712_digest(chain),
}
}
fn to_bytes(&self) -> Result<Vec<u8>, VaultError> {
bincode::serialize(self).map_err(|_| VaultError::SerializationError)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettlementAuthorization {
pub authorization: TransferWithAuthorization,
pub chain: Chain,
pub eip712_digest: [u8; 32],
#[serde(with = "sig65")]
pub external_signature: [u8; 65],
pub intent: SpendIntent,
pub intent_signature: Vec<u8>,
pub intent_signer_pk: Vec<u8>,
}
pub struct Wallet<S: SettlementSigner> {
signer: S,
identity: Identity,
policy: SpendPolicy,
policy_state: PolicyState,
last_receipt: Option<crate::receipt::Receipt>,
}
impl<S: SettlementSigner> Wallet<S> {
pub fn new(signer: S, identity: Identity, policy: SpendPolicy) -> Self {
Wallet {
signer,
identity,
policy,
policy_state: PolicyState::default(),
last_receipt: None,
}
}
pub fn policy_state(&self) -> &PolicyState {
&self.policy_state
}
pub fn public_key(&self) -> Vec<u8> {
self.identity.public_key()
}
pub fn authorize_eip3009(
&mut self,
to: [u8; 20],
value: u128,
nonce: [u8; 32],
chain: Chain,
now_unix: u64,
hitl: Option<&HitlApproval>,
) -> Result<SettlementAuthorization, VaultError> {
let from = self.signer.address();
let valid_after = now_unix;
let valid_before = now_unix.saturating_add(self.policy.ttl_secs);
let auth = TransferWithAuthorization {
from,
to,
value,
valid_after,
valid_before,
nonce,
};
let digest = auth.eip712_digest(chain);
let req = SpendRequest {
rail: chain.rail(),
amount: value,
to,
};
let decision = self
.policy
.check(&self.policy_state, &req, now_unix)
.map_err(VaultError::from)?;
if decision == Decision::RequireHitl {
let approved = match hitl {
Some(a) => a
.verify(
&digest,
value,
self.policy.hitl_approver_pk.as_deref(),
now_unix,
)
.map_err(VaultError::from)?,
None => false,
};
if !approved {
return Err(VaultError::HitlRequired);
}
}
let external_signature = self.signer.sign_digest(&digest).map_err(VaultError::from)?;
let intent = SpendIntent::from_authorization(&auth, chain);
let intent_bytes = intent.to_bytes()?;
let intent_signature = self
.identity
.sign_with_purpose(purpose::VAULT_SPEND_INTENT_V1, &intent_bytes)
.map_err(VaultError::from)?;
self.policy_state.record(value, now_unix);
let receipt = crate::receipt::Receipt {
auth_hash: digest,
amount: value,
dst: to,
ts: now_unix,
chain_id: chain.chain_id(),
};
self.last_receipt = Some(receipt);
Ok(SettlementAuthorization {
authorization: auth,
chain,
eip712_digest: digest,
external_signature,
intent,
intent_signature,
intent_signer_pk: self.identity.public_key(),
})
}
pub fn last_receipt(&self) -> Option<&crate::receipt::Receipt> {
self.last_receipt.as_ref()
}
pub fn sign_receipt(
&self,
receipt: &crate::receipt::Receipt,
) -> Result<crate::receipt::SignedReceipt, VaultError> {
receipt.sign(&self.identity)
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::SettlementError;
use super::SettlementSigner;
pub struct MockSigner {
pub address: [u8; 20],
pub fixed_signature: [u8; 65],
pub reject: bool,
}
impl MockSigner {
pub fn new(address: [u8; 20]) -> Self {
MockSigner {
address,
fixed_signature: [0x42u8; 65],
reject: false,
}
}
pub fn rejecting(address: [u8; 20]) -> Self {
MockSigner {
address,
fixed_signature: [0u8; 65],
reject: true,
}
}
}
impl SettlementSigner for MockSigner {
fn sign_digest(&self, _digest: &[u8; 32]) -> Result<[u8; 65], SettlementError> {
if self.reject {
Err(SettlementError::SignerRejected)
} else {
Ok(self.fixed_signature)
}
}
fn address(&self) -> [u8; 20] {
self.address
}
}
}
#[cfg(test)]
mod tests {
use super::test_support::MockSigner;
use super::*;
use aethel_core::signing::Identity;
use alloc::vec;
#[test]
fn keccak256_of_usd_coin_matches_self_computed_value() {
let expected =
hex_literal(b"52878b207aaddbfc15ea7bebcda681eb8ccd306e2227b61cef68505c8c056341");
let got = keccak256(b"USD Coin");
assert_eq!(got, expected, "keccak256(\"USD Coin\") changed");
}
#[test]
fn keccak256_of_version_2_matches_self_computed_value() {
let expected =
hex_literal(b"ad7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5");
let got = keccak256(b"2");
assert_eq!(got, expected, "keccak256(\"2\") changed");
}
#[test]
fn domain_type_hash_matches_self_computed_value() {
let expected =
hex_literal(b"8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f");
assert_eq!(domain_type_hash(), expected);
}
#[test]
fn transfer_type_hash_matches_self_computed_value() {
let expected =
hex_literal(b"7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267");
assert_eq!(transfer_with_authorization_type_hash(), expected);
}
#[test]
fn keccak256_is_deterministic_and_sensitive_to_input() {
assert_eq!(keccak256(b"USD Coin"), keccak256(b"USD Coin"));
assert_ne!(keccak256(b"USD Coin"), keccak256(b"2"));
assert_ne!(keccak256(b""), keccak256(b" "));
}
#[test]
fn domain_type_hash_is_stable_across_calls() {
assert_eq!(domain_type_hash(), domain_type_hash());
}
#[test]
fn transfer_type_hash_is_stable_across_calls() {
assert_eq!(
transfer_with_authorization_type_hash(),
transfer_with_authorization_type_hash()
);
}
#[test]
fn domain_type_hash_differs_from_transfer_type_hash() {
assert_ne!(domain_type_hash(), transfer_with_authorization_type_hash());
}
#[test]
fn base_domain_separator_matches_the_published_value() {
let separator = Chain::Base.domain_separator();
let domain_type_hash = keccak256(
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
);
let name_hash = keccak256(b"USD Coin");
let version_hash = keccak256(b"2");
let chain_id_word = pad_left_32(&8453u64.to_be_bytes());
let contract_word = pad_left_32(&Chain::Base.usdc_contract());
let mut buf = Vec::with_capacity(32 * 5);
buf.extend_from_slice(&domain_type_hash);
buf.extend_from_slice(&name_hash);
buf.extend_from_slice(&version_hash);
buf.extend_from_slice(&chain_id_word);
buf.extend_from_slice(&contract_word);
let recomputed = keccak256(&buf);
assert_eq!(
separator, recomputed,
"Chain::domain_separator diverged from an independent recomputation"
);
}
#[test]
fn different_chains_produce_different_domain_separators() {
assert_ne!(
Chain::Base.domain_separator(),
Chain::Ethereum.domain_separator()
);
assert_ne!(
Chain::Base.domain_separator(),
Chain::ArbitrumOne.domain_separator()
);
assert_ne!(
Chain::Ethereum.domain_separator(),
Chain::ArbitrumOne.domain_separator()
);
}
#[test]
fn usdc_contract_addresses_match_the_table() {
assert_eq!(
to_hex_lower(&Chain::Base.usdc_contract()),
"833589fcd6edb6e08f4c7c32d4f71b54bda02913"
);
assert_eq!(
to_hex_lower(&Chain::Ethereum.usdc_contract()),
"a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
);
assert_eq!(
to_hex_lower(&Chain::ArbitrumOne.usdc_contract()),
"af88d065e77c8cc2239327c5edb3a432268e5831"
);
}
#[test]
fn eip712_digest_is_deterministic() {
let auth = TransferWithAuthorization {
from: [0x11u8; 20],
to: [0x22u8; 20],
value: 1_000_000,
valid_after: 0,
valid_before: 1_000,
nonce: [0x33u8; 32],
};
let a = auth.eip712_digest(Chain::Base);
let b = auth.eip712_digest(Chain::Base);
assert_eq!(a, b);
}
#[test]
fn eip712_digest_differs_across_chains() {
let auth = TransferWithAuthorization {
from: [0x11u8; 20],
to: [0x22u8; 20],
value: 1_000_000,
valid_after: 0,
valid_before: 1_000,
nonce: [0x33u8; 32],
};
assert_ne!(
auth.eip712_digest(Chain::Base),
auth.eip712_digest(Chain::Ethereum)
);
}
#[test]
fn eip712_digest_differs_when_any_field_changes() {
let base = TransferWithAuthorization {
from: [0x11u8; 20],
to: [0x22u8; 20],
value: 1_000_000,
valid_after: 0,
valid_before: 1_000,
nonce: [0x33u8; 32],
};
let base_digest = base.eip712_digest(Chain::Base);
let mut changed_value = base.clone();
changed_value.value += 1;
assert_ne!(base_digest, changed_value.eip712_digest(Chain::Base));
let mut changed_nonce = base.clone();
changed_nonce.nonce[0] ^= 1;
assert_ne!(base_digest, changed_nonce.eip712_digest(Chain::Base));
}
#[test]
fn wallet_bind_digest_is_deterministic() {
let addr = [0xAAu8; 20];
let nonce = [0xBBu8; 32];
let a = eip191_wallet_bind_digest("did:aethel:abc", Chain::Base, &addr, &nonce);
let b = eip191_wallet_bind_digest("did:aethel:abc", Chain::Base, &addr, &nonce);
assert_eq!(a, b);
}
#[test]
fn wallet_bind_digest_differs_by_agent_did() {
let addr = [0xAAu8; 20];
let nonce = [0xBBu8; 32];
let a = eip191_wallet_bind_digest("did:aethel:abc", Chain::Base, &addr, &nonce);
let b = eip191_wallet_bind_digest("did:aethel:xyz", Chain::Base, &addr, &nonce);
assert_ne!(a, b);
}
#[test]
fn wallet_bind_canonical_json_is_well_formed_and_stable() {
let addr = [0xAAu8; 20];
let nonce = [0xBBu8; 32];
let json = wallet_bind_canonical_json("did:aethel:abc", Chain::Base, &addr, &nonce);
assert!(json.starts_with("{\"agent_did\":\"did:aethel:abc\""));
assert!(json.contains("\"chain_id\":8453"));
assert_eq!(
json,
wallet_bind_canonical_json("did:aethel:abc", Chain::Base, &addr, &nonce)
);
}
#[test]
fn manifest_contains_no_ecdsa_crates() {
let manifest = include_str!("../Cargo.toml");
for forbidden in ["k256", "secp256k1", "alloy", "ethers"] {
let declared_as_dependency = manifest.lines().map(str::trim_start).any(|line| {
!line.starts_with('#')
&& (line.starts_with(&format!("{forbidden} "))
|| line.starts_with(&format!("{forbidden}.")))
&& line.contains('=')
});
assert!(
!declared_as_dependency,
"Cargo.toml declares forbidden dependency `{forbidden}` — \
secp256k1 must stay outside aethel-vault (SAGP-PG-001, LOCKED RULE 1)"
);
}
}
fn test_policy(hitl_above: u128) -> SpendPolicy {
SpendPolicy {
max_per_call: 10_000_000,
daily_cap: 100_000_000,
allow_rails: vec![Rail::Eip3009Base],
hitl_above,
ttl_secs: 3600,
hitl_approver_pk: None,
}
}
#[test]
fn authorize_eip3009_produces_both_signatures() {
let signer = MockSigner::new([0x01u8; 20]);
let identity = Identity::generate(&[0x55u8; 32]).expect("generate");
let mut wallet = Wallet::new(signer, identity, test_policy(1_000_000));
let result = wallet
.authorize_eip3009(
[0x02u8; 20],
500_000,
[0x03u8; 32],
Chain::Base,
1_000,
None,
)
.expect("authorize");
assert_eq!(result.external_signature, [0x42u8; 65]);
assert!(!result.intent_signature.is_empty());
assert_eq!(result.authorization.from, [0x01u8; 20]);
assert_eq!(result.authorization.valid_after, 1_000);
assert_eq!(result.authorization.valid_before, 1_000 + 3600);
let intent_bytes = bincode::serialize(&result.intent).unwrap();
assert_eq!(
aethel_core::signing::verify_with_purpose(
&result.intent_signer_pk,
purpose::VAULT_SPEND_INTENT_V1,
&intent_bytes,
&result.intent_signature,
),
Ok(true)
);
}
#[test]
fn authorize_eip3009_denied_by_policy_produces_no_signature() {
let signer = MockSigner::new([0x01u8; 20]);
let identity = Identity::generate(&[0x55u8; 32]).expect("generate");
let mut policy = test_policy(1_000_000);
policy.max_per_call = 100; let mut wallet = Wallet::new(signer, identity, policy);
let result = wallet.authorize_eip3009(
[0x02u8; 20],
500_000,
[0x03u8; 32],
Chain::Base,
1_000,
None,
);
assert!(matches!(
result,
Err(VaultError::PolicyDenied(
crate::policy::PolicyViolation::ExceedsMaxPerCall
))
));
}
#[test]
fn authorize_eip3009_requires_hitl_when_above_threshold() {
let signer = MockSigner::new([0x01u8; 20]);
let identity = Identity::generate(&[0x55u8; 32]).expect("generate");
let mut wallet = Wallet::new(signer, identity, test_policy(100));
let result = wallet.authorize_eip3009(
[0x02u8; 20],
500_000,
[0x03u8; 32],
Chain::Base,
1_000,
None,
);
assert_eq!(result, Err(VaultError::HitlRequired));
}
#[test]
fn authorize_eip3009_with_valid_hitl_approval_succeeds() {
let signer = MockSigner::new([0x01u8; 20]);
let identity = Identity::generate(&[0x55u8; 32]).expect("generate");
let approver = Identity::generate(&[0x66u8; 32]).expect("generate");
let mut policy = test_policy(100);
policy.hitl_approver_pk = Some(approver.public_key());
let mut wallet = Wallet::new(signer, identity, policy);
let auth = TransferWithAuthorization {
from: [0x01u8; 20],
to: [0x02u8; 20],
value: 500_000,
valid_after: 1_000,
valid_before: 1_000 + 3600,
nonce: [0x03u8; 32],
};
let digest = auth.eip712_digest(Chain::Base);
let msg = crate::policy::hitl_approval_message(&digest, 500_000, 10_000);
let sig = approver
.sign_with_purpose(purpose::VAULT_HITL_APPROVAL_V1, &msg)
.unwrap();
let approval = HitlApproval {
approver_pk: approver.public_key(),
signature: sig,
intent_hash: digest,
amount: 500_000,
expiry_unix: 10_000,
};
let result = wallet.authorize_eip3009(
[0x02u8; 20],
500_000,
[0x03u8; 32],
Chain::Base,
1_000,
Some(&approval),
);
assert!(result.is_ok(), "{:?}", result.err());
}
#[test]
fn self_signed_hitl_approval_is_refused_without_configured_approver() {
let signer = MockSigner::new([0x01u8; 20]);
let identity = Identity::generate(&[0x55u8; 32]).expect("generate");
let mut wallet = Wallet::new(signer, identity, test_policy(100));
let auth = TransferWithAuthorization {
from: [0x01u8; 20],
to: [0x02u8; 20],
value: 500_000,
valid_after: 1_000,
valid_before: 1_000 + 3600,
nonce: [0x03u8; 32],
};
let digest = auth.eip712_digest(Chain::Base);
let rogue = Identity::generate(&[0x99u8; 32]).expect("generate");
let msg = crate::policy::hitl_approval_message(&digest, 500_000, 10_000);
let approval = HitlApproval {
approver_pk: rogue.public_key(),
signature: rogue
.sign_with_purpose(purpose::VAULT_HITL_APPROVAL_V1, &msg)
.unwrap(),
intent_hash: digest,
amount: 500_000,
expiry_unix: 10_000,
};
let result = wallet.authorize_eip3009(
[0x02u8; 20],
500_000,
[0x03u8; 32],
Chain::Base,
1_000,
Some(&approval),
);
assert_eq!(result, Err(VaultError::HitlRequired));
}
#[test]
fn authorize_eip3009_propagates_signer_rejection() {
let signer = MockSigner::rejecting([0x01u8; 20]);
let identity = Identity::generate(&[0x55u8; 32]).expect("generate");
let mut wallet = Wallet::new(signer, identity, test_policy(1_000_000));
let result = wallet.authorize_eip3009(
[0x02u8; 20],
500_000,
[0x03u8; 32],
Chain::Base,
1_000,
None,
);
assert_eq!(
result,
Err(VaultError::SignerFailure(SettlementError::SignerRejected))
);
}
#[test]
fn no_receipt_is_produced_unless_requested() {
let signer = MockSigner::new([0x01u8; 20]);
let identity = Identity::generate(&[0x55u8; 32]).expect("generate");
let wallet = Wallet::new(signer, identity, test_policy(1_000_000));
assert!(wallet.last_receipt().is_none());
}
fn hex_literal(hex: &[u8]) -> [u8; 32] {
let s = core::str::from_utf8(hex).unwrap();
let mut out = [0u8; 32];
for i in 0..32 {
out[i] = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap();
}
out
}
}