Skip to main content

paralock_evm/
escrow.rs

1//! Pieces shared by the two escrow clients.
2
3use std::future::Future;
4use std::time::{Duration, Instant};
5
6use alloy::primitives::{Address, U256};
7use serde::{Deserialize, Serialize};
8
9use crate::connection::{EvmConnection, TxResult};
10use crate::error::EvmError;
11
12/// Where event scans start and how many blocks one `eth_getLogs` request may span.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct ScanConfig {
16    /// First block to scan: the contract's deployment block (`startBlock` in `contracts/deployments/*.json`). A
17    /// value after the deployment makes older locks invisible ([`EvmError::LockEventMissing`]); 0 (the default) scans
18    /// the whole chain, which is slow on public networks.
19    pub from_block: u64,
20    /// Largest block span per `eth_getLogs` request. Public RPCs cap it: a window the node refuses (JSON-RPC error,
21    /// HTTP 400/413, or a range/result-size message) is halved automatically, and the client keeps the narrower
22    /// window that worked (`scan_window()`). Default 50,000.
23    pub max_block_range: u64,
24}
25
26impl Default for ScanConfig {
27    fn default() -> Self {
28        Self { from_block: 0, max_block_range: 50_000 }
29    }
30}
31
32impl ScanConfig {
33    /// Scan from `from_block` with the default window.
34    pub fn from_block(from_block: u64) -> Self {
35        Self { from_block, ..Self::default() }
36    }
37}
38
39/// Result of `approve_and_lock`: the approval (if one was needed) and the lock.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct LockReceipt {
43    /// The exact approval, or `None` if the allowance already covered the amount.
44    pub approval: Option<TxResult>,
45    /// The lock transaction.
46    pub lock: TxResult,
47}
48
49/// Checks shared by both `lock`s before anything is sent: the account is the sender, the amount and recipient are
50/// non-zero, and `now < expiry ≤ now + max_lock_seconds` in EVM time.
51pub(crate) async fn check_new_lock(
52    conn: &EvmConnection,
53    account: Address,
54    sender: Address,
55    recipient: Address,
56    amount: U256,
57    expiry: u64,
58) -> Result<(), EvmError> {
59    if sender != account {
60        return Err(EvmError::InvalidInput(format!(
61            "the swap's sender {sender} is not this client's account {account}"
62        )));
63    }
64    if amount.is_zero() {
65        return Err(EvmError::InvalidInput("amount is zero".to_owned()));
66    }
67    if recipient == Address::ZERO {
68        return Err(EvmError::InvalidInput("recipient is the zero address".to_owned()));
69    }
70    let now = conn.now().await?;
71    if expiry <= now {
72        return Err(EvmError::InvalidInput(format!("expiry {expiry} is not after the chain's time {now}")));
73    }
74    let max = conn.max_lock_seconds();
75    if expiry - now > max {
76        return Err(EvmError::InvalidInput(format!(
77            "expiry {expiry} is {} s after the chain's time {now}, more than the allowed {max} s (expiries are Unix \
78             seconds, not milliseconds)",
79            expiry - now
80        )));
81    }
82    Ok(())
83}
84
85/// Calls `check` every `interval` until it yields a value. Transient errors are retried; the last one is returned
86/// if `timeout` elapses first, else [`EvmError::Timeout`]. `None` waits forever (drop the future to cancel).
87pub(crate) async fn poll_until<T, F, Fut>(
88    interval: Duration,
89    timeout: Option<Duration>,
90    what: &str,
91    mut check: F,
92) -> Result<T, EvmError>
93where
94    F: FnMut() -> Fut,
95    Fut: Future<Output = Result<Option<T>, EvmError>>,
96{
97    // A timeout too large to represent is no timeout.
98    let deadline = timeout.and_then(|t| Instant::now().checked_add(t));
99    loop {
100        let last_error = match check().await {
101            Ok(Some(value)) => return Ok(value),
102            Ok(None) => None,
103            Err(err) if err.is_transient() => Some(err),
104            Err(err) => return Err(err),
105        };
106        if let (Some(deadline), Some(timeout)) = (deadline, timeout)
107            && Instant::now().checked_add(interval).is_none_or(|next| next > deadline)
108        {
109            return Err(last_error.unwrap_or(EvmError::Timeout { what: what.to_owned(), timeout }));
110        }
111        tokio::time::sleep(interval).await;
112    }
113}