Skip to main content

hypercall_signer/
exchange.rs

1use alloy::{
2    primitives::{Address, U256},
3    providers::DynProvider,
4    sol,
5};
6use hypercall_db::NonceWriter;
7use hypercall_types::WalletAddress;
8
9use crate::RsmSignerError;
10
11sol! {
12    #[sol(rpc)]
13    contract Exchange {
14        function isNonceUsed(address signer, uint256 nonce) external view returns (bool);
15    }
16}
17
18/// Repairs the persisted next nonce for `signer_address` from on-chain state.
19///
20/// Scanning starts at the persisted `next_nonce`, or zero if no tracker exists,
21/// and advances while `Exchange.isNonceUsed` returns true. On success, both
22/// `next_nonce` and `last_synced_nonce` are persisted to the first unused nonce.
23/// Callers should serialize repairs per signer with normal signing startup.
24pub async fn repair_nonce_tracker_for_signer(
25    db: &dyn NonceWriter,
26    provider: &DynProvider,
27    exchange_address: Address,
28    signer_address: WalletAddress,
29) -> Result<(), RsmSignerError> {
30    let saved = db
31        .get_rsm_signer_nonce(&signer_address)
32        .await
33        .map_err(|error| {
34            RsmSignerError::PersistenceFailed(format!(
35                "failed to load RSM signer nonce tracker: {}",
36                error
37            ))
38        })?;
39    let mut next_nonce = match saved {
40        Some(record) => u64::try_from(record.next_nonce).map_err(|_| {
41            RsmSignerError::PersistenceFailed(format!(
42                "persisted next_nonce {} for signer {} is negative",
43                record.next_nonce, signer_address
44            ))
45        })?,
46        None => 0,
47    };
48
49    let exchange = Exchange::new(exchange_address, provider);
50    while exchange
51        .isNonceUsed(signer_address.inner(), U256::from(next_nonce))
52        .call()
53        .await
54        .map_err(|error| {
55            RsmSignerError::SigningFailed(format!(
56                "failed to query Exchange.isNonceUsed({}, {}): {}",
57                signer_address, next_nonce, error
58            ))
59        })?
60    {
61        next_nonce = next_nonce.checked_add(1).ok_or_else(|| {
62            RsmSignerError::PersistenceFailed(
63                "RSM nonce overflow while repairing tracker".to_string(),
64            )
65        })?;
66    }
67
68    db.save_rsm_signer_nonce(&hypercall_db::RsmSignerNonceRecord {
69        signer_address,
70        next_nonce: i64::try_from(next_nonce).map_err(|_| {
71            RsmSignerError::PersistenceFailed(format!(
72                "next_nonce {} exceeds i64 range",
73                next_nonce
74            ))
75        })?,
76        last_synced_nonce: Some(i64::try_from(next_nonce).map_err(|_| {
77            RsmSignerError::PersistenceFailed(format!(
78                "next_nonce {} exceeds i64 range",
79                next_nonce
80            ))
81        })?),
82        created_at: None,
83        updated_at: None,
84    })
85    .await
86    .map_err(|error| {
87        RsmSignerError::PersistenceFailed(format!(
88            "failed to persist repaired RSM nonce tracker: {}",
89            error
90        ))
91    })?;
92
93    Ok(())
94}
95
96/// Checks the exchange contract for whether `nonce` is consumed by `signer_address`.
97///
98/// This helper is read-only and does not mutate DB nonce tracker state.
99pub async fn is_nonce_used_for_signer(
100    provider: &DynProvider,
101    exchange_address: Address,
102    signer_address: WalletAddress,
103    nonce: u64,
104) -> Result<bool, RsmSignerError> {
105    let exchange = Exchange::new(exchange_address, provider);
106    exchange
107        .isNonceUsed(signer_address.inner(), U256::from(nonce))
108        .call()
109        .await
110        .map_err(|error| {
111            RsmSignerError::SigningFailed(format!("failed to check nonce on-chain: {}", error))
112        })
113}