anvil/eth/
sign.rs

1use crate::eth::error::BlockchainError;
2use alloy_consensus::SignableTransaction;
3use alloy_dyn_abi::TypedData;
4use alloy_network::TxSignerSync;
5use alloy_primitives::{map::AddressHashMap, Address, PrimitiveSignature as Signature, B256, U256};
6use alloy_signer::Signer as AlloySigner;
7use alloy_signer_local::PrivateKeySigner;
8use anvil_core::eth::transaction::{
9    optimism::DepositTransaction, TypedTransaction, TypedTransactionRequest,
10};
11use op_alloy_consensus::TxDeposit;
12
13/// A transaction signer
14#[async_trait::async_trait]
15pub trait Signer: Send + Sync {
16    /// returns the available accounts for this signer
17    fn accounts(&self) -> Vec<Address>;
18
19    /// Returns `true` whether this signer can sign for this address
20    fn is_signer_for(&self, addr: Address) -> bool {
21        self.accounts().contains(&addr)
22    }
23
24    /// Returns the signature
25    async fn sign(&self, address: Address, message: &[u8]) -> Result<Signature, BlockchainError>;
26
27    /// Encodes and signs the typed data according EIP-712. Payload must conform to the EIP-712
28    /// standard.
29    async fn sign_typed_data(
30        &self,
31        address: Address,
32        payload: &TypedData,
33    ) -> Result<Signature, BlockchainError>;
34
35    /// Signs the given hash.
36    async fn sign_hash(&self, address: Address, hash: B256) -> Result<Signature, BlockchainError>;
37
38    /// signs a transaction request using the given account in request
39    fn sign_transaction(
40        &self,
41        request: TypedTransactionRequest,
42        address: &Address,
43    ) -> Result<Signature, BlockchainError>;
44}
45
46/// Maintains developer keys
47pub struct DevSigner {
48    addresses: Vec<Address>,
49    accounts: AddressHashMap<PrivateKeySigner>,
50}
51
52impl DevSigner {
53    pub fn new(accounts: Vec<PrivateKeySigner>) -> Self {
54        let addresses = accounts.iter().map(|wallet| wallet.address()).collect::<Vec<_>>();
55        let accounts = addresses.iter().cloned().zip(accounts).collect();
56        Self { addresses, accounts }
57    }
58}
59
60#[async_trait::async_trait]
61impl Signer for DevSigner {
62    fn accounts(&self) -> Vec<Address> {
63        self.addresses.clone()
64    }
65
66    fn is_signer_for(&self, addr: Address) -> bool {
67        self.accounts.contains_key(&addr)
68    }
69
70    async fn sign(&self, address: Address, message: &[u8]) -> Result<Signature, BlockchainError> {
71        let signer = self.accounts.get(&address).ok_or(BlockchainError::NoSignerAvailable)?;
72
73        Ok(signer.sign_message(message).await?)
74    }
75
76    async fn sign_typed_data(
77        &self,
78        address: Address,
79        payload: &TypedData,
80    ) -> Result<Signature, BlockchainError> {
81        let mut signer =
82            self.accounts.get(&address).ok_or(BlockchainError::NoSignerAvailable)?.to_owned();
83
84        // Explicitly set chainID as none, to avoid any EIP-155 application to `v` when signing
85        // typed data.
86        signer.set_chain_id(None);
87
88        Ok(signer.sign_dynamic_typed_data(payload).await?)
89    }
90
91    async fn sign_hash(&self, address: Address, hash: B256) -> Result<Signature, BlockchainError> {
92        let signer = self.accounts.get(&address).ok_or(BlockchainError::NoSignerAvailable)?;
93
94        Ok(signer.sign_hash(&hash).await?)
95    }
96
97    fn sign_transaction(
98        &self,
99        request: TypedTransactionRequest,
100        address: &Address,
101    ) -> Result<Signature, BlockchainError> {
102        let signer = self.accounts.get(address).ok_or(BlockchainError::NoSignerAvailable)?;
103        match request {
104            TypedTransactionRequest::Legacy(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?),
105            TypedTransactionRequest::EIP2930(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?),
106            TypedTransactionRequest::EIP1559(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?),
107            TypedTransactionRequest::EIP4844(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?),
108            TypedTransactionRequest::Deposit(_) => {
109                unreachable!("op deposit txs should not be signed")
110            }
111        }
112    }
113}
114
115/// converts the `request` into a [`TypedTransactionRequest`] with the given signature
116///
117/// # Errors
118///
119/// This will fail if the `signature` contains an erroneous recovery id.
120pub fn build_typed_transaction(
121    request: TypedTransactionRequest,
122    signature: Signature,
123) -> Result<TypedTransaction, BlockchainError> {
124    let tx = match request {
125        TypedTransactionRequest::Legacy(tx) => TypedTransaction::Legacy(tx.into_signed(signature)),
126        TypedTransactionRequest::EIP2930(tx) => {
127            TypedTransaction::EIP2930(tx.into_signed(signature))
128        }
129        TypedTransactionRequest::EIP1559(tx) => {
130            TypedTransaction::EIP1559(tx.into_signed(signature))
131        }
132        TypedTransactionRequest::EIP4844(tx) => {
133            TypedTransaction::EIP4844(tx.into_signed(signature))
134        }
135        TypedTransactionRequest::Deposit(tx) => {
136            let TxDeposit {
137                from,
138                gas_limit,
139                to,
140                value,
141                input,
142                source_hash,
143                mint,
144                is_system_transaction,
145                ..
146            } = tx;
147            TypedTransaction::Deposit(DepositTransaction {
148                from,
149                gas_limit,
150                kind: to,
151                value,
152                input,
153                source_hash,
154                mint: mint.map_or(U256::ZERO, U256::from),
155                is_system_tx: is_system_transaction,
156                nonce: 0,
157            })
158        }
159    };
160
161    Ok(tx)
162}