Skip to main content

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}