Skip to main content

ethexe_ethereum/
lib.rs

1// Copyright (C) Gear Technologies Inc.
2// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
3
4//! # ethexe-ethereum
5//!
6//! Ethereum contract-interaction layer for the ethexe system: typed Rust wrappers around the
7//! ethexe Solidity contracts (Router, Mirror, WrappedVara, Middleware), built on the `alloy`
8//! client stack.
9//!
10//! Consumed by `ethexe-observer`, `ethexe-consensus`, `ethexe-cli`, `ethexe-service`, and
11//! `ethexe-sdk`; depends on `ethexe-common`, `gsigner`, and `gprimitives`. This is purely a
12//! client/binding layer: it does not execute Gear programs (`ethexe-processor`), watch the
13//! chain (`ethexe-observer`), implement consensus (`ethexe-consensus`), or persist data
14//! (`ethexe-db`).
15//!
16//! ## Usage
17//!
18//! Build a client with [`EthereumBuilder`], then call methods on the returned [`Ethereum`]:
19//!
20//! ```rust,no_run
21//! use ethexe_ethereum::{Ethereum, EthereumBuilder};
22//!
23//! # async fn example(signer: gsigner::secp256k1::Signer, sender: gsigner::secp256k1::Address) -> anyhow::Result<()> {
24//! let ethereum = EthereumBuilder::default()
25//!     .router_address(Ethereum::DEFAULT_ROUTER_ADDRESS.into())
26//!     .signer(signer)
27//!     .sender_address(sender)
28//!     .build()
29//!     .await?;
30//!
31//! let router = ethereum.router(); // Router.sol write handle
32//! let mirror = ethereum.mirror(Default::default()); // per-program Mirror.sol handle
33//! mirror.send_message(vec![], 0).await?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! ## Public API
39//!
40//! - [`EthereumBuilder`] — Fluent builder: RPC URL, router address, signer, fee tuning
41//! - [`Ethereum`] — Central connection; vends contract handles and block queries
42//! - [`AlloyProvider`] — Concrete alloy provider stack; returned by [`Ethereum::provider`]
43//! - [`router::Router`] — Router.sol: code validation, batch commitment, program creation
44//! - [`mirror::Mirror`] — Mirror.sol: send/reply messages, claim value, top up balances
45//! - [`wvara::WVara`] — WrappedVara ERC-20: transfer, approve, mint, balance queries
46//! - [`middleware::Middleware`] — Validator election/permissions contract
47//! - [`TryGetReceipt`] — Retry-aware receipt retrieval with log parsing and revert detection
48//! - [`IntoBlockId`] — Convert `H256` / `u32` / `u64` into a block id
49//! - [`deploy::EthereumDeployer`] — Deploy the full contract set in local/test environments
50//! - [`primitives`] / [`abi`] — Re-exported `alloy::primitives`; generated ABI bindings
51//!
52//! Each contract handle also vends a read-only query counterpart (`RouterQuery`, `MirrorQuery`,
53//! `WVaraQuery`, `MiddlewareQuery`); `Router`, `Mirror`, and `WVara` additionally vend
54//! event-filter builders.
55//!
56//! ## Invariants
57//!
58//! - [`EthereumBuilder::build`] errors if `signer` or `sender_address` are not set.
59//! - [`Ethereum::middleware`] panics if the middleware address is zero, which happens when the
60//!   contract set was deployed without the middleware flag on [`deploy::EthereumDeployer`].
61//! - [`TryGetReceipt::try_get_message_send_receipt`] requires a `MessageQueueingRequested` log
62//!   in the receipt or returns an error.
63
64#![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    /// Sets the Ethereum RPC URL to connect to.
144    ///
145    /// The default is [`Ethereum::DEFAULT_ETHEREUM_RPC`].
146    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    /// Sets the Ethereum router contract address.
152    ///
153    /// The default is [`Ethereum::DEFAULT_ROUTER_ADDRESS`].
154    pub fn router_address(mut self, router_address: Address) -> Self {
155        self.router_address = Some(router_address);
156        self
157    }
158
159    /// Sets the signer to use for signing transactions.
160    pub fn signer(mut self, signer: Signer) -> Self {
161        self.signer = Some(signer);
162        self
163    }
164
165    /// Sets the sender address to use for signing transactions.
166    pub fn sender_address(mut self, sender_address: Address) -> Self {
167        self.sender_address = Some(sender_address);
168        self
169    }
170
171    /// Sets the EIP-1559 fee increase percentage (from "medium") to use for transaction fee estimation.
172    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    /// Sets the EIP-1559 fee increase percentage (from "medium") to use for transaction fee estimation.
181    ///
182    /// The default is [`Ethereum::NO_EIP1559_FEE_INCREASE_PERCENTAGE`].
183    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    /// Sets the EIP-1559 fee increase percentage to value that increases the estimated fee
188    /// by 15% compared to "medium" estimation.
189    pub fn with_eip1559_increased_fee(self) -> Self {
190        self.eip1559_fee_increase_percentage(Ethereum::INCREASED_EIP1559_FEE_INCREASE_PERCENTAGE)
191    }
192
193    /// Sets the EIP-1559 max fee per gas in gwei to use for transaction fee estimation
194    /// (for batch commitments).
195    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    /// Sets the blob gas multiplier to use for transaction fee estimation.
204    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    /// Sets the blob gas multiplier to use for transaction fee estimation.
210    ///
211    /// The default is [`Ethereum::NO_BLOB_GAS_MULTIPLIER`].
212    pub fn blob_gas_multiplier(self, blob_gas_multiplier: u128) -> Self {
213        self.blob_gas_multiplier_opt(Some(blob_gas_multiplier))
214    }
215
216    /// Sets the blob gas multiplier to value that increases the estimated blob gas by 3x.
217    pub fn with_increased_blob_gas_multiplier(self) -> Self {
218        self.blob_gas_multiplier(Ethereum::INCREASED_BLOB_GAS_MULTIPLIER)
219    }
220
221    /// Sets whether to initialize the router-related addresses (wvara and middleware)
222    /// during the construction of the [`Ethereum`] instance.
223    pub(crate) fn initialize_addresses(mut self, initialize_addresses: bool) -> Self {
224        self.initialize_addresses = Some(initialize_addresses);
225        self
226    }
227
228    /// Sets whether to initialize the router-related addresses (wvara and middleware)
229    /// during the construction of the [`Ethereum`] instance.
230    pub(crate) fn without_initializing_addresses(self) -> Self {
231        self.initialize_addresses(false)
232    }
233
234    /// Constructs [`Ethereum`] instance from the builder.
235    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    /// NOTE: Middleware address will be zero if `with_middleware` flag was not passed
282    /// for [`deploy::EthereumDeployer`].
283    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    /// Default Ethereum RPC.
293    pub const DEFAULT_ETHEREUM_RPC: &str = "ws://localhost:8545";
294    /// Default Ethereum router contract address.
295    pub const DEFAULT_ROUTER_ADDRESS: AlloyAddress =
296        address!("Cf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9");
297
298    /// Default EIP-1559 fee increase percentage for transaction fee estimation.
299    pub const NO_EIP1559_FEE_INCREASE_PERCENTAGE: u64 = 0;
300    /// Default EIP-1559 max fee per gas in gwei for transaction fee estimation (for batch commitments).
301    pub const NO_EIP1559_MAX_FEE_PER_GAS_IN_GWEI: u128 = 0;
302    /// EIP-1559 fee increase percentage for transaction fee estimation that increases the estimated fee
303    /// by 15% compared to "medium" estimation.
304    ///
305    /// This is useful for faster batch commitment.
306    pub const INCREASED_EIP1559_FEE_INCREASE_PERCENTAGE: u64 = 15;
307
308    /// Default blob gas multiplier for transaction fee estimation.
309    pub const NO_BLOB_GAS_MULTIPLIER: u128 = 1;
310    /// Blob gas multiplier for transaction fee estimation that increases the estimated blob gas by 3x.
311    ///
312    /// This is useful mostly on testnets, where a lot of L2s can spam the network with blob transactions.
313    pub const INCREASED_BLOB_GAS_MULTIPLIER: u128 = 3;
314
315    /// Default offset for permit deadline from the current block timestamp.
316    pub const PERMIT_DEADLINE_OFFSET: u64 = 300; // 5 minutes
317
318    #[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    /// Works like `self.get_receipt().await`, but retries a few times if rpc returns a null response.
593    async fn try_get_receipt(self) -> Result<N::ReceiptResponse>;
594
595    /// Works like `self.try_get_receipt().await`, but also extracts the message id from the logs.
596    async fn try_get_message_send_receipt(self) -> Result<(H256, MessageId)>;
597
598    /// Works like `self.try_get_receipt().await`, but also checks if the transaction was reverted.
599    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
712/// A helping trait for converting various types into `alloy::eips::BlockId`.
713pub 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}