arkiv_sdk/
client.rs

1use std::ops::Deref;
2use std::sync::Arc;
3
4use alloy::eips::BlockNumberOrTag;
5use alloy::primitives::Address;
6use alloy::providers::{DynProvider, Provider, ProviderBuilder};
7use alloy::rpc::client::ClientRef;
8use alloy::signers::local::PrivateKeySigner;
9use alloy::transports::http::reqwest::Url;
10use bigdecimal::BigDecimal;
11use bon::bon;
12use tokio::sync::Mutex;
13
14use crate::utils::wei_to_eth;
15
16/// Tracks and assigns sequential Ethereum nonces for concurrent transactions.
17pub struct NonceManager {
18    /// Last known on-chain nonce.
19    pub base_nonce: u64,
20    /// Number of in-flight (pending) transactions.
21    pub in_flight: u64,
22}
23
24impl NonceManager {
25    /// Returns the next available nonce and increments the in-flight counter.
26    pub async fn next_nonce(&mut self) -> u64 {
27        let nonce = self.base_nonce + self.in_flight;
28        self.in_flight += 1;
29        nonce
30    }
31
32    /// Marks a transaction as completed by decrementing the in-flight counter.
33    pub async fn complete(&mut self) {
34        if self.in_flight > 0 {
35            self.in_flight -= 1;
36        }
37    }
38}
39
40/// A client for interacting with the Arkiv system.
41/// Provides methods for account management, entity operations, balance queries, and event subscriptions.
42///
43/// # Example Usage
44///
45/// A client builder is provided for both [`ArkivClient`] and [`ArkivRoClient`],
46/// however, an instance of [`ArkivClient`] can be dereferenced to [`ArkivRoClient`] like so:
47///
48/// ```rs
49/// use arkiv_sdk::{ArkivClient, ArkivRoClient, PrivateKeySigner, Url};
50///
51/// let keypath = dirs::config_dir()
52///     .ok_or("Failed to get config directory")?
53///     .join("golembase")
54///     .join("wallet.json");
55/// let signer = PrivateKeySigner::decrypt_keystore(keypath, "password")?;
56/// let url = Url::parse("http://localhost:8545")?;
57///
58/// let client = ArkivClient::builder()
59///     .wallet(signer)
60///     .rpc_url(url)
61///     .build();
62///
63/// let ro_client: &ArkivRoClient = *client;
64/// ```
65#[derive(Clone)]
66pub struct ArkivRoClient {
67    /// The underlying provider for making RPC calls.
68    pub(crate) provider: DynProvider,
69}
70
71#[bon]
72impl ArkivRoClient {
73    /// Creates a new builder for `ArkivClient` with the given wallet and RPC URL.
74    /// Initializes the provider and sets up default configuration.
75    #[builder]
76    pub fn builder(rpc_url: Url, provider: Option<DynProvider>) -> Self {
77        let provider = provider.unwrap_or_else(|| {
78            ProviderBuilder::new()
79                .connect_http(rpc_url.clone())
80                .erased()
81        });
82
83        Self { provider }
84    }
85}
86
87/// A client for interacting with the Arkiv system.
88/// Provides methods for account management, entity operations, balance queries, and event subscriptions.
89///
90/// # Example Usage
91///
92/// A client builder is provided for both [`ArkivClient`] and [`ArkivRoClient`],
93/// however, an instance of [`ArkivClient`] can be dereferenced to [`ArkivRoClient`] like so:
94///
95/// ```rs
96/// use arkiv_sdk::{ArkivClient, ArkivRoClient, PrivateKeySigner, Url};
97///
98/// let keypath = dirs::config_dir()
99///     .ok_or("Failed to get config directory")?
100///     .join("golembase")
101///     .join("wallet.json");
102/// let signer = PrivateKeySigner::decrypt_keystore(keypath, "password")?;
103/// let url = Url::parse("http://localhost:8545")?;
104///
105/// let client = ArkivClient::builder()
106///     .wallet(signer)
107///     .rpc_url(url)
108///     .build();
109///
110/// let ro_client: &ArkivRoClient = *client;
111/// ```
112#[derive(Clone)]
113pub struct ArkivClient {
114    /// The underlying ArkivRoClient
115    pub(crate) ro_client: ArkivRoClient,
116    /// The Ethereum address of the client owner.
117    pub(crate) wallet: PrivateKeySigner,
118    /// Nonce manager for tracking transaction nonces.
119    pub(crate) nonce_manager: Arc<Mutex<NonceManager>>,
120}
121
122impl Deref for ArkivClient {
123    type Target = ArkivRoClient;
124
125    fn deref(&self) -> &Self::Target {
126        &self.ro_client
127    }
128}
129
130#[bon]
131impl ArkivClient {
132    /// Creates a new builder for `ArkivClient` with the given wallet and RPC URL.
133    /// Initializes the provider and sets up default configuration.
134    #[builder]
135    pub fn builder(wallet: PrivateKeySigner, rpc_url: Url) -> Self {
136        let provider = ProviderBuilder::new()
137            .wallet(wallet.clone())
138            .connect_http(rpc_url.clone())
139            .erased();
140
141        let ro_client = ArkivRoClient::builder()
142            .rpc_url(rpc_url)
143            .provider(provider)
144            .build();
145
146        Self {
147            ro_client,
148            wallet,
149            nonce_manager: Arc::new(Mutex::new(NonceManager {
150                base_nonce: 0,
151                in_flight: 0,
152            })),
153        }
154    }
155
156    /// Gets the underlying Reqwest client used for HTTP requests.
157    pub fn get_reqwest_client(&self) -> ClientRef<'_> {
158        self.provider.client()
159    }
160
161    /// Gets the Ethereum address of the client owner.
162    pub fn get_owner_address(&self) -> Address {
163        self.wallet.address()
164    }
165
166    /// Gets the chain ID from the provider.
167    /// Returns the chain ID as a `u64`.
168    pub async fn get_chain_id(&self) -> anyhow::Result<u64> {
169        self.provider
170            .get_chain_id()
171            .await
172            .map_err(|e| anyhow::anyhow!("Failed to get chain ID: {e}"))
173    }
174
175    /// Gets an account's ETH balance as a `BigDecimal`.
176    pub async fn get_balance(&self, account: Address) -> anyhow::Result<BigDecimal> {
177        let balance = self.provider.get_balance(account).await?;
178        Ok(wei_to_eth(balance))
179    }
180
181    /// Gets the current block number from the chain.
182    /// Returns the latest block number as a `u64`.
183    pub async fn get_current_block_number(&self) -> anyhow::Result<u64> {
184        let latest_block = self
185            .provider
186            .get_block_by_number(BlockNumberOrTag::Latest)
187            .await?
188            .ok_or_else(|| anyhow::anyhow!("Failed to get latest block"))?;
189        Ok(latest_block.header.number)
190    }
191}