1#![allow(dead_code, clippy::new_without_default)]
65
66use crate::{
67 abi::{
68 IMirror, IRouter, IWrappedVara,
69 utils::{self as abi_utils, Permit},
70 },
71 wvara::WVaraQuery,
72};
73use alloy::{
74 consensus::SignableTransaction,
75 eips::BlockId,
76 network::{self, Ethereum as AlloyEthereum, EthereumWallet, Network, TxSigner},
77 primitives::{Address as AlloyAddress, B256, ChainId, Signature, U256 as AlloyU256, address},
78 providers::{
79 Identity, PendingTransactionBuilder, PendingTransactionError, Provider, ProviderBuilder,
80 RootProvider, WalletProvider,
81 fillers::{
82 BlobGasEstimator, BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill,
83 NonceFiller, SimpleNonceManager, WalletFiller,
84 },
85 utils::{self, Eip1559Estimator},
86 },
87 rpc::types::{TransactionReceipt, TransactionRequest, eth::Log},
88 signers::{
89 self as alloy_signer, Error as SignerError, Result as SignerResult, Signer as AlloySigner,
90 SignerSync, sign_transaction_with_chain_id,
91 },
92 sol_types::{SolEvent, SolStruct},
93 transports::RpcError,
94};
95use anyhow::{Context, Result, anyhow};
96use async_trait::async_trait;
97use ethexe_common::{BlockHeader, Digest, SimpleBlockData, ecdsa::PublicKey};
98use gprimitives::{ActorId, H256, MessageId};
99use gsigner::secp256k1::{Address, Secp256k1SignerExt, Signer};
100use middleware::Middleware;
101use mirror::Mirror;
102use router::{Router, RouterQuery};
103use std::time::Duration;
104
105pub mod abi;
106pub mod deploy;
107pub mod middleware;
108pub mod mirror;
109pub mod router;
110pub mod wvara;
111
112pub mod primitives {
113 pub use alloy::primitives::*;
114}
115
116pub type AlloyProvider = FillProvider<
117 JoinFill<
118 JoinFill<
119 JoinFill<
120 JoinFill<JoinFill<Identity, GasFiller>, BlobGasFiller>,
121 NonceFiller<SimpleNonceManager>,
122 >,
123 ChainIdFiller,
124 >,
125 WalletFiller<EthereumWallet>,
126 >,
127 RootProvider,
128>;
129
130#[derive(Debug, Clone, Default)]
131pub struct EthereumBuilder {
132 rpc_url: Option<String>,
133 router_address: Option<Address>,
134 signer: Option<Signer>,
135 sender_address: Option<Address>,
136 eip1559_fee_increase_percentage: Option<u64>,
137 eip1559_max_fee_per_gas_in_gwei: Option<u128>,
138 blob_gas_multiplier: Option<u128>,
139 initialize_addresses: Option<bool>,
140}
141
142impl EthereumBuilder {
143 pub fn rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
147 self.rpc_url = Some(rpc_url.into());
148 self
149 }
150
151 pub fn router_address(mut self, router_address: Address) -> Self {
155 self.router_address = Some(router_address);
156 self
157 }
158
159 pub fn signer(mut self, signer: Signer) -> Self {
161 self.signer = Some(signer);
162 self
163 }
164
165 pub fn sender_address(mut self, sender_address: Address) -> Self {
167 self.sender_address = Some(sender_address);
168 self
169 }
170
171 pub fn eip1559_fee_increase_percentage_opt(
173 mut self,
174 eip1559_fee_increase_percentage: Option<u64>,
175 ) -> Self {
176 self.eip1559_fee_increase_percentage = eip1559_fee_increase_percentage;
177 self
178 }
179
180 pub fn eip1559_fee_increase_percentage(self, eip1559_fee_increase_percentage: u64) -> Self {
184 self.eip1559_fee_increase_percentage_opt(Some(eip1559_fee_increase_percentage))
185 }
186
187 pub fn with_eip1559_increased_fee(self) -> Self {
190 self.eip1559_fee_increase_percentage(Ethereum::INCREASED_EIP1559_FEE_INCREASE_PERCENTAGE)
191 }
192
193 pub fn eip1559_max_fee_per_gas_in_gwei(
196 mut self,
197 eip1559_max_fee_per_gas_in_gwei: u128,
198 ) -> Self {
199 self.eip1559_max_fee_per_gas_in_gwei = Some(eip1559_max_fee_per_gas_in_gwei);
200 self
201 }
202
203 pub fn blob_gas_multiplier_opt(mut self, blob_gas_multiplier: Option<u128>) -> Self {
205 self.blob_gas_multiplier = blob_gas_multiplier;
206 self
207 }
208
209 pub fn blob_gas_multiplier(self, blob_gas_multiplier: u128) -> Self {
213 self.blob_gas_multiplier_opt(Some(blob_gas_multiplier))
214 }
215
216 pub fn with_increased_blob_gas_multiplier(self) -> Self {
218 self.blob_gas_multiplier(Ethereum::INCREASED_BLOB_GAS_MULTIPLIER)
219 }
220
221 pub(crate) fn initialize_addresses(mut self, initialize_addresses: bool) -> Self {
224 self.initialize_addresses = Some(initialize_addresses);
225 self
226 }
227
228 pub(crate) fn without_initializing_addresses(self) -> Self {
231 self.initialize_addresses(false)
232 }
233
234 pub async fn build(self) -> Result<Ethereum> {
236 let rpc_url = self
237 .rpc_url
238 .unwrap_or_else(|| Ethereum::DEFAULT_ETHEREUM_RPC.into());
239 let router_address = self
240 .router_address
241 .unwrap_or(Ethereum::DEFAULT_ROUTER_ADDRESS.into());
242 let signer = self.signer.context("signer is required")?;
243 let sender_address = self.sender_address.context("sender address is required")?;
244 let eip1559_fee_increase_percentage = self
245 .eip1559_fee_increase_percentage
246 .unwrap_or(Ethereum::NO_EIP1559_FEE_INCREASE_PERCENTAGE);
247 let eip1559_max_fee_per_gas_in_gwei = self
248 .eip1559_max_fee_per_gas_in_gwei
249 .unwrap_or(Ethereum::NO_EIP1559_MAX_FEE_PER_GAS_IN_GWEI);
250 let blob_gas_multiplier = self
251 .blob_gas_multiplier
252 .unwrap_or(Ethereum::NO_BLOB_GAS_MULTIPLIER);
253 let initialize_addresses = self.initialize_addresses.unwrap_or(true);
254
255 Ethereum::new(
256 &rpc_url,
257 router_address,
258 signer,
259 sender_address,
260 eip1559_fee_increase_percentage,
261 eip1559_max_fee_per_gas_in_gwei,
262 blob_gas_multiplier,
263 initialize_addresses,
264 )
265 .await
266 }
267}
268
269#[derive(Debug)]
270pub(crate) struct Eip712PermitData {
271 pub deadline: AlloyU256,
272 pub v: u8,
273 pub r: B256,
274 pub s: B256,
275}
276
277#[derive(Clone)]
278pub struct Ethereum {
279 router: AlloyAddress,
280 wvara: AlloyAddress,
281 middleware: AlloyAddress,
284 provider: AlloyProvider,
285 signer: Signer,
286 sender_address: Address,
287 eip1559_estimator: Eip1559Estimator,
288 eip1559_max_fee_per_gas_in_gwei: u128,
289}
290
291impl Ethereum {
292 pub const DEFAULT_ETHEREUM_RPC: &str = "ws://localhost:8545";
294 pub const DEFAULT_ROUTER_ADDRESS: AlloyAddress =
296 address!("Cf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9");
297
298 pub const NO_EIP1559_FEE_INCREASE_PERCENTAGE: u64 = 0;
300 pub const NO_EIP1559_MAX_FEE_PER_GAS_IN_GWEI: u128 = 0;
302 pub const INCREASED_EIP1559_FEE_INCREASE_PERCENTAGE: u64 = 15;
307
308 pub const NO_BLOB_GAS_MULTIPLIER: u128 = 1;
310 pub const INCREASED_BLOB_GAS_MULTIPLIER: u128 = 3;
314
315 pub const PERMIT_DEADLINE_OFFSET: u64 = 300; #[allow(clippy::too_many_arguments)]
319 pub(crate) async fn new(
320 ethereum_rpc_url: &str,
321 router_address: Address,
322 signer: Signer,
323 sender_address: Address,
324 eip1559_fee_increase_percentage: u64,
325 eip1559_max_fee_per_gas_in_gwei: u128,
326 blob_gas_multiplier: u128,
327 initialize_addresses: bool,
328 ) -> Result<Ethereum> {
329 let (provider, eip1559_estimator) = create_provider(
330 ethereum_rpc_url,
331 signer.clone(),
332 sender_address,
333 eip1559_fee_increase_percentage,
334 blob_gas_multiplier,
335 )
336 .await?;
337 let router_query = RouterQuery::from_provider(router_address, provider.root().clone());
338 let router = router_address.into();
339 let (wvara, middleware) = if initialize_addresses {
340 (
341 router_query.wvara_address().await?.into(),
342 router_query.middleware_address().await?.into(),
343 )
344 } else {
345 (AlloyAddress::ZERO, AlloyAddress::ZERO)
346 };
347 Ok(Self {
348 router,
349 wvara,
350 middleware,
351 provider,
352 signer,
353 sender_address,
354 eip1559_estimator,
355 eip1559_max_fee_per_gas_in_gwei,
356 })
357 }
358
359 pub(crate) async fn initialize_addresses(&mut self) -> Result<()> {
360 if self.wvara == AlloyAddress::ZERO && self.middleware == AlloyAddress::ZERO {
361 let router_query =
362 RouterQuery::from_provider(self.router, self.provider.root().clone());
363 self.wvara = router_query.wvara_address().await?.into();
364 self.middleware = router_query.middleware_address().await?.into();
365 }
366 Ok(())
367 }
368
369 pub fn signer(&self) -> &Signer {
370 &self.signer
371 }
372
373 pub(crate) fn sender(&self) -> Sender {
374 Sender::new(self.signer.clone(), self.sender_address).expect("infallible")
375 }
376
377 pub fn sender_address(&self) -> Address {
378 self.sender_address
379 }
380
381 pub fn provider(&self) -> AlloyProvider {
382 self.provider.clone()
383 }
384
385 pub async fn chain_id(&self) -> Result<u64> {
386 self.provider.get_chain_id().await.map_err(Into::into)
387 }
388
389 pub(crate) async fn get_latest_block_inner(
390 provider: &AlloyProvider,
391 ) -> Result<SimpleBlockData> {
392 Self::get_block_inner(provider, BlockId::latest()).await
393 }
394
395 pub(crate) async fn get_block_inner(
396 provider: &AlloyProvider,
397 block_id: impl IntoBlockId,
398 ) -> Result<SimpleBlockData> {
399 let block_resp = provider
400 .get_block(block_id.into_block_id())
401 .await
402 .with_context(|| "failed to get latest block")?
403 .ok_or_else(|| anyhow!("latest block not found"))?;
404 let height = block_resp
405 .number()
406 .try_into()
407 .with_context(|| "block number overflow")?;
408 let hash = block_resp.hash().0.into();
409 let header = block_resp.into_header();
410 let parent_hash = header.parent_hash.0.into();
411 let timestamp = header.timestamp;
412 let header = BlockHeader {
413 height,
414 timestamp,
415 parent_hash,
416 };
417 Ok(SimpleBlockData { hash, header })
418 }
419
420 pub async fn get_latest_block(&self) -> Result<SimpleBlockData> {
421 Self::get_latest_block_inner(&self.provider()).await
422 }
423
424 pub async fn get_block(&self, block_id: impl IntoBlockId) -> Result<SimpleBlockData> {
425 Self::get_block_inner(&self.provider(), block_id).await
426 }
427
428 pub(crate) async fn prepare_permit_data(
429 provider: &AlloyProvider,
430 wvara_query: WVaraQuery,
431 sender: &Sender,
432 spender: ActorId,
433 value: u128,
434 ) -> Result<Eip712PermitData> {
435 let signer_address = provider.default_signer_address();
436 let signer_actor_id = Address::from(signer_address).into();
437
438 let nonce = abi_utils::u256_to_uint256(wvara_query.nonces(signer_actor_id).await?);
439 let eip712_domain = wvara_query.eip712_domain().await?;
440 let SimpleBlockData {
441 header: BlockHeader { timestamp, .. },
442 ..
443 } = Self::get_latest_block_inner(provider).await?;
444 let deadline = AlloyU256::from(
445 timestamp
446 .checked_add(Self::PERMIT_DEADLINE_OFFSET)
447 .expect("infallible"),
448 );
449
450 let permit = Permit {
451 owner: signer_address,
452 spender: spender.into(),
453 value: AlloyU256::from(value),
454 nonce,
455 deadline,
456 };
457
458 let hash = permit.eip712_signing_hash(&eip712_domain);
459 let signature = sender.sign_hash(&hash).await?;
460
461 let v = (signature.v() as u8) + 27;
462 let r = signature.r().into();
463 let s = signature.s().into();
464
465 Ok(Eip712PermitData { deadline, v, r, s })
466 }
467
468 pub fn mirror(&self, actor_id: ActorId) -> Mirror {
469 Mirror::new(actor_id.into(), self.wvara, self.sender(), self.provider())
470 }
471
472 pub fn router(&self) -> Router {
473 Router::new(
474 self.router,
475 self.wvara,
476 self.eip1559_estimator.clone(),
477 self.eip1559_max_fee_per_gas_in_gwei,
478 self.sender(),
479 self.provider(),
480 )
481 }
482
483 pub fn wrapped_vara(&self) -> WVara {
484 WVara::new(self.wvara, self.provider())
485 }
486
487 pub fn middleware(&self) -> Middleware {
488 assert_ne!(
489 self.middleware,
490 AlloyAddress::ZERO,
491 "Middleware address is zero. Make sure to deploy the middleware contract and pass `with_middleware` flag to `EthereumDeployer`."
492 );
493 Middleware::new(self.middleware, self.provider())
494 }
495}
496
497pub(crate) async fn create_provider(
498 rpc_url: &str,
499 signer: Signer,
500 sender_address: Address,
501 eip1559_fee_increase_percentage: u64,
502 blob_gas_multiplier: u128,
503) -> Result<(AlloyProvider, Eip1559Estimator)> {
504 let eip1559_estimator = Eip1559Estimator::new(move |base_fee_per_gas, rewards| {
505 utils::eip1559_default_estimator(base_fee_per_gas, rewards)
506 .scaled_by_pct(eip1559_fee_increase_percentage)
507 });
508 Ok((
509 ProviderBuilder::default()
510 .with_eip1559_estimator(eip1559_estimator.clone())
511 .with_blob_gas_estimator(BlobGasEstimator::scaled(blob_gas_multiplier))
512 .with_simple_nonce_management()
513 .fetch_chain_id()
514 .wallet(EthereumWallet::new(Sender::new(signer, sender_address)?))
515 .connect(rpc_url)
516 .await?,
517 eip1559_estimator,
518 ))
519}
520
521#[derive(Debug, Clone)]
522struct Sender {
523 signer: Signer,
524 sender: PublicKey,
525 chain_id: Option<ChainId>,
526}
527
528impl Sender {
529 pub fn new(signer: Signer, sender_address: Address) -> Result<Self> {
530 let sender = signer
531 .get_key_by_address(sender_address)?
532 .ok_or_else(|| anyhow!("no key found for {sender_address}"))?;
533
534 Ok(Self {
535 signer,
536 sender,
537 chain_id: None,
538 })
539 }
540}
541
542#[async_trait]
543impl AlloySigner for Sender {
544 async fn sign_hash(&self, hash: &B256) -> SignerResult<Signature> {
545 self.sign_hash_sync(hash)
546 }
547
548 fn address(&self) -> AlloyAddress {
549 self.sender.to_address().into()
550 }
551
552 fn chain_id(&self) -> Option<ChainId> {
553 self.chain_id
554 }
555
556 fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
557 self.chain_id = chain_id;
558 }
559}
560
561#[async_trait]
562impl TxSigner<Signature> for Sender {
563 fn address(&self) -> AlloyAddress {
564 self.sender.to_address().into()
565 }
566
567 async fn sign_transaction(
568 &self,
569 tx: &mut dyn SignableTransaction<Signature>,
570 ) -> SignerResult<Signature> {
571 sign_transaction_with_chain_id!(self, tx, self.sign_hash_sync(&tx.signature_hash()))
572 }
573}
574
575impl SignerSync for Sender {
576 fn sign_hash_sync(&self, hash: &B256) -> SignerResult<Signature> {
577 let digest = Digest(hash.0);
578 let signature = self
579 .signer
580 .sign_digest(self.sender, digest, None)
581 .map_err(|err| SignerError::Other(err.into()))?;
582 Signature::from_raw(&signature.as_raw_bytes()).map_err(|err| SignerError::Other(err.into()))
583 }
584
585 fn chain_id_sync(&self) -> Option<ChainId> {
586 self.chain_id
587 }
588}
589
590#[async_trait::async_trait]
591pub trait TryGetReceipt<N: Network> {
592 async fn try_get_receipt(self) -> Result<N::ReceiptResponse>;
594
595 async fn try_get_message_send_receipt(self) -> Result<(H256, MessageId)>;
597
598 async fn try_get_receipt_check_reverted(self) -> Result<N::ReceiptResponse>;
600}
601
602#[async_trait::async_trait]
603impl TryGetReceipt<network::Ethereum> for PendingTransactionBuilder<network::Ethereum> {
604 async fn try_get_receipt(self) -> Result<TransactionReceipt> {
605 let tx_hash = *self.tx_hash();
606 let provider = self.provider().clone();
607
608 let mut err = match self.get_receipt().await {
609 Ok(r) => return Ok(r),
610 Err(err) => err,
611 };
612
613 log::trace!("Failed to get transaction receipt for {tx_hash}. Retrying...");
614 for n in 0..20 {
615 log::trace!("Attempt {n}. Error - {err}");
616 match err {
617 PendingTransactionError::TransportError(RpcError::NullResp) => {}
618 _ => break,
619 }
620
621 tokio::time::sleep(Duration::from_millis(100)).await;
622
623 match provider.get_transaction_receipt(tx_hash).await {
624 Ok(Some(r)) => return Ok(r),
625 Ok(None) => {}
626 Err(e) => err = e.into(),
627 }
628 }
629
630 Err(anyhow!(
631 "Failed to get transaction receipt for {tx_hash}: {err}"
632 ))
633 }
634
635 async fn try_get_message_send_receipt(self) -> Result<(H256, MessageId)> {
636 let receipt = self.try_get_receipt().await?;
637 let tx_hash = (*receipt.transaction_hash).into();
638 let mut message_id = None;
639
640 for log in receipt.inner.logs() {
641 if log.topic0() == Some(&mirror::signatures::MESSAGE_QUEUEING_REQUESTED) {
642 let event = crate::decode_log::<IMirror::MessageQueueingRequested>(log)?;
643
644 message_id = Some((*event.id).into());
645
646 break;
647 }
648 }
649
650 let message_id =
651 message_id.ok_or_else(|| anyhow!("Couldn't find `MessageQueueingRequested` log"))?;
652
653 Ok((tx_hash, message_id))
654 }
655
656 async fn try_get_receipt_check_reverted(self) -> Result<TransactionReceipt> {
657 let provider = self.provider().clone();
658 let receipt = self.try_get_receipt().await?;
659
660 let try_request_error_reason = async |provider: RootProvider| {
661 let tx = provider
662 .get_transaction_by_hash(receipt.transaction_hash)
663 .await
664 .ok()??;
665 let request = TransactionRequest::from_recovered_transaction(tx.into_recovered());
666 provider
667 .call(request)
668 .block(receipt.block_hash?.into())
669 .await
670 .err()
671 };
672
673 if receipt.status() {
674 Ok(receipt)
675 } else if let Some(err) = try_request_error_reason(provider).await {
676 Err(anyhow!(
677 "Transaction {:?} was reverted at block {:?}: {err}",
678 receipt.transaction_hash,
679 receipt.block_hash
680 ))
681 } else {
682 Err(anyhow!(
683 "Transaction {:?} was reverted by unknown reason at block {:?}",
684 receipt.transaction_hash,
685 receipt.block_hash
686 ))
687 }
688 }
689}
690
691pub(crate) fn decode_log<E: SolEvent>(log: &Log) -> Result<E> {
692 E::decode_raw_log(log.topics(), &log.data().data).map_err(Into::into)
693}
694
695macro_rules! signatures_consts {
696 (
697 $type_name:ident;
698 $( $const_name:ident: $name:ident, )*
699 ) => {
700 $(
701 pub const $const_name: alloy::primitives::B256 = $type_name::$name::SIGNATURE_HASH;
702 )*
703
704 pub const ALL: &[alloy::primitives::B256] = &[$($const_name,)*];
705 };
706}
707
708pub(crate) use signatures_consts;
709
710use crate::wvara::WVara;
711
712pub trait IntoBlockId {
714 fn into_block_id(self) -> BlockId;
715}
716
717impl IntoBlockId for H256 {
718 fn into_block_id(self) -> BlockId {
719 BlockId::hash(self.0.into())
720 }
721}
722
723impl IntoBlockId for u32 {
724 fn into_block_id(self) -> BlockId {
725 BlockId::number(self.into())
726 }
727}
728
729impl IntoBlockId for u64 {
730 fn into_block_id(self) -> BlockId {
731 BlockId::number(self)
732 }
733}
734
735impl IntoBlockId for BlockId {
736 fn into_block_id(self) -> BlockId {
737 self
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn sender_signs_prehashed_message() {
747 let signer = Signer::memory();
748 let public_key = signer.generate().unwrap();
749 let address = signer.address(public_key);
750
751 let sender = Sender::new(signer.clone(), address).expect("sender init");
752
753 let hash = B256::from([0xAA; 32]);
754 let signature = sender.sign_hash_sync(&hash).expect("signature");
755
756 let recovered_vk = signature.recover_from_prehash(&hash).expect("recover");
757 let recovered_bytes: [u8; 33] = recovered_vk
758 .to_encoded_point(true)
759 .as_bytes()
760 .try_into()
761 .expect("compressed size");
762 let recovered_address = gsigner::secp256k1::PublicKey::from_bytes(recovered_bytes)
763 .expect("valid public key")
764 .to_address();
765
766 assert_eq!(recovered_address, address);
767 }
768}