Skip to main content

df_elysium_core/client/
mod.rs

1//! Connecting to Elysium: the [`Client`] that reads, signs and sends over one network's JSON-RPC,
2//! the [`network`], the methods Nitro adds ([`rpc`]) and the records it answers with
3//! ([`records`]), typed contract calls ([`method`], [`event`], [`read`]), and the native and
4//! browser wires from df-hyperliquid-core.
5
6pub mod event;
7pub mod method;
8pub mod network;
9pub mod read;
10pub mod records;
11pub mod rpc;
12
13#[cfg(feature = "native")]
14pub use df_hyperliquid_core::client::native;
15#[cfg(feature = "web")]
16pub use df_hyperliquid_core::client::web;
17
18pub use df_hyperliquid_core::client::evm::tx::{Call, Eip1559, Fees, create_address, tx_hash, with_headroom};
19pub use df_hyperliquid_core::client::{Http, Method as HttpMethod, OFFLINE, Request, Response, TransportError};
20pub use event::{BadLog, Event};
21pub use method::{Arg, Method};
22pub use network::{Network, TESTNET_CHAIN_ID, UnknownNetwork};
23pub use read::{Read, ReadError};
24pub use records::{Block, BlockTransactions, FeeHistory, Log, Receipt, Transaction, TxKind};
25pub use rpc::{BlockTag, CallRequest, LogFilter, Query, Rpc, RpcError};
26
27use crate::account::Nonces;
28#[cfg(feature = "sign")]
29use crate::account::{SignError, Signer, verify};
30use crate::gas::{self, Estimate};
31use crate::utils::{Address, Dec};
32use crate::{B256, U256};
33
34/// Why a transaction was not sent. One whose answer never came
35/// ([`TransportError::Unreachable`]) may have landed, so it is never resent here.
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37pub enum SendError {
38    #[cfg(feature = "sign")]
39    #[error(transparent)]
40    Sign(#[from] SignError),
41    #[error(transparent)]
42    Rpc(#[from] RpcError),
43}
44
45/// Reads for any module (`nitro::sys::arb_os_version()` through [`Client::read`]) and, with a
46/// [`Signer`], transactions. `S` is `()` for a client that only reads.
47///
48/// [`Client::parent`] is the same client on HyperEVM, Elysium's parent, for the bridge's calls
49/// there: deposits, retryable tickets and claims.
50pub struct Client<H, S = ()> {
51    rpc: Rpc<H>,
52    network: Network,
53    chain: u64,
54    signer: S,
55    nonces: Nonces,
56}
57
58/// Its network, chain and node; never its signer or its wire.
59impl<H: Http, S> std::fmt::Debug for Client<H, S> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("Client")
62            .field("network", &self.network)
63            .field("chain", &self.chain)
64            .field("url", &self.rpc.url())
65            .finish_non_exhaustive()
66    }
67}
68
69impl<H: Http> Client<H> {
70    /// A client that reads and never signs, through Kinetiq's public node.
71    pub fn reader(http: H, network: Network) -> Self {
72        Client::new(http, network, ())
73    }
74}
75
76impl<H: Http, S> Client<H, S> {
77    /// One network through Kinetiq's public node ([`Network::rpc_url`]), signing with `signer`.
78    pub fn new(http: H, network: Network, signer: S) -> Self {
79        Client::at(http, network.rpc_url(), network, signer)
80    }
81
82    /// One network through another node: Conduit's, or a provider's with its key in the URL.
83    pub fn at(http: H, url: impl Into<String>, network: Network, signer: S) -> Self {
84        Client { rpc: Rpc::new(http, url), network, chain: network.chain_id(), signer, nonces: Nonces::new() }
85    }
86
87    /// The network's parent, HyperEVM, through its own node
88    /// ([`df_hyperliquid_core::Network::evm_rpc_url`]), signing for its chain
89    /// ([`Network::parent_chain_id`]): for the bridge's calls there. Sends and Ethereum's reads work
90    /// as on Elysium, and receipts and transactions read; Nitro's own (blocks, [`Client::estimate`])
91    /// are Elysium's, so read HyperEVM's blocks through [`Client::rpc`].
92    pub fn parent(http: H, network: Network, signer: S) -> Self {
93        Client {
94            rpc: Rpc::new(http, network.hyperliquid().evm_rpc_url()),
95            network,
96            chain: network.parent_chain_id(),
97            signer,
98            nonces: Nonces::new(),
99        }
100    }
101
102    #[must_use]
103    pub const fn network(&self) -> Network {
104        self.network
105    }
106
107    /// The chain this client signs for: Elysium's, or HyperEVM's for [`Client::parent`].
108    #[must_use]
109    pub const fn chain(&self) -> u64 {
110        self.chain
111    }
112
113    /// The node, for any of df-hyperliquid-core's `eth_` reads.
114    #[must_use]
115    pub const fn rpc(&self) -> &Rpc<H> {
116        &self.rpc
117    }
118
119    #[must_use]
120    pub const fn http(&self) -> &H {
121        self.rpc.http()
122    }
123
124    #[must_use]
125    pub const fn signer(&self) -> &S {
126        &self.signer
127    }
128
129    /// The nonce kept between sends.
130    #[must_use]
131    pub const fn nonces(&self) -> &Nonces {
132        &self.nonces
133    }
134
135    /// Any query, sent once.
136    pub async fn run<T>(&self, query: Query<T>) -> Result<T, RpcError> {
137        rpc::run(&self.rpc, query).await
138    }
139
140    /// A typed read at the latest block.
141    pub async fn read<T>(&self, read: &Read<T>) -> Result<T, ReadError> {
142        read.on(&self.rpc, BlockTag::Latest).await
143    }
144
145    pub async fn chain_id(&self) -> Result<u64, RpcError> {
146        self.rpc.chain_id().await
147    }
148
149    pub async fn block_number(&self) -> Result<u64, RpcError> {
150        self.rpc.block_number().await
151    }
152
153    /// An address's HYPE in wei, at the latest block.
154    pub async fn balance(&self, owner: Address) -> Result<U256, RpcError> {
155        self.rpc.balance(owner, BlockTag::Latest).await
156    }
157
158    /// An address's HYPE, exactly.
159    pub async fn hype(&self, owner: Address) -> Result<Dec, RpcError> {
160        let wei = self.balance(owner).await?;
161        crate::utils::hype(wei)
162            .map_err(|error| RpcError::Unreadable { method: "eth_getBalance", detail: error.to_string() })
163    }
164
165    /// The next nonce `owner` may use, counting what the sequencer has taken.
166    pub async fn nonce(&self, owner: Address) -> Result<u64, RpcError> {
167        self.rpc.transaction_count(owner, BlockTag::Pending).await
168    }
169
170    /// Empty for an account that is not a contract; `0xfe` for a precompile.
171    pub async fn code(&self, at: Address) -> Result<Vec<u8>, RpcError> {
172        self.rpc.code(at, BlockTag::Latest).await
173    }
174
175    /// What one unit of gas costs now, in wei: the base fee. There is no tip to add.
176    pub async fn gas_price(&self) -> Result<U256, RpcError> {
177        self.rpc.gas_price().await
178    }
179
180    /// The gas `call` takes when `from` sends it, split into Elysium's execution and HyperEVM's
181    /// data, at the base fees it would pay ([`crate::nitro::node::gas_estimate_components`]).
182    pub async fn estimate(&self, from: Option<Address>, call: &Call) -> Result<Estimate, ReadError> {
183        self.read(&gas::components(from, call)).await
184    }
185
186    pub async fn receipt(&self, hash: B256) -> Result<Option<Receipt>, RpcError> {
187        self.run(rpc::nitro::receipt(hash)).await
188    }
189
190    pub async fn transaction(&self, hash: B256) -> Result<Option<Transaction>, RpcError> {
191        self.run(rpc::nitro::transaction(hash)).await
192    }
193
194    pub async fn block(&self, block: BlockTag, full: bool) -> Result<Option<Block>, RpcError> {
195        self.run(rpc::nitro::block(block, full)).await
196    }
197
198    pub async fn block_by_hash(&self, hash: B256, full: bool) -> Result<Option<Block>, RpcError> {
199        self.run(rpc::nitro::block_by_hash(hash, full)).await
200    }
201
202    pub async fn block_receipts(&self, block: BlockTag) -> Result<Option<Vec<Receipt>>, RpcError> {
203        self.run(rpc::nitro::block_receipts(block)).await
204    }
205
206    /// The logs `filter` matches. A filter that names no address and no topic is sent in windows
207    /// of [`Network::log_span`] blocks, its tags read as the head; any other is sent whole.
208    pub async fn logs(&self, filter: &LogFilter) -> Result<Vec<Log>, RpcError> {
209        if filter.block_hash.is_some() || rpc::is_narrow(filter) {
210            return self.rpc.logs(filter).await;
211        }
212        let fixed = |tag: Option<BlockTag>| match tag {
213            Some(BlockTag::Number(number)) => Some(number),
214            Some(BlockTag::Earliest) => Some(0),
215            _ => None,
216        };
217        let head = match (fixed(filter.from_block), fixed(filter.to_block)) {
218            (Some(_), Some(_)) => 0,
219            _ => self.rpc.block_number().await?,
220        };
221        let from = fixed(filter.from_block).unwrap_or(head);
222        let to = fixed(filter.to_block).unwrap_or(head);
223        let mut logs = Vec::new();
224        for (first, last) in rpc::pages(from, to, self.network.log_span()) {
225            logs.extend(self.rpc.logs(&rpc::window(filter, first, last)).await?);
226        }
227        Ok(logs)
228    }
229}
230
231/// Sending needs the `sign` feature: each signature is recovered and must match the signer before
232/// anything is sent, since a remote signer can sign as another address.
233#[cfg(feature = "sign")]
234impl<H: Http, S: Signer> Client<H, S> {
235    /// The address this client sends from.
236    pub fn address(&self) -> Address {
237        self.signer.address()
238    }
239
240    /// The transaction that makes `call`: this client's chain, the next nonce, the node's gas
241    /// estimate with [`with_headroom`], and fees over the current base fee ([`gas::fees`]).
242    pub async fn prepare(&self, call: Call) -> Result<Eip1559, SendError> {
243        let from = self.signer.address();
244        let gas = self.rpc.estimate_gas(&CallRequest::of(Some(from), &call)).await?;
245        let base = self.rpc.gas_price().await?;
246        let nonce = match self.nonces.kept() {
247            Some(nonce) => nonce,
248            None => self.nonces.read(self.nonce(from).await?),
249        };
250        Ok(call.transaction(self.chain, nonce, with_headroom(gas), gas::fees(base)))
251    }
252
253    /// The bytes a node is sent: signed by the signer, the signature recovered and checked.
254    pub async fn sign(&self, tx: &Eip1559) -> Result<Vec<u8>, SignError> {
255        let hash = tx.signing_hash();
256        let signature = self.signer.sign_hash(&hash).await?;
257        verify(&hash.0, &signature, self.signer.address())?;
258        tx.signed(&signature)
259    }
260
261    /// Prepares, signs and sends `call` once; the hash the node took it under.
262    pub async fn send(&self, call: Call) -> Result<B256, SendError> {
263        let tx = self.prepare(call).await?;
264        self.send_prepared(&tx).await
265    }
266
267    /// Signs and sends a prepared transaction once.
268    pub async fn send_prepared(&self, tx: &Eip1559) -> Result<B256, SendError> {
269        let sent = async { Ok::<_, SendError>(self.rpc.send_raw_transaction(&self.sign(tx).await?).await?) };
270        self.kept(tx.nonce, sent.await)
271    }
272
273    /// Prepares, signs and sends `call` once with `eth_sendRawTransactionSync`: the answer is its
274    /// receipt, a reverted one included. The node gives up after `timeout_ms` (its own default
275    /// when `None`); then the transaction may still land.
276    pub async fn send_sync(&self, call: Call, timeout_ms: Option<u64>) -> Result<Receipt, SendError> {
277        let tx = self.prepare(call).await?;
278        self.send_prepared_sync(&tx, timeout_ms).await
279    }
280
281    /// Signs and sends a prepared transaction once, answering with its receipt.
282    pub async fn send_prepared_sync(&self, tx: &Eip1559, timeout_ms: Option<u64>) -> Result<Receipt, SendError> {
283        let sent = async {
284            let raw = self.sign(tx).await?;
285            Ok::<_, SendError>(self.run(rpc::nitro::send_raw_transaction_sync(&raw, timeout_ms)).await?)
286        };
287        self.kept(tx.nonce, sent.await)
288    }
289
290    /// Keeps the nonce past a sent transaction, or forgets it when the send failed.
291    fn kept<T>(&self, nonce: u64, sent: Result<T, SendError>) -> Result<T, SendError> {
292        match &sent {
293            Ok(_) => self.nonces.sent(nonce),
294            Err(_) => self.nonces.forget(),
295        }
296        sent
297    }
298}
299
300#[cfg(test)]
301#[path = "tests/client.rs"]
302mod tests;