use clear_signing::decoder::parse_signature;
use clear_signing::eip712::TypedData;
use clear_signing::provider::EmptyDataProvider;
use clear_signing::resolver::ResolvedDescriptor;
use clear_signing::token::{StaticTokenSource, TokenMeta};
use clear_signing::types::descriptor::Descriptor;
use clear_signing::DisplayEntry;
fn load_descriptor(fixture: &str) -> Descriptor {
let path = format!("{}/tests/fixtures/{fixture}", env!("CARGO_MANIFEST_DIR"));
let json = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
Descriptor::from_json(&json).unwrap_or_else(|e| panic!("parse {path}: {e}"))
}
fn address_word(hex_addr: &str) -> Vec<u8> {
let hex_str = hex_addr
.strip_prefix("0x")
.or_else(|| hex_addr.strip_prefix("0X"))
.unwrap_or(hex_addr);
let addr_bytes = hex::decode(hex_str).expect("valid hex address");
let mut word = vec![0u8; 12];
word.extend_from_slice(&addr_bytes);
assert_eq!(word.len(), 32);
word
}
fn uint_word(val: u128) -> Vec<u8> {
let mut word = vec![0u8; 16];
word.extend_from_slice(&val.to_be_bytes());
assert_eq!(word.len(), 32);
word
}
fn build_erc20_transfer_calldata(to: &str, amount: u128) -> Vec<u8> {
let sig = parse_signature("transfer(address,uint256)").unwrap();
let mut calldata = Vec::new();
calldata.extend_from_slice(&sig.selector);
calldata.extend_from_slice(&address_word(to));
calldata.extend_from_slice(&uint_word(amount));
calldata
}
fn build_execute_calldata(dest: &str, value: u128, inner_func: &[u8]) -> Vec<u8> {
let sig = parse_signature("execute(address,uint256,bytes)").unwrap();
let mut calldata = Vec::new();
calldata.extend_from_slice(&sig.selector);
calldata.extend_from_slice(&address_word(dest));
calldata.extend_from_slice(&uint_word(value));
calldata.extend_from_slice(&uint_word(96));
calldata.extend_from_slice(&uint_word(inner_func.len() as u128));
calldata.extend_from_slice(inner_func);
let pad = inner_func.len().div_ceil(32) * 32 - inner_func.len();
calldata.extend_from_slice(&vec![0u8; pad]);
calldata
}
fn build_userop_typed_data(sender: &str, call_data: &[u8]) -> TypedData {
let typed_data_json = serde_json::json!({
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"PackedUserOperation": [
{"name": "sender", "type": "address"},
{"name": "nonce", "type": "uint256"},
{"name": "callData", "type": "bytes"},
{"name": "preVerificationGas", "type": "uint256"}
]
},
"primaryType": "PackedUserOperation",
"domain": {
"name": "Account",
"version": "1",
"chainId": 1,
"verifyingContract": "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789"
},
"message": {
"sender": sender,
"nonce": "1",
"callData": format!("0x{}", hex::encode(call_data)),
"preVerificationGas": "21000"
}
});
serde_json::from_value(typed_data_json).unwrap()
}
#[tokio::test]
async fn userop_with_erc20_transfer_via_execute() {
let userop_descriptor = load_descriptor("userops-eip712.json");
let account_descriptor = load_descriptor("smart-account-execute.json");
let erc20_descriptor = load_descriptor("erc20-transfer.json");
let sender = "0x1111111111111111111111111111111111111111";
let usdc_addr = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
let recipient = "0x2222222222222222222222222222222222222222";
let transfer_calldata = build_erc20_transfer_calldata(recipient, 5_000_000);
let execute_calldata = build_execute_calldata(usdc_addr, 0, &transfer_calldata);
let typed_data = build_userop_typed_data(sender, &execute_calldata);
let descriptors = vec![
ResolvedDescriptor {
descriptor: account_descriptor,
chain_id: 1,
address: sender.to_string(),
},
ResolvedDescriptor {
descriptor: erc20_descriptor,
chain_id: 1,
address: usdc_addr.to_string(),
},
];
let mut tokens = StaticTokenSource::new();
tokens.insert(
1,
usdc_addr,
TokenMeta {
symbol: "USDC".to_string(),
decimals: 6,
name: "USD Coin".to_string(),
},
);
let mut all_descriptors = vec![ResolvedDescriptor {
descriptor: userop_descriptor,
chain_id: 1,
address: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789".to_string(),
}];
all_descriptors.extend(descriptors);
let result = clear_signing::format_typed_data(&all_descriptors, &typed_data, &tokens)
.await
.unwrap();
assert_eq!(result.intent, "Sign Packed User Operation");
assert!(
result
.interpolated_intent
.as_ref()
.unwrap()
.contains(sender),
"interpolated intent should contain sender: {:?}",
result.interpolated_intent
);
if let DisplayEntry::Item(ref item) = result.entries[0] {
assert_eq!(item.label, "Sender Account");
assert!(
item.value
.to_lowercase()
.contains(&sender[2..].to_lowercase()),
"sender value should contain sender address: {}",
item.value
);
} else {
panic!(
"expected Item for Sender Account, got {:?}",
result.entries[0]
);
}
match &result.entries[1] {
DisplayEntry::Nested {
label,
intent,
entries,
..
} => {
assert_eq!(label, "Embedded Call");
assert_eq!(intent, "Execute call");
assert!(
entries.len() >= 2,
"expected at least 2 entries in execute, got {}",
entries.len()
);
if let DisplayEntry::Item(ref item) = entries[0] {
assert_eq!(item.label, "Destination");
assert!(
item.value
.to_lowercase()
.contains("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
"destination should contain USDC address: {}",
item.value
);
} else {
panic!("expected Item for Destination");
}
let inner_call_idx = entries.len() - 1; match &entries[inner_call_idx] {
DisplayEntry::Nested {
label,
intent,
entries: inner_entries,
..
} => {
assert_eq!(label, "Inner Call");
assert_eq!(intent, "Transfer tokens");
if let DisplayEntry::Item(ref item) = inner_entries[0] {
assert_eq!(item.label, "To");
}
if let DisplayEntry::Item(ref item) = inner_entries[1] {
assert_eq!(item.label, "Amount");
assert_eq!(item.value, "5 USDC");
}
}
other => {
panic!("expected Nested for Inner Call, got {:?}", other);
}
}
}
other => {
panic!("expected Nested for Embedded Call, got {:?}", other);
}
}
}
#[tokio::test]
async fn userop_direct_erc20_transfer() {
let userop_descriptor = load_descriptor("userops-eip712.json");
let erc20_descriptor = load_descriptor("erc20-transfer.json");
let usdc_addr = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
let recipient = "0x3333333333333333333333333333333333333333";
let transfer_calldata = build_erc20_transfer_calldata(recipient, 2_000_000); let typed_data = build_userop_typed_data(usdc_addr, &transfer_calldata);
let descriptors = vec![ResolvedDescriptor {
descriptor: erc20_descriptor,
chain_id: 1,
address: usdc_addr.to_string(),
}];
let mut tokens = StaticTokenSource::new();
tokens.insert(
1,
usdc_addr,
TokenMeta {
symbol: "USDC".to_string(),
decimals: 6,
name: "USD Coin".to_string(),
},
);
let mut all_descriptors = vec![ResolvedDescriptor {
descriptor: userop_descriptor,
chain_id: 1,
address: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789".to_string(),
}];
all_descriptors.extend(descriptors);
let result = clear_signing::format_typed_data(&all_descriptors, &typed_data, &tokens)
.await
.unwrap();
assert_eq!(result.intent, "Sign Packed User Operation");
match &result.entries[1] {
DisplayEntry::Nested {
label,
intent,
entries,
..
} => {
assert_eq!(label, "Embedded Call");
assert_eq!(intent, "Transfer tokens");
if let DisplayEntry::Item(ref item) = entries[1] {
assert_eq!(item.label, "Amount");
assert_eq!(item.value, "2 USDC");
}
}
other => {
panic!("expected Nested for Embedded Call, got {:?}", other);
}
}
}
#[tokio::test]
async fn userop_no_matching_inner_descriptor() {
let userop_descriptor = load_descriptor("userops-eip712.json");
let unknown_sender = "0x9999999999999999999999999999999999999999";
let random_calldata =
hex::decode("12345678000000000000000000000000000000000000000000000000000000000000002a")
.unwrap();
let typed_data = build_userop_typed_data(unknown_sender, &random_calldata);
let all_descriptors = vec![ResolvedDescriptor {
descriptor: userop_descriptor,
chain_id: 1,
address: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789".to_string(),
}];
let result =
clear_signing::format_typed_data(&all_descriptors, &typed_data, &EmptyDataProvider)
.await
.unwrap();
assert_eq!(result.intent, "Sign Packed User Operation");
assert_eq!(
result.fallback_reason(),
Some(&clear_signing::FallbackReason::NestedCallNotClearSigned)
);
match &result.entries[1] {
DisplayEntry::Nested { label, intent, .. } => {
assert_eq!(label, "Embedded Call");
assert!(
intent.contains("Unknown function"),
"expected raw fallback, got: {intent}"
);
}
other => {
panic!("expected Nested for Embedded Call, got {:?}", other);
}
}
}
#[tokio::test]
async fn userop_hash_prefix_resolves_from_message() {
let userop_descriptor = load_descriptor("userops-eip712.json");
let erc20_descriptor = load_descriptor("erc20-transfer.json");
let sender = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
let recipient = "0x4444444444444444444444444444444444444444";
let transfer_calldata = build_erc20_transfer_calldata(recipient, 1_000_000);
let typed_data = build_userop_typed_data(sender, &transfer_calldata);
let descriptors = vec![ResolvedDescriptor {
descriptor: erc20_descriptor,
chain_id: 1,
address: sender.to_string(), }];
let mut all_descriptors = vec![ResolvedDescriptor {
descriptor: userop_descriptor,
chain_id: 1,
address: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789".to_string(),
}];
all_descriptors.extend(descriptors);
let result =
clear_signing::format_typed_data(&all_descriptors, &typed_data, &EmptyDataProvider)
.await
.unwrap();
match &result.entries[1] {
DisplayEntry::Nested {
intent, entries, ..
} => {
assert_eq!(intent, "Transfer tokens");
assert!(entries.len() >= 2);
if let DisplayEntry::Item(ref item) = entries[0] {
assert_eq!(item.label, "To");
}
}
other => {
panic!(
"expected Nested with Transfer tokens intent, got {:?}",
other
);
}
}
}