near_kit/client/query.rs
1//! Query builders for fluent read operations.
2//!
3//! All query builders implement `IntoFuture` so they can be `.await`ed directly.
4
5use std::future::{Future, IntoFuture};
6use std::marker::PhantomData;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use serde::de::DeserializeOwned;
11
12use crate::error::Error;
13use crate::types::{
14 AccessKeyListView, AccountBalance, AccountId, AccountView, BlockReference, CryptoHash, Finality,
15};
16
17use super::rpc::RpcClient;
18
19// ============================================================================
20// BalanceQuery
21// ============================================================================
22
23/// Query builder for getting account balance.
24///
25/// # Example
26///
27/// ```rust,no_run
28/// # use near_kit::*;
29/// # async fn example() -> Result<(), near_kit::Error> {
30/// let near = Near::testnet().build();
31///
32/// // Simple query
33/// let balance = near.balance("alice.testnet").await?;
34///
35/// // Query at specific block
36/// let balance = near.balance("alice.testnet")
37/// .at_block(100_000_000)
38/// .await?;
39///
40/// // Query with specific finality
41/// let balance = near.balance("alice.testnet")
42/// .finality(Finality::Optimistic)
43/// .await?;
44/// # Ok(())
45/// # }
46/// ```
47pub struct BalanceQuery {
48 rpc: Arc<RpcClient>,
49 account_id: AccountId,
50 block_ref: BlockReference,
51}
52
53impl BalanceQuery {
54 pub(crate) fn new(rpc: Arc<RpcClient>, account_id: AccountId) -> Self {
55 Self {
56 rpc,
57 account_id,
58 block_ref: BlockReference::default(),
59 }
60 }
61
62 /// Query at a specific block height.
63 pub fn at_block(mut self, height: u64) -> Self {
64 self.block_ref = BlockReference::Height(height);
65 self
66 }
67
68 /// Query at a specific block hash.
69 pub fn at_block_hash(mut self, hash: CryptoHash) -> Self {
70 self.block_ref = BlockReference::Hash(hash);
71 self
72 }
73
74 /// Query with specific finality.
75 pub fn finality(mut self, finality: Finality) -> Self {
76 self.block_ref = BlockReference::Finality(finality);
77 self
78 }
79}
80
81impl IntoFuture for BalanceQuery {
82 type Output = Result<AccountBalance, Error>;
83 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
84
85 fn into_future(self) -> Self::IntoFuture {
86 Box::pin(async move {
87 let view = self
88 .rpc
89 .view_account(&self.account_id, self.block_ref)
90 .await?;
91 Ok(AccountBalance::from(view))
92 })
93 }
94}
95
96// ============================================================================
97// AccountQuery
98// ============================================================================
99
100/// Query builder for getting full account information.
101///
102/// # Example
103///
104/// ```rust,no_run
105/// # use near_kit::*;
106/// # async fn example() -> Result<(), near_kit::Error> {
107/// let near = Near::testnet().build();
108///
109/// let account = near.account("alice.testnet").await?;
110/// println!("Storage used: {} bytes", account.storage_usage);
111/// # Ok(())
112/// # }
113/// ```
114pub struct AccountQuery {
115 rpc: Arc<RpcClient>,
116 account_id: AccountId,
117 block_ref: BlockReference,
118}
119
120impl AccountQuery {
121 pub(crate) fn new(rpc: Arc<RpcClient>, account_id: AccountId) -> Self {
122 Self {
123 rpc,
124 account_id,
125 block_ref: BlockReference::default(),
126 }
127 }
128
129 /// Query at a specific block height.
130 pub fn at_block(mut self, height: u64) -> Self {
131 self.block_ref = BlockReference::Height(height);
132 self
133 }
134
135 /// Query at a specific block hash.
136 pub fn at_block_hash(mut self, hash: CryptoHash) -> Self {
137 self.block_ref = BlockReference::Hash(hash);
138 self
139 }
140
141 /// Query with specific finality.
142 pub fn finality(mut self, finality: Finality) -> Self {
143 self.block_ref = BlockReference::Finality(finality);
144 self
145 }
146}
147
148impl IntoFuture for AccountQuery {
149 type Output = Result<AccountView, Error>;
150 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
151
152 fn into_future(self) -> Self::IntoFuture {
153 Box::pin(async move {
154 let view = self
155 .rpc
156 .view_account(&self.account_id, self.block_ref)
157 .await?;
158 Ok(view)
159 })
160 }
161}
162
163// ============================================================================
164// AccountExistsQuery
165// ============================================================================
166
167/// Query builder for checking if an account exists.
168///
169/// # Example
170///
171/// ```rust,no_run
172/// # use near_kit::*;
173/// # async fn example() -> Result<(), near_kit::Error> {
174/// let near = Near::testnet().build();
175///
176/// if near.account_exists("alice.testnet").await? {
177/// println!("Account exists!");
178/// }
179/// # Ok(())
180/// # }
181/// ```
182pub struct AccountExistsQuery {
183 rpc: Arc<RpcClient>,
184 account_id: AccountId,
185 block_ref: BlockReference,
186}
187
188impl AccountExistsQuery {
189 pub(crate) fn new(rpc: Arc<RpcClient>, account_id: AccountId) -> Self {
190 Self {
191 rpc,
192 account_id,
193 block_ref: BlockReference::default(),
194 }
195 }
196
197 /// Query at a specific block height.
198 pub fn at_block(mut self, height: u64) -> Self {
199 self.block_ref = BlockReference::Height(height);
200 self
201 }
202
203 /// Query at a specific block hash.
204 pub fn at_block_hash(mut self, hash: CryptoHash) -> Self {
205 self.block_ref = BlockReference::Hash(hash);
206 self
207 }
208
209 /// Query with specific finality.
210 pub fn finality(mut self, finality: Finality) -> Self {
211 self.block_ref = BlockReference::Finality(finality);
212 self
213 }
214}
215
216impl IntoFuture for AccountExistsQuery {
217 type Output = Result<bool, Error>;
218 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
219
220 fn into_future(self) -> Self::IntoFuture {
221 Box::pin(async move {
222 match self
223 .rpc
224 .view_account(&self.account_id, self.block_ref)
225 .await
226 {
227 Ok(_) => Ok(true),
228 Err(crate::error::RpcError::AccountNotFound(_)) => Ok(false),
229 Err(e) => Err(e.into()),
230 }
231 })
232 }
233}
234
235// ============================================================================
236// AccessKeysQuery
237// ============================================================================
238
239/// Query builder for listing access keys.
240///
241/// # Example
242///
243/// ```rust,no_run
244/// # use near_kit::*;
245/// # async fn example() -> Result<(), near_kit::Error> {
246/// let near = Near::testnet().build();
247///
248/// let keys = near.access_keys("alice.testnet").await?;
249/// for key_info in keys.keys {
250/// println!("Key: {}", key_info.public_key);
251/// }
252/// # Ok(())
253/// # }
254/// ```
255pub struct AccessKeysQuery {
256 rpc: Arc<RpcClient>,
257 account_id: AccountId,
258 block_ref: BlockReference,
259}
260
261impl AccessKeysQuery {
262 pub(crate) fn new(rpc: Arc<RpcClient>, account_id: AccountId) -> Self {
263 Self {
264 rpc,
265 account_id,
266 block_ref: BlockReference::default(),
267 }
268 }
269
270 /// Query at a specific block height.
271 pub fn at_block(mut self, height: u64) -> Self {
272 self.block_ref = BlockReference::Height(height);
273 self
274 }
275
276 /// Query at a specific block hash.
277 pub fn at_block_hash(mut self, hash: CryptoHash) -> Self {
278 self.block_ref = BlockReference::Hash(hash);
279 self
280 }
281
282 /// Query with specific finality.
283 pub fn finality(mut self, finality: Finality) -> Self {
284 self.block_ref = BlockReference::Finality(finality);
285 self
286 }
287}
288
289impl IntoFuture for AccessKeysQuery {
290 type Output = Result<AccessKeyListView, Error>;
291 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
292
293 fn into_future(self) -> Self::IntoFuture {
294 Box::pin(async move {
295 let list = self
296 .rpc
297 .view_access_key_list(&self.account_id, self.block_ref)
298 .await?;
299 Ok(list)
300 })
301 }
302}
303
304// ============================================================================
305// ViewCall
306// ============================================================================
307
308/// Query builder for calling view functions on contracts.
309///
310/// # Example
311///
312/// ```rust,no_run
313/// # use near_kit::*;
314/// # async fn example() -> Result<(), near_kit::Error> {
315/// let near = Near::testnet().build();
316///
317/// // Simple view call without args
318/// let count: u64 = near.view("counter.testnet", "get_count").await?;
319///
320/// // View call with args
321/// let messages: Vec<String> = near.view("guestbook.testnet", "get_messages")
322/// .args(serde_json::json!({ "limit": 10 }))
323/// .await?;
324///
325/// // Query at specific block
326/// let old_count: u64 = near.view("counter.testnet", "get_count")
327/// .at_block(100_000_000)
328/// .await?;
329/// # Ok(())
330/// # }
331/// ```
332pub struct ViewCall<T> {
333 rpc: Arc<RpcClient>,
334 contract_id: AccountId,
335 method: String,
336 args: Vec<u8>,
337 block_ref: BlockReference,
338 _phantom: PhantomData<T>,
339}
340
341impl<T> ViewCall<T> {
342 pub(crate) fn new(rpc: Arc<RpcClient>, contract_id: AccountId, method: String) -> Self {
343 Self {
344 rpc,
345 contract_id,
346 method,
347 args: vec![],
348 block_ref: BlockReference::default(),
349 _phantom: PhantomData,
350 }
351 }
352
353 /// Set JSON arguments for the view call.
354 ///
355 /// The arguments will be serialized to JSON.
356 pub fn args<A: serde::Serialize>(mut self, args: A) -> Self {
357 self.args = serde_json::to_vec(&args).unwrap_or_default();
358 self
359 }
360
361 /// Set raw byte arguments (e.g., Borsh encoded).
362 pub fn args_raw(mut self, args: Vec<u8>) -> Self {
363 self.args = args;
364 self
365 }
366
367 /// Set Borsh-encoded arguments.
368 pub fn args_borsh<A: borsh::BorshSerialize>(mut self, args: A) -> Self {
369 self.args = borsh::to_vec(&args).unwrap_or_default();
370 self
371 }
372
373 /// Query at a specific block height.
374 pub fn at_block(mut self, height: u64) -> Self {
375 self.block_ref = BlockReference::Height(height);
376 self
377 }
378
379 /// Query at a specific block hash.
380 pub fn at_block_hash(mut self, hash: CryptoHash) -> Self {
381 self.block_ref = BlockReference::Hash(hash);
382 self
383 }
384
385 /// Query with specific finality.
386 pub fn finality(mut self, finality: Finality) -> Self {
387 self.block_ref = BlockReference::Finality(finality);
388 self
389 }
390
391 /// Switch to Borsh deserialization for the response.
392 ///
393 /// By default, `ViewCall` deserializes responses as JSON. Call this method
394 /// to deserialize as Borsh instead. This is useful for contracts that return
395 /// Borsh-encoded data.
396 ///
397 /// # Example
398 ///
399 /// ```rust,no_run
400 /// use near_kit::*;
401 /// use borsh::BorshDeserialize;
402 ///
403 /// #[derive(BorshDeserialize)]
404 /// struct ContractState { count: u64 }
405 ///
406 /// async fn example() -> Result<(), near_kit::Error> {
407 /// let near = Near::testnet().build();
408 ///
409 /// // Borsh response deserialization
410 /// let state: ContractState = near.view("contract.testnet", "get_state")
411 /// .borsh()
412 /// .await?;
413 /// Ok(())
414 /// }
415 /// ```
416 pub fn borsh(self) -> ViewCallBorsh<T> {
417 ViewCallBorsh {
418 rpc: self.rpc,
419 contract_id: self.contract_id,
420 method: self.method,
421 args: self.args,
422 block_ref: self.block_ref,
423 _phantom: PhantomData,
424 }
425 }
426}
427
428impl<T: DeserializeOwned + Send + 'static> IntoFuture for ViewCall<T> {
429 type Output = Result<T, Error>;
430 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
431
432 fn into_future(self) -> Self::IntoFuture {
433 Box::pin(async move {
434 let result = self
435 .rpc
436 .view_function(&self.contract_id, &self.method, &self.args, self.block_ref)
437 .await?;
438 Ok(result.json()?)
439 })
440 }
441}
442
443// ============================================================================
444// ViewCallBorsh
445// ============================================================================
446
447/// Query builder for view functions with Borsh deserialization.
448///
449/// Created by calling [`.borsh()`](ViewCall::borsh) on a `ViewCall`.
450/// This variant deserializes the response as Borsh instead of JSON.
451///
452/// # Example
453///
454/// ```rust,no_run
455/// use near_kit::*;
456/// use borsh::BorshDeserialize;
457///
458/// #[derive(BorshDeserialize)]
459/// struct ContractState { count: u64 }
460///
461/// #[derive(borsh::BorshSerialize)]
462/// struct MyArgs { key: u64 }
463///
464/// async fn example() -> Result<(), near_kit::Error> {
465/// let near = Near::testnet().build();
466///
467/// // JSON args, Borsh response
468/// let state: ContractState = near.view("contract.testnet", "get_state")
469/// .args(serde_json::json!({ "key": "value" }))
470/// .borsh()
471/// .await?;
472///
473/// // Borsh args, Borsh response
474/// let state: ContractState = near.view("contract.testnet", "get_state")
475/// .args_borsh(MyArgs { key: 123 })
476/// .borsh()
477/// .await?;
478/// Ok(())
479/// }
480/// ```
481pub struct ViewCallBorsh<T> {
482 rpc: Arc<RpcClient>,
483 contract_id: AccountId,
484 method: String,
485 args: Vec<u8>,
486 block_ref: BlockReference,
487 _phantom: PhantomData<T>,
488}
489
490impl<T: borsh::BorshDeserialize + Send + 'static> IntoFuture for ViewCallBorsh<T> {
491 type Output = Result<T, Error>;
492 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
493
494 fn into_future(self) -> Self::IntoFuture {
495 Box::pin(async move {
496 let result = self
497 .rpc
498 .view_function(&self.contract_id, &self.method, &self.args, self.block_ref)
499 .await?;
500 result.borsh().map_err(|e| Error::Borsh(e.to_string()))
501 })
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn test_balance_query_builder() {
511 let rpc = Arc::new(RpcClient::new("http://localhost:3030"));
512 let account_id: AccountId = "alice.testnet".parse().unwrap();
513
514 let query = BalanceQuery::new(rpc.clone(), account_id.clone());
515 assert_eq!(query.block_ref, BlockReference::default());
516
517 let query = BalanceQuery::new(rpc.clone(), account_id.clone()).at_block(12345);
518 assert_eq!(query.block_ref, BlockReference::Height(12345));
519
520 let query = BalanceQuery::new(rpc.clone(), account_id).finality(Finality::Optimistic);
521 assert_eq!(
522 query.block_ref,
523 BlockReference::Finality(Finality::Optimistic)
524 );
525 }
526}