use alloy_primitives::{Address, B256, keccak256};
use cow_errors::CowError;
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
pub trait CowSigner: Send + Sync {
fn address(&self) -> Address;
async fn sign_typed_data(
&self,
domain_separator: B256,
struct_hash: B256,
) -> Result<Vec<u8>, CowError>;
async fn sign_message(&self, message: &[u8]) -> Result<Vec<u8>, CowError>;
}
fn map_alloy_signing_error(e: alloy_signer::Error) -> CowError {
CowError::Signing(e.to_string())
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CowSigner for alloy_signer_local::PrivateKeySigner {
fn address(&self) -> Address {
alloy_signer::Signer::address(self)
}
async fn sign_typed_data(
&self,
domain_separator: B256,
struct_hash: B256,
) -> Result<Vec<u8>, CowError> {
let mut msg = [0u8; 66];
msg[0] = 0x19;
msg[1] = 0x01;
msg[2..34].copy_from_slice(domain_separator.as_ref());
msg[34..66].copy_from_slice(struct_hash.as_ref());
let digest = keccak256(msg);
let sig = alloy_signer::Signer::sign_hash(self, &digest)
.await
.map_err(map_alloy_signing_error)?;
Ok(sig.as_bytes().to_vec())
}
async fn sign_message(&self, message: &[u8]) -> Result<Vec<u8>, CowError> {
let sig = alloy_signer::Signer::sign_message(self, message)
.await
.map_err(map_alloy_signing_error)?;
Ok(sig.as_bytes().to_vec())
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code; panic on unexpected state is acceptable"
)]
mod tests {
use alloy_signer_local::PrivateKeySigner;
use super::*;
const TEST_KEY: &str = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f99ae5f1e7b8a6c5e1";
fn signer() -> PrivateKeySigner {
TEST_KEY.parse().expect("valid test key")
}
#[tokio::test]
async fn private_key_signer_address_via_trait() {
let s = signer();
let direct = alloy_signer::Signer::address(&s);
let via_trait = <PrivateKeySigner as CowSigner>::address(&s);
assert_eq!(direct, via_trait);
}
#[tokio::test]
async fn private_key_signer_sign_typed_data_via_trait() {
let s = signer();
let domain = B256::from([0xaa; 32]);
let struct_hash = B256::from([0xbb; 32]);
let sig = <PrivateKeySigner as CowSigner>::sign_typed_data(&s, domain, struct_hash)
.await
.expect("signing should succeed");
assert_eq!(sig.len(), 65, "ECDSA signature is r || s || v = 65 bytes");
}
#[tokio::test]
async fn private_key_signer_sign_typed_data_is_deterministic() {
let s = signer();
let domain = B256::from([0x11; 32]);
let struct_hash = B256::from([0x22; 32]);
let a = <PrivateKeySigner as CowSigner>::sign_typed_data(&s, domain, struct_hash)
.await
.unwrap();
let b = <PrivateKeySigner as CowSigner>::sign_typed_data(&s, domain, struct_hash)
.await
.unwrap();
assert_eq!(a, b);
}
#[tokio::test]
async fn private_key_signer_sign_message_via_trait() {
let s = signer();
let sig = <PrivateKeySigner as CowSigner>::sign_message(&s, b"hello world")
.await
.expect("signing should succeed");
assert_eq!(sig.len(), 65);
}
#[tokio::test]
async fn private_key_signer_typed_and_message_signatures_differ() {
let s = signer();
let typed = <PrivateKeySigner as CowSigner>::sign_typed_data(
&s,
B256::from([0x33; 32]),
B256::from([0x44; 32]),
)
.await
.unwrap();
let msg =
<PrivateKeySigner as CowSigner>::sign_message(&s, b"different scheme").await.unwrap();
assert_ne!(typed, msg);
}
#[test]
fn map_alloy_signing_error_wraps_into_cow_signing() {
let err = super::map_alloy_signing_error(alloy_signer::Error::other("boom"));
assert!(matches!(&err, CowError::Signing(m) if m.contains("boom")), "got {err:?}");
}
}