use alloy_primitives::{Address, B256, keccak256};
use alloy_signer::Signer as _;
use alloy_signer_local::PrivateKeySigner;
use crate::{error::CowError, types::EcdsaSigningScheme};
pub async fn ecdsa_sign_typed_data(
scheme: EcdsaSigningScheme,
domain_sep: B256,
struct_hash: B256,
signer: &PrivateKeySigner,
) -> Result<String, CowError> {
let digest = match scheme {
EcdsaSigningScheme::Eip712 => {
let mut buf = [0u8; 66];
buf[0] = 0x19;
buf[1] = 0x01;
buf[2..34].copy_from_slice(domain_sep.as_slice());
buf[34..66].copy_from_slice(struct_hash.as_slice());
keccak256(buf)
}
EcdsaSigningScheme::EthSign => {
let mut typed_buf = [0u8; 66];
typed_buf[0] = 0x19;
typed_buf[1] = 0x01;
typed_buf[2..34].copy_from_slice(domain_sep.as_slice());
typed_buf[34..66].copy_from_slice(struct_hash.as_slice());
let typed_hash = keccak256(typed_buf);
let prefix = "\x19Ethereum Signed Message:\n32";
let mut msg = Vec::with_capacity(prefix.len() + 32);
msg.extend_from_slice(prefix.as_bytes());
msg.extend_from_slice(typed_hash.as_slice());
keccak256(&msg)
}
};
let sig = signer.sign_hash(&digest).await.map_err(|e| CowError::Signing(e.to_string()))?;
let sig_bytes = sig.as_bytes();
Ok(format!("0x{}", alloy_primitives::hex::encode(sig_bytes)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypedDataVersion {
V3,
V4,
}
impl TypedDataVersion {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::V3 => "v3",
Self::V4 => "v4",
}
}
}
#[derive(Debug, Clone)]
pub struct TypedDataVersionedSigner {
pub signer: PrivateKeySigner,
pub version: TypedDataVersion,
}
#[must_use]
pub const fn get_typed_data_versioned_signer(
signer: PrivateKeySigner,
version: TypedDataVersion,
) -> TypedDataVersionedSigner {
TypedDataVersionedSigner { signer, version }
}
#[must_use]
pub const fn get_typed_data_v3_signer(signer: PrivateKeySigner) -> TypedDataVersionedSigner {
get_typed_data_versioned_signer(signer, TypedDataVersion::V3)
}
#[must_use]
pub const fn get_int_chain_id_typed_data_v4_signer(
signer: PrivateKeySigner,
) -> TypedDataVersionedSigner {
get_typed_data_versioned_signer(signer, TypedDataVersion::V4)
}
const VAULT_ACTION_IDS: [&str; 4] = [
"0xeba777d811cd36c06d540d7ff2ed18ed042fd67bbf7c9afcf88c818c7ee6b498",
"0x1282ab709b2b70070f829c46bc36f76b32ad4989fecb2fcb09a1b3ce00bbfc30",
"0x78ad1b68d148c070372f8643c4648efbb63c6a8a338f3c24714868e791367653",
"0x7b8a1d293670124924a0f532213753b89db10bde737249d4540e9a03657d1aff",
];
#[must_use]
#[allow(clippy::type_complexity, reason = "return type matches domain contract encoding")]
pub fn grant_required_roles(
authorizer_address: Address,
vault_relayer_address: Address,
) -> Vec<(Address, Vec<u8>)> {
let selector = &keccak256("grantRole(bytes32,address)")[..4];
VAULT_ACTION_IDS
.iter()
.filter_map(|action_id_hex| {
let action_id =
alloy_primitives::hex::decode(action_id_hex.trim_start_matches("0x")).ok()?;
if action_id.len() != 32 {
return None;
}
let mut calldata = Vec::with_capacity(4 + 64);
calldata.extend_from_slice(selector);
calldata.extend_from_slice(&action_id);
let mut addr_word = [0u8; 32];
addr_word[12..32].copy_from_slice(vault_relayer_address.as_slice());
calldata.extend_from_slice(&addr_word);
Some((authorizer_address, calldata))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grant_required_roles_produces_correct_count() {
let authorizer: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
let relayer: Address = "0x2222222222222222222222222222222222222222".parse().unwrap();
let calls = grant_required_roles(authorizer, relayer);
assert_eq!(calls.len(), 4);
for (target, data) in &calls {
assert_eq!(*target, authorizer);
assert_eq!(data.len(), 68);
}
}
#[test]
fn typed_data_version_str() {
assert_eq!(TypedDataVersion::V3.as_str(), "v3");
assert_eq!(TypedDataVersion::V4.as_str(), "v4");
}
#[test]
fn typed_data_version_equality() {
assert_eq!(TypedDataVersion::V3, TypedDataVersion::V3);
assert_eq!(TypedDataVersion::V4, TypedDataVersion::V4);
assert_ne!(TypedDataVersion::V3, TypedDataVersion::V4);
}
#[test]
fn grant_required_roles_calldata_starts_with_selector() {
let authorizer: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
let relayer: Address = "0x2222222222222222222222222222222222222222".parse().unwrap();
let calls = grant_required_roles(authorizer, relayer);
let expected_selector = &keccak256("grantRole(bytes32,address)")[..4];
for (_, data) in &calls {
assert_eq!(&data[..4], expected_selector);
}
}
#[test]
fn grant_required_roles_embeds_relayer_address() {
let authorizer: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
let relayer: Address = "0x2222222222222222222222222222222222222222".parse().unwrap();
let calls = grant_required_roles(authorizer, relayer);
for (_, data) in &calls {
assert_eq!(&data[48..68], relayer.as_slice());
}
}
#[test]
fn get_typed_data_versioned_signer_v3() {
let signer = PrivateKeySigner::random();
let versioned = get_typed_data_versioned_signer(signer, TypedDataVersion::V3);
assert_eq!(versioned.version, TypedDataVersion::V3);
}
#[test]
fn get_typed_data_v3_signer_returns_v3() {
let signer = PrivateKeySigner::random();
let versioned = get_typed_data_v3_signer(signer);
assert_eq!(versioned.version, TypedDataVersion::V3);
}
#[test]
fn get_int_chain_id_typed_data_v4_signer_returns_v4() {
let signer = PrivateKeySigner::random();
let versioned = get_int_chain_id_typed_data_v4_signer(signer);
assert_eq!(versioned.version, TypedDataVersion::V4);
}
#[tokio::test]
async fn ecdsa_sign_typed_data_eip712() {
let signer = PrivateKeySigner::random();
let domain_sep = B256::ZERO;
let struct_hash = B256::ZERO;
let sig =
ecdsa_sign_typed_data(EcdsaSigningScheme::Eip712, domain_sep, struct_hash, &signer)
.await
.unwrap();
assert!(sig.starts_with("0x"));
assert_eq!(sig.len(), 132);
}
#[tokio::test]
async fn ecdsa_sign_typed_data_ethsign() {
let signer = PrivateKeySigner::random();
let domain_sep = B256::ZERO;
let struct_hash = B256::ZERO;
let sig =
ecdsa_sign_typed_data(EcdsaSigningScheme::EthSign, domain_sep, struct_hash, &signer)
.await
.unwrap();
assert!(sig.starts_with("0x"));
assert_eq!(sig.len(), 132);
}
#[tokio::test]
async fn ecdsa_sign_typed_data_different_schemes_produce_different_sigs() {
let signer = PrivateKeySigner::random();
let domain_sep = keccak256("test domain");
let struct_hash = keccak256("test struct");
let sig712 =
ecdsa_sign_typed_data(EcdsaSigningScheme::Eip712, domain_sep, struct_hash, &signer)
.await
.unwrap();
let sig_eth =
ecdsa_sign_typed_data(EcdsaSigningScheme::EthSign, domain_sep, struct_hash, &signer)
.await
.unwrap();
assert_ne!(sig712, sig_eth);
}
}