use bytes::Bytes;
use calldata::encode_calldata;
use ethereum_types::{H160, H256, U256};
use ethrex_common::types::EIP7702Transaction;
use ethrex_common::types::FeeTokenTransaction;
use ethrex_common::types::Fork;
use ethrex_common::utils::keccak;
use ethrex_common::{
Address,
types::{
AccessListEntry, BlobsBundle, EIP1559Transaction, GenericTransaction, TxKind, TxType,
WrappedEIP4844Transaction,
},
};
use ethrex_l2_common::{calldata::Value, messages::L1MessageProof};
use ethrex_l2_rpc::{
clients::get_l1_message_proof,
signer::{LocalSigner, Signable, Signer},
};
use ethrex_rlp::encode::RLPEncode;
use ethrex_rpc::clients::eth::{EthClient, Overrides, errors::EthClientError};
use ethrex_rpc::types::block_identifier::{BlockIdentifier, BlockTag};
use ethrex_rpc::types::receipt::RpcReceipt;
use secp256k1::SecretKey;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::ops::{Add, Div};
use std::str::FromStr;
use std::{fs::read_to_string, path::Path};
use tracing::{error, warn};
pub mod calldata;
pub mod l1_to_l2_tx_data;
pub mod privileged_data;
pub use l1_to_l2_tx_data::{L1ToL2TransactionData, send_l1_to_l2_tx};
#[doc(inline)]
pub use ethrex_sdk_contract_utils::*;
use calldata::from_hex_string_to_h256_array;
use crate::privileged_data::PrivilegedTransactionData;
pub const DEFAULT_BRIDGE_ADDRESS: Address = H160([
0x79, 0x7b, 0x34, 0x7d, 0x22, 0x09, 0xf7, 0xdc, 0xb8, 0xf9, 0xbb, 0x68, 0xfe, 0x42, 0x96, 0xa0,
0x5d, 0x5a, 0x2c, 0x2b,
]);
pub const COMMON_BRIDGE_L2_ADDRESS: Address = H160([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff,
]);
pub const L2_TO_L1_MESSENGER_ADDRESS: Address = H160([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xfe,
]);
pub const FEE_TOKEN_REGISTRY_ADDRESS: Address = H160([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xfc,
]);
pub const FEE_TOKEN_PRICER_ADDRESS: Address = H160([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xfb,
]);
pub const ADDRESS_ALIASING: Address = H160([
0xee, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x11, 0xff,
]);
pub const L2_WITHDRAW_SIGNATURE: &str = "withdraw(address)";
pub const SP1_VERIFIER_ID: u8 = 1;
pub const RISC0_VERIFIER_ID: u8 = 2;
pub const REGISTER_FEE_TOKEN_SIGNATURE: &str = "registerNewFeeToken(address)";
pub const SET_FEE_TOKEN_RATIO_SIGNATURE: &str = "setFeeTokenRatio(address,uint256)";
const ERC1967_PROXY_BYTECODE: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/contracts/solc_out/ERC1967Proxy.bytecode"
));
#[derive(Debug, thiserror::Error)]
pub enum SdkError {
#[error("Failed to parse address from hex")]
FailedToParseAddressFromHex,
}
pub fn bridge_address() -> Result<Address, SdkError> {
std::env::var("ETHREX_WATCHER_BRIDGE_ADDRESS")
.unwrap_or(format!("{DEFAULT_BRIDGE_ADDRESS:#x}"))
.parse()
.map_err(|_| SdkError::FailedToParseAddressFromHex)
}
pub async fn wait_for_transaction_receipt(
tx_hash: H256,
client: &EthClient,
max_retries: u64,
) -> Result<RpcReceipt, EthClientError> {
let mut r#try = 1;
loop {
match client.get_transaction_receipt(tx_hash).await {
Ok(Some(receipt)) => return Ok(receipt),
Ok(None) => {
}
Err(e) => {
let error_msg = e.to_string();
if !error_msg.contains("transaction indexing is in progress") {
return Err(e);
}
}
}
println!("[{try}/{max_retries}] Retrying to get transaction receipt for {tx_hash:#x}");
if max_retries == r#try {
return Err(EthClientError::Custom(format!(
"Transaction receipt for {tx_hash:#x} not found after {max_retries} retries"
)));
}
r#try += 1;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
pub async fn transfer(
amount: U256,
from: Address,
to: Address,
private_key: &SecretKey,
client: &EthClient,
) -> Result<H256, EthClientError> {
println!("Transferring {amount} from {from:#x} to {to:#x}");
let gas_price = client
.get_gas_price_with_extra(20)
.await?
.try_into()
.map_err(|_| {
EthClientError::InternalError("Failed to convert gas_price to a u64".to_owned())
})?;
let tx = build_generic_tx(
client,
TxType::EIP1559,
to,
from,
Default::default(),
Overrides {
value: Some(amount),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?;
let signer = LocalSigner::new(*private_key).into();
send_generic_transaction(client, tx, &signer).await
}
pub async fn deposit_through_transfer(
amount: U256,
from: Address,
from_pk: &SecretKey,
eth_client: &EthClient,
) -> Result<H256, EthClientError> {
println!("Depositing {amount} from {from:#x} to bridge");
transfer(
amount,
from,
bridge_address().map_err(|err| EthClientError::Custom(err.to_string()))?,
from_pk,
eth_client,
)
.await
}
pub async fn withdraw(
amount: U256,
from: Address,
from_pk: SecretKey,
proposer_client: &EthClient,
nonce: Option<u64>,
gas_limit: Option<u64>,
) -> Result<H256, EthClientError> {
let withdraw_transaction = build_generic_tx(
proposer_client,
TxType::EIP1559,
COMMON_BRIDGE_L2_ADDRESS,
from,
Bytes::from(encode_calldata(
L2_WITHDRAW_SIGNATURE,
&[Value::Address(from)],
)?),
Overrides {
value: Some(amount),
nonce,
gas_limit,
..Default::default()
},
)
.await?;
let signer = LocalSigner::new(from_pk).into();
send_generic_transaction(proposer_client, withdraw_transaction, &signer).await
}
pub async fn claim_withdraw(
amount: U256,
from: Address,
from_pk: SecretKey,
eth_client: &EthClient,
message_proof: &L1MessageProof,
) -> Result<H256, EthClientError> {
println!("Claiming {amount} from bridge to {from:#x}");
const CLAIM_WITHDRAWAL_SIGNATURE: &str = "claimWithdrawal(uint256,uint256,uint256,bytes32[])";
let calldata_values = vec![
Value::Uint(amount),
Value::Uint(message_proof.batch_number.into()),
Value::Uint(message_proof.message_id),
Value::Array(
message_proof
.merkle_proof
.iter()
.map(|hash| Value::FixedBytes(hash.as_fixed_bytes().to_vec().into()))
.collect(),
),
];
let claim_withdrawal_data = encode_calldata(CLAIM_WITHDRAWAL_SIGNATURE, &calldata_values)?;
println!(
"Claiming withdrawal with calldata: {}",
hex::encode(&claim_withdrawal_data)
);
let claim_tx = build_generic_tx(
eth_client,
TxType::EIP1559,
bridge_address().map_err(|err| EthClientError::Custom(err.to_string()))?,
from,
claim_withdrawal_data.into(),
Overrides {
from: Some(from),
..Default::default()
},
)
.await?;
let signer = LocalSigner::new(from_pk).into();
send_generic_transaction(eth_client, claim_tx, &signer).await
}
pub async fn claim_erc20withdraw(
token_l1: Address,
token_l2: Address,
amount: U256,
from_signer: &Signer,
eth_client: &EthClient,
message_proof: &L1MessageProof,
) -> Result<H256, EthClientError> {
let from = from_signer.address();
const CLAIM_WITHDRAWAL_ERC20_SIGNATURE: &str =
"claimWithdrawalERC20(address,address,uint256,uint256,uint256,bytes32[])";
let calldata_values = vec![
Value::Address(token_l1),
Value::Address(token_l2),
Value::Uint(amount),
Value::Uint(U256::from(message_proof.batch_number)),
Value::Uint(message_proof.message_id),
Value::Array(
message_proof
.merkle_proof
.iter()
.map(|v| Value::Uint(U256::from_big_endian(v.as_bytes())))
.collect(),
),
];
let claim_withdrawal_data =
encode_calldata(CLAIM_WITHDRAWAL_ERC20_SIGNATURE, &calldata_values)?;
println!(
"Claiming withdrawal with calldata: {}",
hex::encode(&claim_withdrawal_data)
);
let claim_tx = build_generic_tx(
eth_client,
TxType::EIP1559,
bridge_address().map_err(|err| EthClientError::Custom(err.to_string()))?,
from,
claim_withdrawal_data.into(),
Overrides {
from: Some(from),
..Default::default()
},
)
.await?;
send_generic_transaction(eth_client, claim_tx, from_signer).await
}
pub async fn deposit_erc20(
token_l1: Address,
token_l2: Address,
amount: U256,
from: Address,
from_signer: &Signer,
eth_client: &EthClient,
) -> Result<H256, EthClientError> {
println!("Claiming {amount} from bridge to {from:#x}");
const DEPOSIT_ERC20_SIGNATURE: &str = "depositERC20(address,address,address,uint256)";
let calldata_values = vec![
Value::Address(token_l1),
Value::Address(token_l2),
Value::Address(from),
Value::Uint(amount),
];
let deposit_data = encode_calldata(DEPOSIT_ERC20_SIGNATURE, &calldata_values)?;
let mut deposit_tx = build_generic_tx(
eth_client,
TxType::EIP1559,
bridge_address().map_err(|err| EthClientError::Custom(err.to_string()))?,
from,
deposit_data.into(),
Overrides {
from: Some(from),
..Default::default()
},
)
.await?;
deposit_tx.gas = deposit_tx.gas.map(|gas| gas * 2);
send_generic_transaction(eth_client, deposit_tx, from_signer).await
}
pub fn secret_key_deserializer<'de, D>(deserializer: D) -> Result<SecretKey, D::Error>
where
D: Deserializer<'de>,
{
let hex = H256::deserialize(deserializer)?;
SecretKey::from_slice(hex.as_bytes()).map_err(serde::de::Error::custom)
}
pub fn secret_key_serializer<S>(secret_key: &SecretKey, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex = H256::from_slice(&secret_key.secret_bytes());
hex.serialize(serializer)
}
pub const DETERMINISTIC_DEPLOYMENT_PROXY_ADDRESS: Address = H160([
0x4e, 0x59, 0xb4, 0x48, 0x47, 0xb3, 0x79, 0x57, 0x85, 0x88, 0x92, 0x0c, 0xa7, 0x8f, 0xbf, 0x26,
0xc0, 0xb4, 0x95, 0x6c,
]);
pub const SAFE_SINGLETON_FACTORY_ADDRESS: Address = H160([
0x91, 0x4d, 0x7F, 0xec, 0x6a, 0xac, 0x8c, 0xd5, 0x42, 0xe7, 0x2b, 0xca, 0x78, 0xb3, 0x06, 0x50,
0xd4, 0x56, 0x43, 0xd7,
]);
pub const CREATE2DEPLOYER_ADDRESS: Address = H160([
0x13, 0xb0, 0xd8, 0x5c, 0xcb, 0x8b, 0xf8, 0x60, 0xb6, 0xb7, 0x9a, 0xf3, 0x02, 0x9f, 0xca, 0x08,
0x1a, 0xe9, 0xbe, 0xf2,
]);
#[derive(Default, Clone)]
pub struct ProxyDeployment {
pub proxy_address: Address,
pub proxy_tx_hash: H256,
pub implementation_address: Address,
pub implementation_tx_hash: H256,
}
#[derive(Debug, thiserror::Error)]
pub enum DeployError {
#[error("Failed to decode init code: {0}")]
FailedToReadInitCode(#[from] std::io::Error),
#[error("Failed to decode init code: {0}")]
FailedToDecodeBytecode(#[from] hex::FromHexError),
#[error("Failed to deploy contract: {0}")]
FailedToDeploy(#[from] EthClientError),
#[error("Proxy bytecode not found. Make sure to compile the sdk with `COMPILE_CONTRACTS` set.")]
ProxyBytecodeNotFound,
}
pub async fn create_deploy(
client: &EthClient,
deployer: &Signer,
init_code: Bytes,
overrides: Overrides,
) -> Result<(H256, Address), EthClientError> {
let mut deploy_overrides = overrides;
deploy_overrides.to = Some(TxKind::Create);
let deploy_tx = build_generic_tx(
client,
TxType::EIP1559,
Address::zero(),
deployer.address(),
init_code,
deploy_overrides,
)
.await?;
let deploy_tx_hash = send_generic_transaction(client, deploy_tx, deployer).await?;
let receipt = wait_for_transaction_receipt(deploy_tx_hash, client, 1000).await?;
let deployed_address = receipt.tx_info.contract_address.ok_or_else(|| {
EthClientError::Custom("Deploy transaction did not create a contract".to_owned())
})?;
Ok((deploy_tx_hash, deployed_address))
}
pub async fn create2_deploy_from_path(
constructor_args: &[u8],
contract_path: &Path,
deployer: &Signer,
salt: &[u8],
eth_client: &EthClient,
) -> Result<(H256, Address), DeployError> {
let bytecode_hex = read_to_string(contract_path)?;
let bytecode = hex::decode(bytecode_hex.trim_start_matches("0x").trim())?;
create2_deploy_from_bytecode(constructor_args, &bytecode, deployer, salt, eth_client).await
}
pub async fn create2_deploy_from_bytecode(
constructor_args: &[u8],
bytecode: &[u8],
deployer: &Signer,
salt: &[u8],
eth_client: &EthClient,
) -> Result<(H256, Address), DeployError> {
let (deploy_tx_hash, contract_address) = create2_deploy_from_bytecode_no_wait(
constructor_args,
bytecode,
deployer,
salt,
eth_client,
Overrides::default(),
)
.await?;
wait_for_transaction_receipt(deploy_tx_hash, eth_client, 10).await?;
Ok((deploy_tx_hash, contract_address))
}
pub async fn create2_deploy_from_bytecode_no_wait(
constructor_args: &[u8],
bytecode: &[u8],
deployer: &Signer,
salt: &[u8],
eth_client: &EthClient,
overrides: Overrides,
) -> Result<(H256, Address), DeployError> {
let init_code = [bytecode, constructor_args].concat();
let (deploy_tx_hash, contract_address) =
create2_deploy_no_wait(salt, &init_code, deployer, eth_client, overrides).await?;
Ok((deploy_tx_hash, contract_address))
}
fn build_proxy_init_code(implementation_address: Address) -> Result<Bytes, DeployError> {
#[allow(clippy::const_is_empty)]
if ERC1967_PROXY_BYTECODE.is_empty() {
return Err(DeployError::ProxyBytecodeNotFound);
}
let mut init_code = ERC1967_PROXY_BYTECODE.to_vec();
init_code.extend(H256::from(implementation_address).0);
init_code.extend(H256::from_low_u64_be(0x40).0);
init_code.extend(H256::zero().0);
Ok(Bytes::from(init_code))
}
async fn deploy_proxy(
deployer: &Signer,
eth_client: &EthClient,
implementation_address: Address,
salt: &[u8],
) -> Result<(H256, Address), DeployError> {
let (tx_hash, address) = deploy_proxy_no_wait(
deployer,
eth_client,
implementation_address,
salt,
Overrides::default(),
)
.await?;
wait_for_transaction_receipt(tx_hash, eth_client, 10).await?;
Ok((tx_hash, address))
}
async fn deploy_proxy_no_wait(
deployer: &Signer,
eth_client: &EthClient,
implementation_address: Address,
salt: &[u8],
overrides: Overrides,
) -> Result<(H256, Address), DeployError> {
let init_code = build_proxy_init_code(implementation_address)?;
create2_deploy_no_wait(salt, &init_code, deployer, eth_client, overrides)
.await
.map_err(DeployError::from)
}
pub async fn deploy_with_proxy(
deployer: &Signer,
eth_client: &EthClient,
contract_path: &Path,
salt: &[u8],
) -> Result<ProxyDeployment, DeployError> {
let (implementation_tx_hash, implementation_address) =
create2_deploy_from_path(&[], contract_path, deployer, salt, eth_client).await?;
let (proxy_tx_hash, proxy_address) =
deploy_proxy(deployer, eth_client, implementation_address, salt).await?;
Ok(ProxyDeployment {
proxy_address,
proxy_tx_hash,
implementation_address,
implementation_tx_hash,
})
}
pub async fn deploy_with_proxy_no_wait(
deployer: &Signer,
eth_client: &EthClient,
contract_path: &Path,
salt: &[u8],
overrides: Overrides,
) -> Result<ProxyDeployment, DeployError> {
let bytecode_hex = read_to_string(contract_path)?;
let bytecode = hex::decode(bytecode_hex.trim_start_matches("0x").trim())?;
let (implementation_tx_hash, implementation_address) = create2_deploy_from_bytecode_no_wait(
&[],
&bytecode,
deployer,
salt,
eth_client,
overrides.clone(),
)
.await?;
let (proxy_tx_hash, proxy_address) = deploy_proxy_no_wait(
deployer,
eth_client,
implementation_address,
salt,
Overrides {
nonce: overrides.nonce.map(|nonce| nonce + 1),
..overrides
},
)
.await?;
Ok(ProxyDeployment {
proxy_address,
proxy_tx_hash,
implementation_address,
implementation_tx_hash,
})
}
pub async fn deploy_with_proxy_from_bytecode(
deployer: &Signer,
eth_client: &EthClient,
bytecode: &[u8],
salt: &[u8],
) -> Result<ProxyDeployment, DeployError> {
let (implementation_tx_hash, implementation_address) =
create2_deploy_from_bytecode(&[], bytecode, deployer, salt, eth_client).await?;
let (proxy_tx_hash, proxy_address) =
deploy_proxy(deployer, eth_client, implementation_address, salt).await?;
Ok(ProxyDeployment {
proxy_address,
proxy_tx_hash,
implementation_address,
implementation_tx_hash,
})
}
pub async fn deploy_with_proxy_from_bytecode_no_wait(
deployer: &Signer,
eth_client: &EthClient,
bytecode: &[u8],
salt: &[u8],
overrides: Overrides,
) -> Result<ProxyDeployment, DeployError> {
let (implementation_tx_hash, implementation_address) = create2_deploy_from_bytecode_no_wait(
&[],
bytecode,
deployer,
salt,
eth_client,
overrides.clone(),
)
.await?;
let (proxy_tx_hash, proxy_address) = deploy_proxy_no_wait(
deployer,
eth_client,
implementation_address,
salt,
Overrides {
nonce: overrides.nonce.map(|nonce| nonce + 1),
..overrides
},
)
.await?;
Ok(ProxyDeployment {
proxy_address,
proxy_tx_hash,
implementation_address,
implementation_tx_hash,
})
}
async fn build_create2_deploy_tx(
salt: &[u8],
init_code: &[u8],
deployer: &Signer,
eth_client: &EthClient,
overrides: Overrides,
) -> Result<GenericTransaction, EthClientError> {
let calldata = [salt, init_code].concat();
let gas_price = eth_client
.get_gas_price_with_extra(20)
.await?
.try_into()
.map_err(|_| {
EthClientError::InternalError("Failed to convert gas_price to a u64".to_owned())
})?;
build_generic_tx(
eth_client,
TxType::EIP1559,
DETERMINISTIC_DEPLOYMENT_PROXY_ADDRESS,
deployer.address(),
calldata.into(),
Overrides {
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..overrides
},
)
.await
}
async fn create2_deploy_no_wait(
salt: &[u8],
init_code: &[u8],
deployer: &Signer,
eth_client: &EthClient,
overrides: Overrides,
) -> Result<(H256, Address), EthClientError> {
let deploy_tx =
build_create2_deploy_tx(salt, init_code, deployer, eth_client, overrides).await?;
let deploy_tx_hash = send_generic_transaction(eth_client, deploy_tx, deployer).await?;
let deployed_address = create2_address(salt, keccak(init_code));
Ok((deploy_tx_hash, deployed_address))
}
#[allow(clippy::indexing_slicing)]
fn create2_address(salt: &[u8], init_code_hash: H256) -> Address {
Address::from_slice(
&keccak(
[
&[0xff],
DETERMINISTIC_DEPLOYMENT_PROXY_ADDRESS.as_bytes(),
salt,
init_code_hash.as_bytes(),
]
.concat(),
)
.as_bytes()[12..],
)
}
pub async fn initialize_contract_no_wait(
contract_address: Address,
initialize_calldata: Vec<u8>,
initializer: &Signer,
eth_client: &EthClient,
overrides: Overrides,
) -> Result<H256, EthClientError> {
let initialize_tx = build_generic_tx(
eth_client,
TxType::EIP1559,
contract_address,
initializer.address(),
initialize_calldata.into(),
overrides,
)
.await?;
let initialize_tx_hash =
send_generic_transaction(eth_client, initialize_tx, initializer).await?;
Ok(initialize_tx_hash)
}
pub async fn call_contract(
client: &EthClient,
signer: &Signer,
to: Address,
signature: &str,
parameters: Vec<Value>,
) -> Result<H256, EthClientError> {
let calldata = encode_calldata(signature, ¶meters)?.into();
let from = signer.address();
let tx = build_generic_tx(
client,
TxType::EIP1559,
to,
from,
calldata,
Default::default(),
)
.await?;
let tx_hash = send_generic_transaction(client, tx, signer).await?;
wait_for_transaction_receipt(tx_hash, client, 100).await?;
Ok(tx_hash)
}
pub fn address_to_word(address: Address) -> U256 {
let mut word = [0u8; 32];
for (word_byte, address_byte) in word.iter_mut().skip(12).zip(address.as_bytes().iter()) {
*word_byte = *address_byte;
}
U256::from_big_endian(&word)
}
pub fn get_erc1967_slot(name: &str) -> U256 {
U256::from_big_endian(&keccak(name).0) - U256::one()
}
pub fn get_address_alias(address: Address) -> Address {
let address = U256::from_big_endian(&address.to_fixed_bytes());
let alias = address.add(U256::from_big_endian(&ADDRESS_ALIASING.to_fixed_bytes()));
H160::from_slice(&alias.to_big_endian()[12..32])
}
const WAIT_TIME_FOR_RECEIPT_SECONDS: u64 = 2;
pub async fn send_generic_transaction(
client: &EthClient,
generic_tx: GenericTransaction,
signer: &Signer,
) -> Result<H256, EthClientError> {
let mut encoded_tx = vec![generic_tx.r#type.into()];
match generic_tx.r#type {
TxType::EIP1559 => {
let tx: EIP1559Transaction = generic_tx.try_into()?;
let signed_tx = tx
.sign(signer)
.await
.map_err(|err| EthClientError::Custom(err.to_string()))?;
signed_tx.encode(&mut encoded_tx);
}
TxType::EIP4844 => {
let mut tx: WrappedEIP4844Transaction = generic_tx.try_into()?;
tx.tx
.sign_inplace(signer)
.await
.map_err(|err| EthClientError::Custom(err.to_string()))?;
tx.encode(&mut encoded_tx);
}
TxType::EIP7702 => {
let tx: EIP7702Transaction = generic_tx.try_into()?;
let signed_tx = tx
.sign(signer)
.await
.map_err(|err| EthClientError::Custom(err.to_string()))?;
signed_tx.encode(&mut encoded_tx);
}
TxType::FeeToken => {
let tx: FeeTokenTransaction = generic_tx.try_into()?;
let signed_tx = tx
.sign(signer)
.await
.map_err(|err| EthClientError::Custom(err.to_string()))?;
signed_tx.encode(&mut encoded_tx);
}
_ => {
return Err(EthClientError::Custom(format!(
"Unsupported transaction type: {:?}",
generic_tx.r#type
)));
}
};
client.send_raw_transaction(encoded_tx.as_slice()).await
}
pub async fn send_tx_bump_gas_exponential_backoff(
client: &EthClient,
mut tx: GenericTransaction,
signer: &Signer,
) -> Result<H256, EthClientError> {
let mut number_of_retries = 0;
'outer: while number_of_retries < client.max_number_of_retries {
if let Some(max_fee_per_gas) = client.maximum_allowed_max_fee_per_gas {
let (Some(tx_max_fee), Some(tx_max_priority_fee)) =
(&mut tx.max_fee_per_gas, &mut tx.max_priority_fee_per_gas)
else {
return Err(EthClientError::Custom(
"Invalid transaction: max_fee_per_gas or max_priority_fee_per_gas is missing"
.to_string(),
));
};
if *tx_max_fee > max_fee_per_gas {
*tx_max_fee = max_fee_per_gas;
if *tx_max_priority_fee > *tx_max_fee {
*tx_max_priority_fee = *tx_max_fee;
}
warn!(
"max_fee_per_gas exceeds the allowed limit, adjusting it to {max_fee_per_gas}"
);
}
}
if let Some(tx_max_fee_per_blob_gas) = &mut tx.max_fee_per_blob_gas
&& let Some(max_fee_per_blob_gas) = client.maximum_allowed_max_fee_per_blob_gas
&& *tx_max_fee_per_blob_gas > U256::from(max_fee_per_blob_gas)
{
*tx_max_fee_per_blob_gas = U256::from(max_fee_per_blob_gas);
warn!(
"max_fee_per_blob_gas exceeds the allowed limit, adjusting it to {max_fee_per_blob_gas}"
);
}
let Ok(tx_hash) = send_generic_transaction(client, tx.clone(), signer)
.await
.inspect_err(|e| {
error!(
"Error sending generic transaction {e} attempts [{number_of_retries}/{}]",
client.max_number_of_retries
);
})
else {
bump_gas_generic_tx(&mut tx, 30);
number_of_retries += 1;
continue;
};
if number_of_retries > 0 {
warn!(
"Resending Transaction after bumping gas, attempts [{number_of_retries}/{}]\nTxHash: {tx_hash:#x}",
client.max_number_of_retries
);
}
let mut receipt = client.get_transaction_receipt(tx_hash).await?;
let mut attempt = 1;
#[allow(clippy::as_conversions)]
let attempts_to_wait_in_seconds = client
.backoff_factor
.pow(number_of_retries as u32)
.clamp(client.min_retry_delay, client.max_retry_delay);
while receipt.is_none() {
if attempt >= (attempts_to_wait_in_seconds / WAIT_TIME_FOR_RECEIPT_SECONDS) {
bump_gas_generic_tx(&mut tx, 30);
number_of_retries += 1;
continue 'outer;
}
attempt += 1;
tokio::time::sleep(std::time::Duration::from_secs(
WAIT_TIME_FOR_RECEIPT_SECONDS,
))
.await;
receipt = client.get_transaction_receipt(tx_hash).await?;
}
return Ok(tx_hash);
}
Err(EthClientError::TimeoutError)
}
fn bump_gas_generic_tx(tx: &mut GenericTransaction, bump_percentage: u64) {
if let (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) =
(&mut tx.max_fee_per_gas, &mut tx.max_priority_fee_per_gas)
{
*max_fee_per_gas = (*max_fee_per_gas * (100 + bump_percentage)) / 100;
*max_priority_fee_per_gas = (*max_priority_fee_per_gas * (100 + bump_percentage)) / 100;
}
if let Some(max_fee_per_blob_gas) = &mut tx.max_fee_per_blob_gas {
let factor = 1 + (bump_percentage / 100) * 10;
*max_fee_per_blob_gas = max_fee_per_blob_gas
.saturating_mul(U256::from(factor))
.div(10);
}
}
pub async fn build_generic_tx(
client: &EthClient,
r#type: TxType,
to: Address,
from: Address,
calldata: Bytes,
overrides: Overrides,
) -> Result<GenericTransaction, EthClientError> {
match r#type {
TxType::EIP1559
| TxType::EIP4844
| TxType::EIP7702
| TxType::Privileged
| TxType::FeeToken => {}
TxType::EIP2930 | TxType::Legacy => {
return Err(EthClientError::Custom(
"Unsupported tx type in build_generic_tx".to_owned(),
));
}
}
if overrides.fee_token.is_none() && r#type == TxType::FeeToken {
return Err(EthClientError::Custom(
"fee_token must be set for FeeToken tx type".to_owned(),
));
};
let mut tx = GenericTransaction {
r#type,
to: overrides.to.clone().unwrap_or(TxKind::Call(to)),
chain_id: Some(if let Some(chain_id) = overrides.chain_id {
chain_id
} else {
client.get_chain_id().await?.try_into().map_err(|_| {
EthClientError::Custom("Failed at get_chain_id().try_into()".to_owned())
})?
}),
nonce: Some(get_nonce_from_overrides_or_rpc(client, &overrides, from).await?),
max_fee_per_gas: Some(
get_fee_from_override_or_get_gas_price(client, overrides.max_fee_per_gas).await?,
),
max_priority_fee_per_gas: Some(
priority_fee_from_override_or_rpc(client, overrides.max_priority_fee_per_gas).await?,
),
max_fee_per_blob_gas: overrides.gas_price_per_blob,
value: overrides.value.unwrap_or_default(),
input: calldata,
access_list: overrides
.access_list
.iter()
.map(AccessListEntry::from)
.collect(),
fee_token: overrides.fee_token,
from,
wrapper_version: overrides.wrapper_version,
authorization_list: overrides.authorization_list,
..Default::default()
};
tx.gas_price = U256::from(tx.max_fee_per_gas.unwrap_or_default());
if let Some(blobs_bundle) = &overrides.blobs_bundle {
tx.blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
add_blobs_to_generic_tx(&mut tx, blobs_bundle);
}
tx.gas = Some(match overrides.gas_limit {
Some(gas) => gas,
None => client.estimate_gas(tx.clone()).await?,
});
Ok(tx)
}
pub fn add_blobs_to_generic_tx(tx: &mut GenericTransaction, bundle: &BlobsBundle) {
tx.blobs = bundle
.blobs
.iter()
.map(|blob| Bytes::copy_from_slice(blob))
.collect()
}
async fn get_nonce_from_overrides_or_rpc(
client: &EthClient,
overrides: &Overrides,
address: Address,
) -> Result<u64, EthClientError> {
if let Some(nonce) = overrides.nonce {
return Ok(nonce);
}
client
.get_nonce(address, BlockIdentifier::Tag(BlockTag::Latest))
.await
}
async fn get_fee_from_override_or_get_gas_price(
client: &EthClient,
maybe_gas_fee: Option<u64>,
) -> Result<u64, EthClientError> {
if let Some(gas_fee) = maybe_gas_fee {
return Ok(gas_fee);
}
client
.get_gas_price()
.await?
.try_into()
.map_err(|_| EthClientError::Custom("Failed to get gas for fee".to_owned()))
}
async fn priority_fee_from_override_or_rpc(
client: &EthClient,
maybe_priority_fee: Option<u64>,
) -> Result<u64, EthClientError> {
if let Some(priority_fee) = maybe_priority_fee {
return Ok(priority_fee);
}
if let Ok(priority_fee) = client.get_max_priority_fee().await
&& let Ok(priority_fee_u64) = priority_fee.try_into()
{
return Ok(priority_fee_u64);
}
get_fee_from_override_or_get_gas_price(client, None).await
}
pub async fn wait_for_l1_message_proof(
client: &EthClient,
transaction_hash: H256,
max_retries: u64,
) -> Result<Vec<L1MessageProof>, EthClientError> {
let mut message_proof = get_l1_message_proof(client, transaction_hash).await?;
let mut r#try = 1;
while message_proof.is_none() {
println!(
"[{try}/{max_retries}] Retrying to get message proof for tx {transaction_hash:#x}"
);
if max_retries == r#try {
return Err(EthClientError::Custom(format!(
"L1Message proof for tx {transaction_hash:#x} not found after {max_retries} retries"
)));
}
r#try += 1;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
message_proof = get_l1_message_proof(client, transaction_hash).await?;
}
message_proof.ok_or(EthClientError::Custom("L1Message proof is None".to_owned()))
}
pub async fn get_last_committed_batch(
client: &EthClient,
on_chain_proposer_address: Address,
) -> Result<u64, EthClientError> {
_call_u64_variable(client, b"lastCommittedBatch()", on_chain_proposer_address).await
}
pub async fn get_last_verified_batch(
client: &EthClient,
on_chain_proposer_address: Address,
) -> Result<u64, EthClientError> {
_call_u64_variable(client, b"lastVerifiedBatch()", on_chain_proposer_address).await
}
pub async fn get_sp1_vk_for_batch(
client: &EthClient,
on_chain_proposer_address: Address,
batch_number: u64,
) -> Result<[u8; 32], EthClientError> {
get_verification_key_for_batch(
client,
on_chain_proposer_address,
batch_number,
SP1_VERIFIER_ID,
)
.await
}
pub async fn get_risc0_vk_for_batch(
client: &EthClient,
on_chain_proposer_address: Address,
batch_number: u64,
) -> Result<[u8; 32], EthClientError> {
get_verification_key_for_batch(
client,
on_chain_proposer_address,
batch_number,
RISC0_VERIFIER_ID,
)
.await
}
async fn get_verification_key_for_batch(
client: &EthClient,
on_chain_proposer_address: Address,
batch_number: u64,
verifier_id: u8,
) -> Result<[u8; 32], EthClientError> {
let commit_hash =
get_batch_commit_hash(client, on_chain_proposer_address, batch_number).await?;
get_verification_key(client, on_chain_proposer_address, commit_hash, verifier_id).await
}
pub async fn get_batch_commit_hash(
client: &EthClient,
on_chain_proposer_address: Address,
batch_number: u64,
) -> Result<[u8; 32], EthClientError> {
let values = vec![Value::Uint(U256::from(batch_number))];
let calldata = encode_calldata("batchCommitments(uint256)", &values)?;
let response = client
.call(
on_chain_proposer_address,
calldata.into(),
Default::default(),
)
.await?;
let hex = response.strip_prefix("0x").ok_or(EthClientError::Custom(
"Couldn't strip '0x' prefix from response".to_owned(),
))?;
let commit_hash_hex = hex.get(384..448).ok_or(EthClientError::Custom(
"Response too short to contain commitHash".to_owned(),
))?;
let bytes = hex::decode(commit_hash_hex)
.map_err(|e| EthClientError::Custom(format!("Failed to decode commit hash hex: {e}")))?;
let arr: [u8; 32] = bytes.try_into().map_err(|_| {
EthClientError::Custom("Failed to convert commit hash bytes to [u8; 32]".to_owned())
})?;
Ok(arr)
}
pub async fn get_verification_key(
client: &EthClient,
on_chain_proposer_address: Address,
commit_hash: [u8; 32],
verifier_id: u8,
) -> Result<[u8; 32], EthClientError> {
let values = vec![
Value::FixedBytes(commit_hash.to_vec().into()),
Value::Uint(U256::from(verifier_id)),
];
let calldata = encode_calldata("verificationKeys(bytes32,uint8)", &values)?;
let response = client
.call(
on_chain_proposer_address,
calldata.into(),
Default::default(),
)
.await?;
let hex = response.strip_prefix("0x").ok_or(EthClientError::Custom(
"Couldn't strip '0x' prefix from response".to_owned(),
))?;
let bytes = hex::decode(hex)
.map_err(|e| EthClientError::Custom(format!("Failed to decode VK hex: {e}")))?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| EthClientError::Custom("Failed to convert VK bytes to [u8; 32]".to_owned()))?;
Ok(arr)
}
pub async fn get_last_fetched_l1_block(
client: &EthClient,
common_bridge_address: Address,
) -> Result<u64, EthClientError> {
_call_u64_variable(client, b"lastFetchedL1Block()", common_bridge_address).await
}
pub async fn get_l2_gas_limit(
client: &EthClient,
common_bridge_address: Address,
) -> Result<u64, EthClientError> {
_call_u64_variable(client, b"l2GasLimit()", common_bridge_address).await
}
pub async fn get_pending_l1_messages(
client: &EthClient,
common_bridge_address: Address,
) -> Result<Vec<H256>, EthClientError> {
let response = _generic_call(
client,
b"getPendingTransactionHashes()",
common_bridge_address,
)
.await?;
from_hex_string_to_h256_array(&response)
}
pub async fn get_pending_l2_messages(
client: &EthClient,
common_bridge_address: Address,
chain_id: u64,
) -> Result<Vec<H256>, EthClientError> {
let selector = keccak(b"getPendingL2MessagesHashes(uint256)")
.as_bytes()
.get(..4)
.ok_or(EthClientError::Custom("Failed to get selector.".to_owned()))?
.to_vec();
let mut calldata = Vec::new();
calldata.extend_from_slice(&selector);
calldata.extend_from_slice(&U256::from(chain_id).to_big_endian());
let response = client
.call(common_bridge_address, calldata.into(), Overrides::default())
.await?;
from_hex_string_to_h256_array(&response)
}
pub async fn get_l1_active_fork(
client: &EthClient,
activation_time: Option<u64>,
) -> Result<Fork, EthClientError> {
let Some(osaka_activation_time) = activation_time else {
return Ok(Fork::Osaka);
};
let current_timestamp = client
.get_block_by_number(BlockIdentifier::Tag(BlockTag::Latest), false)
.await?
.header
.timestamp;
if current_timestamp < osaka_activation_time {
Ok(Fork::Prague)
} else {
Ok(Fork::Osaka)
}
}
async fn _generic_call(
client: &EthClient,
selector: &[u8],
contract_address: Address,
) -> Result<String, EthClientError> {
let selector = keccak(selector)
.as_bytes()
.get(..4)
.ok_or(EthClientError::Custom("Failed to get selector.".to_owned()))?
.to_vec();
let hex_string = client
.call(contract_address, selector.into(), Overrides::default())
.await?;
Ok(hex_string)
}
async fn _call_u64_variable(
client: &EthClient,
selector: &[u8],
contract_address: Address,
) -> Result<u64, EthClientError> {
let hex_string = _generic_call(client, selector, contract_address).await?;
let value = U256::from_str_radix(hex_string.trim_start_matches("0x"), 16)?
.try_into()
.map_err(|_| {
EthClientError::Custom("Failed to convert from_hex_string_to_u256()".to_owned())
})?;
Ok(value)
}
async fn _call_address_variable(
eth_client: &EthClient,
selector: &[u8],
on_chain_proposer_address: Address,
) -> Result<Address, EthClientError> {
let hex_string = _generic_call(eth_client, selector, on_chain_proposer_address).await?;
let hex_str = &hex_string.strip_prefix("0x").ok_or(EthClientError::Custom(
"Couldn't strip prefix from request.".to_owned(),
))?[24..];
let value = Address::from_str(hex_str)
.map_err(|_| EthClientError::Custom("Failed to convert from_str()".to_owned()))?;
Ok(value)
}
async fn _call_bytes32_variable(
client: &EthClient,
selector: &[u8],
contract_address: Address,
) -> Result<[u8; 32], EthClientError> {
let hex_string = _generic_call(client, selector, contract_address).await?;
let hex = hex_string.strip_prefix("0x").ok_or(EthClientError::Custom(
"Couldn't strip '0x' prefix from hex string".to_owned(),
))?;
let bytes = hex::decode(hex)
.map_err(|e| EthClientError::Custom(format!("Failed to decode hex string: {e}")))?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| EthClientError::Custom("Failed to convert bytes to [u8; 32]".to_owned()))?;
Ok(arr)
}
pub async fn get_fee_token_ratio(
fee_token: &Address,
client: &EthClient,
) -> Result<u64, EthClientError> {
let values = vec![Value::Address(*fee_token)];
let calldata = encode_calldata("getFeeTokenRatio(address)", &values)?;
let ratio = client
.call(
FEE_TOKEN_PRICER_ADDRESS,
calldata.into(),
Default::default(),
)
.await?;
let ratio: u64 = ratio
.trim_start_matches("0x")
.parse::<u64>()
.map_err(|e| EthClientError::Custom(format!("Failed to parse ratio to u64: {}", e)))?;
Ok(ratio)
}
pub async fn register_fee_token_no_wait(
client: &EthClient,
bridge_address: Address,
fee_token: Address,
signer: &Signer,
overrides: Overrides,
) -> Result<H256, EthClientError> {
let calldata = encode_calldata(REGISTER_FEE_TOKEN_SIGNATURE, &[Value::Address(fee_token)])?;
let tx_register = build_generic_tx(
client,
TxType::EIP1559,
bridge_address,
signer.address(),
calldata.into(),
overrides,
)
.await?;
send_generic_transaction(client, tx_register, signer).await
}
pub async fn wait_for_l2_deposit_receipt(
l1_rpc_receipt: &RpcReceipt,
l1_client: &EthClient,
l2_client: &EthClient,
) -> Result<RpcReceipt, EthClientError> {
let data = l1_rpc_receipt
.logs
.iter()
.find_map(|log| PrivilegedTransactionData::from_log(log.log.clone()).ok())
.ok_or_else(|| {
EthClientError::Custom(format!(
"RpcReceipt for transaction {:?} contains no valid logs",
l1_rpc_receipt.tx_info.transaction_hash
))
})?;
let l2_deposit_tx_hash = data
.into_tx(
l1_client,
l2_client
.get_chain_id()
.await?
.try_into()
.map_err(|e| EthClientError::Custom(format!("Invalid chain id: {e}")))?,
0,
)
.await?
.get_privileged_hash()
.ok_or_else(|| EthClientError::Custom("Empty transaction hash".to_owned()))?;
wait_for_transaction_receipt(l2_deposit_tx_hash, l2_client, 10000).await
}