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}