Skip to main content

near_kit/client/
near.rs

1//! The main Near client.
2
3use std::sync::Arc;
4
5use serde::de::DeserializeOwned;
6
7use crate::contract::ContractClient;
8use crate::error::Error;
9use crate::types::{AccountId, Gas, IntoNearToken, NearToken, Network, PublicKey, SecretKey};
10
11use super::query::{AccessKeysQuery, AccountExistsQuery, AccountQuery, BalanceQuery, ViewCall};
12use super::rpc::{MAINNET, RetryConfig, RpcClient, TESTNET};
13use super::signer::{InMemorySigner, Signer};
14use super::transaction::{CallBuilder, TransactionBuilder};
15use crate::types::TxExecutionStatus;
16
17/// Trait for sandbox network configuration.
18///
19/// Implement this trait for your sandbox type to enable ergonomic
20/// integration with the `Near` client via [`Near::sandbox()`].
21///
22/// # Example
23///
24/// ```rust,ignore
25/// use near_sandbox::Sandbox;
26///
27/// let sandbox = Sandbox::start_sandbox().await?;
28/// let near = Near::sandbox(&sandbox).build();
29///
30/// // The root account credentials are automatically configured
31/// near.transfer("alice.sandbox", "10 NEAR").await?;
32/// ```
33pub trait SandboxNetwork {
34    /// The RPC URL for the sandbox (e.g., `http://127.0.0.1:3030`).
35    fn rpc_url(&self) -> &str;
36
37    /// The root account ID (e.g., `"sandbox"`).
38    fn root_account_id(&self) -> &str;
39
40    /// The root account's secret key.
41    fn root_secret_key(&self) -> &str;
42}
43
44/// The main client for interacting with NEAR Protocol.
45///
46/// The `Near` client is the single entry point for all NEAR operations.
47/// It can be configured with a signer for write operations, or used
48/// without a signer for read-only operations.
49///
50/// Transport (RPC connection) and signing are separate concerns — the client
51/// holds a shared `Arc<RpcClient>` and an optional signer. Use [`with_signer`](Near::with_signer)
52/// to derive new clients that share the same connection but sign as different accounts.
53///
54/// # Example
55///
56/// ```rust,no_run
57/// use near_kit::*;
58///
59/// #[tokio::main]
60/// async fn main() -> Result<(), near_kit::Error> {
61///     // Read-only client (no signer)
62///     let near = Near::testnet().build();
63///     let balance = near.balance("alice.testnet").await?;
64///     println!("Balance: {}", balance);
65///
66///     // Client with signer for transactions
67///     let near = Near::testnet()
68///         .credentials("ed25519:...", "alice.testnet")?
69///         .build();
70///     near.transfer("bob.testnet", "1 NEAR").await?;
71///
72///     Ok(())
73/// }
74/// ```
75///
76/// # Multiple Accounts
77///
78/// For production apps that manage multiple accounts, set up the connection once
79/// and derive signing contexts with [`with_signer`](Near::with_signer):
80///
81/// ```rust,no_run
82/// # use near_kit::*;
83/// # fn example() -> Result<(), Error> {
84/// let near = Near::testnet().build();
85///
86/// let alice = near.with_signer(InMemorySigner::new("alice.testnet", "ed25519:...")?);
87/// let bob = near.with_signer(InMemorySigner::new("bob.testnet", "ed25519:...")?);
88///
89/// // Both share the same RPC connection, sign as different accounts
90/// # Ok(())
91/// # }
92/// ```
93#[derive(Clone)]
94pub struct Near {
95    rpc: Arc<RpcClient>,
96    signer: Option<Arc<dyn Signer>>,
97    network: Network,
98    max_nonce_retries: u32,
99}
100
101impl Near {
102    /// Create a builder for mainnet.
103    pub fn mainnet() -> NearBuilder {
104        NearBuilder::new(MAINNET.rpc_url, Network::Mainnet)
105    }
106
107    /// Create a builder for testnet.
108    pub fn testnet() -> NearBuilder {
109        NearBuilder::new(TESTNET.rpc_url, Network::Testnet)
110    }
111
112    /// Create a builder with a custom RPC URL.
113    pub fn custom(rpc_url: impl Into<String>) -> NearBuilder {
114        NearBuilder::new(rpc_url, Network::Custom)
115    }
116
117    /// Create a configured client from environment variables.
118    ///
119    /// Reads the following environment variables:
120    /// - `NEAR_NETWORK` (optional): `"mainnet"`, `"testnet"`, or a custom RPC URL.
121    ///   Defaults to `"testnet"` if not set.
122    /// - `NEAR_ACCOUNT_ID` (optional): Account ID for signing transactions.
123    /// - `NEAR_PRIVATE_KEY` (optional): Private key for signing (e.g., `"ed25519:..."`).
124    ///
125    /// If `NEAR_ACCOUNT_ID` and `NEAR_PRIVATE_KEY` are both set, the client will
126    /// be configured with signing capability. Otherwise, it will be read-only.
127    ///
128    /// # Example
129    ///
130    /// ```bash
131    /// # Environment variables
132    /// export NEAR_NETWORK=testnet
133    /// export NEAR_ACCOUNT_ID=alice.testnet
134    /// export NEAR_PRIVATE_KEY=ed25519:...
135    /// ```
136    ///
137    /// ```rust,no_run
138    /// # use near_kit::*;
139    /// # async fn example() -> Result<(), near_kit::Error> {
140    /// // Auto-configures from environment
141    /// let near = Near::from_env()?;
142    ///
143    /// // If credentials are set, transactions work
144    /// near.transfer("bob.testnet", "1 NEAR").await?;
145    /// # Ok(())
146    /// # }
147    /// ```
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if:
152    /// - `NEAR_ACCOUNT_ID` is set without `NEAR_PRIVATE_KEY` (or vice versa)
153    /// - `NEAR_PRIVATE_KEY` contains an invalid key format
154    pub fn from_env() -> Result<Near, Error> {
155        let network = std::env::var("NEAR_NETWORK").ok();
156        let account_id = std::env::var("NEAR_ACCOUNT_ID").ok();
157        let private_key = std::env::var("NEAR_PRIVATE_KEY").ok();
158
159        // Determine builder based on network
160        let mut builder = match network.as_deref() {
161            Some("mainnet") => Near::mainnet(),
162            Some("testnet") | None => Near::testnet(),
163            Some(url) => Near::custom(url),
164        };
165
166        // Configure signer if both account and key are provided
167        match (account_id, private_key) {
168            (Some(account), Some(key)) => {
169                builder = builder.credentials(&key, &account)?;
170            }
171            (Some(_), None) => {
172                return Err(Error::Config(
173                    "NEAR_ACCOUNT_ID is set but NEAR_PRIVATE_KEY is missing".into(),
174                ));
175            }
176            (None, Some(_)) => {
177                return Err(Error::Config(
178                    "NEAR_PRIVATE_KEY is set but NEAR_ACCOUNT_ID is missing".into(),
179                ));
180            }
181            (None, None) => {
182                // Read-only client, no credentials
183            }
184        }
185
186        Ok(builder.build())
187    }
188
189    /// Create a builder configured for a sandbox network.
190    ///
191    /// This automatically configures the client with the sandbox's RPC URL
192    /// and root account credentials, making it ready for transactions.
193    ///
194    /// # Example
195    ///
196    /// ```rust,ignore
197    /// use near_sandbox::Sandbox;
198    /// use near_kit::*;
199    ///
200    /// let sandbox = Sandbox::start_sandbox().await?;
201    /// let near = Near::sandbox(&sandbox);
202    ///
203    /// // Root account credentials are auto-configured - ready for transactions!
204    /// near.transfer("alice.sandbox", "10 NEAR").await?;
205    /// ```
206    pub fn sandbox(network: &impl SandboxNetwork) -> Near {
207        let secret_key: SecretKey = network
208            .root_secret_key()
209            .parse()
210            .expect("sandbox should provide valid secret key");
211        let account_id: AccountId = network
212            .root_account_id()
213            .parse()
214            .expect("sandbox should provide valid account id");
215
216        let signer = InMemorySigner::from_secret_key(account_id, secret_key);
217
218        Near {
219            rpc: Arc::new(RpcClient::new(network.rpc_url())),
220            signer: Some(Arc::new(signer)),
221            network: Network::Sandbox,
222            max_nonce_retries: 3,
223        }
224    }
225
226    /// Get the underlying RPC client.
227    pub fn rpc(&self) -> &RpcClient {
228        &self.rpc
229    }
230
231    /// Get the RPC URL.
232    pub fn rpc_url(&self) -> &str {
233        self.rpc.url()
234    }
235
236    /// Get the signer's account ID, if a signer is configured.
237    pub fn account_id(&self) -> Option<&AccountId> {
238        self.signer.as_ref().map(|s| s.account_id())
239    }
240
241    /// Get the network this client is connected to.
242    pub fn network(&self) -> Network {
243        self.network
244    }
245
246    /// Create a new client that shares this client's transport but uses a different signer.
247    ///
248    /// This is the recommended way to manage multiple accounts. The RPC connection
249    /// is shared (via `Arc`), so there's no overhead from creating multiple clients.
250    ///
251    /// # Example
252    ///
253    /// ```rust,no_run
254    /// # use near_kit::*;
255    /// # fn example() -> Result<(), Error> {
256    /// // Set up a shared connection
257    /// let near = Near::testnet().build();
258    ///
259    /// // Derive signing contexts for different accounts
260    /// let alice = near.with_signer(InMemorySigner::new("alice.testnet", "ed25519:...")?);
261    /// let bob = near.with_signer(InMemorySigner::new("bob.testnet", "ed25519:...")?);
262    ///
263    /// // Both share the same RPC connection
264    /// // alice.transfer("carol.testnet", NearToken::near(1)).await?;
265    /// // bob.transfer("carol.testnet", NearToken::near(2)).await?;
266    /// # Ok(())
267    /// # }
268    /// ```
269    pub fn with_signer(&self, signer: impl Signer + 'static) -> Near {
270        Near {
271            rpc: self.rpc.clone(),
272            signer: Some(Arc::new(signer)),
273            network: self.network,
274            max_nonce_retries: self.max_nonce_retries,
275        }
276    }
277
278    // ========================================================================
279    // Read Operations (Query Builders)
280    // ========================================================================
281
282    /// Get account balance.
283    ///
284    /// Returns a query builder that can be customized with block reference
285    /// options before awaiting.
286    ///
287    /// # Example
288    ///
289    /// ```rust,no_run
290    /// # use near_kit::*;
291    /// # async fn example() -> Result<(), near_kit::Error> {
292    /// let near = Near::testnet().build();
293    ///
294    /// // Simple query
295    /// let balance = near.balance("alice.testnet").await?;
296    /// println!("Available: {}", balance.available);
297    ///
298    /// // Query at specific block height
299    /// let balance = near.balance("alice.testnet")
300    ///     .at_block(100_000_000)
301    ///     .await?;
302    ///
303    /// // Query with specific finality
304    /// let balance = near.balance("alice.testnet")
305    ///     .finality(Finality::Optimistic)
306    ///     .await?;
307    /// # Ok(())
308    /// # }
309    /// ```
310    pub fn balance(&self, account_id: impl AsRef<str>) -> BalanceQuery {
311        let account_id = AccountId::parse_lenient(account_id);
312        BalanceQuery::new(self.rpc.clone(), account_id)
313    }
314
315    /// Get full account information.
316    ///
317    /// # Example
318    ///
319    /// ```rust,no_run
320    /// # use near_kit::*;
321    /// # async fn example() -> Result<(), near_kit::Error> {
322    /// let near = Near::testnet().build();
323    /// let account = near.account("alice.testnet").await?;
324    /// println!("Storage used: {} bytes", account.storage_usage);
325    /// # Ok(())
326    /// # }
327    /// ```
328    pub fn account(&self, account_id: impl AsRef<str>) -> AccountQuery {
329        let account_id = AccountId::parse_lenient(account_id);
330        AccountQuery::new(self.rpc.clone(), account_id)
331    }
332
333    /// Check if an account exists.
334    ///
335    /// # Example
336    ///
337    /// ```rust,no_run
338    /// # use near_kit::*;
339    /// # async fn example() -> Result<(), near_kit::Error> {
340    /// let near = Near::testnet().build();
341    /// if near.account_exists("alice.testnet").await? {
342    ///     println!("Account exists!");
343    /// }
344    /// # Ok(())
345    /// # }
346    /// ```
347    pub fn account_exists(&self, account_id: impl AsRef<str>) -> AccountExistsQuery {
348        let account_id = AccountId::parse_lenient(account_id);
349        AccountExistsQuery::new(self.rpc.clone(), account_id)
350    }
351
352    /// Call a view function on a contract.
353    ///
354    /// Returns a query builder that can be customized with arguments
355    /// and block reference options before awaiting.
356    ///
357    /// # Example
358    ///
359    /// ```rust,no_run
360    /// # use near_kit::*;
361    /// # async fn example() -> Result<(), near_kit::Error> {
362    /// let near = Near::testnet().build();
363    ///
364    /// // Simple view call
365    /// let count: u64 = near.view("counter.testnet", "get_count").await?;
366    ///
367    /// // View call with arguments
368    /// let messages: Vec<String> = near.view("guestbook.testnet", "get_messages")
369    ///     .args(serde_json::json!({ "limit": 10 }))
370    ///     .await?;
371    /// # Ok(())
372    /// # }
373    /// ```
374    pub fn view<T>(&self, contract_id: impl AsRef<str>, method: &str) -> ViewCall<T> {
375        let contract_id = AccountId::parse_lenient(contract_id);
376        ViewCall::new(self.rpc.clone(), contract_id, method.to_string())
377    }
378
379    /// Get all access keys for an account.
380    ///
381    /// # Example
382    ///
383    /// ```rust,no_run
384    /// # use near_kit::*;
385    /// # async fn example() -> Result<(), near_kit::Error> {
386    /// let near = Near::testnet().build();
387    /// let keys = near.access_keys("alice.testnet").await?;
388    /// for key_info in keys.keys {
389    ///     println!("Key: {}", key_info.public_key);
390    /// }
391    /// # Ok(())
392    /// # }
393    /// ```
394    pub fn access_keys(&self, account_id: impl AsRef<str>) -> AccessKeysQuery {
395        let account_id = AccountId::parse_lenient(account_id);
396        AccessKeysQuery::new(self.rpc.clone(), account_id)
397    }
398
399    // ========================================================================
400    // Off-Chain Signing (NEP-413)
401    // ========================================================================
402
403    /// Sign a message for off-chain authentication (NEP-413).
404    ///
405    /// This enables users to prove account ownership without gas fees
406    /// or blockchain transactions. Commonly used for:
407    /// - Web3 authentication/login
408    /// - Off-chain message signing
409    /// - Proof of account ownership
410    ///
411    /// # Example
412    ///
413    /// ```rust,no_run
414    /// # use near_kit::*;
415    /// # async fn example() -> Result<(), near_kit::Error> {
416    /// let near = Near::testnet()
417    ///     .credentials("ed25519:...", "alice.testnet")?
418    ///     .build();
419    ///
420    /// let signed = near.sign_message(nep413::SignMessageParams {
421    ///     message: "Login to MyApp".to_string(),
422    ///     recipient: "myapp.com".to_string(),
423    ///     nonce: nep413::generate_nonce(),
424    ///     callback_url: None,
425    ///     state: None,
426    /// }).await?;
427    ///
428    /// println!("Signed by: {}", signed.account_id);
429    /// # Ok(())
430    /// # }
431    /// ```
432    ///
433    /// @see <https://github.com/near/NEPs/blob/master/neps/nep-0413.md>
434    pub async fn sign_message(
435        &self,
436        params: crate::types::nep413::SignMessageParams,
437    ) -> Result<crate::types::nep413::SignedMessage, Error> {
438        let signer = self.signer.as_ref().ok_or(Error::NoSigner)?;
439        let key = signer.key();
440        key.sign_nep413(signer.account_id(), &params)
441            .await
442            .map_err(Error::Signing)
443    }
444
445    // ========================================================================
446    // Write Operations (Transaction Builders)
447    // ========================================================================
448
449    /// Transfer NEAR tokens.
450    ///
451    /// Returns a transaction builder that can be customized with
452    /// wait options before awaiting.
453    ///
454    /// # Example
455    ///
456    /// ```rust,no_run
457    /// # use near_kit::*;
458    /// # async fn example() -> Result<(), near_kit::Error> {
459    /// let near = Near::testnet()
460    ///         .credentials("ed25519:...", "alice.testnet")?
461    ///     .build();
462    ///
463    /// // Preferred: typed constructor
464    /// near.transfer("bob.testnet", NearToken::near(1)).await?;
465    ///
466    /// // Transfer with wait for finality
467    /// near.transfer("bob.testnet", NearToken::near(1000))
468    ///     .wait_until(TxExecutionStatus::Final)
469    ///     .await?;
470    /// # Ok(())
471    /// # }
472    /// ```
473    pub fn transfer(
474        &self,
475        receiver: impl AsRef<str>,
476        amount: impl IntoNearToken,
477    ) -> TransactionBuilder {
478        self.transaction(receiver).transfer(amount)
479    }
480
481    /// Call a function on a contract.
482    ///
483    /// Returns a transaction builder that can be customized with
484    /// arguments, gas, deposit, and other options before awaiting.
485    ///
486    /// # Example
487    ///
488    /// ```rust,no_run
489    /// # use near_kit::*;
490    /// # async fn example() -> Result<(), near_kit::Error> {
491    /// let near = Near::testnet()
492    ///         .credentials("ed25519:...", "alice.testnet")?
493    ///     .build();
494    ///
495    /// // Simple call
496    /// near.call("counter.testnet", "increment").await?;
497    ///
498    /// // Call with args, gas, and deposit
499    /// near.call("nft.testnet", "nft_mint")
500    ///     .args(serde_json::json!({ "token_id": "1" }))
501    ///     .gas("100 Tgas")
502    ///     .deposit("0.1 NEAR")
503    ///     .await?;
504    /// # Ok(())
505    /// # }
506    /// ```
507    pub fn call(&self, contract_id: impl AsRef<str>, method: &str) -> CallBuilder {
508        self.transaction(contract_id).call(method)
509    }
510
511    /// Deploy a contract.
512    ///
513    /// # Example
514    ///
515    /// ```rust,no_run
516    /// # use near_kit::*;
517    /// # async fn example() -> Result<(), near_kit::Error> {
518    /// let near = Near::testnet()
519    ///         .credentials("ed25519:...", "alice.testnet")?
520    ///     .build();
521    ///
522    /// let wasm_code = std::fs::read("contract.wasm").unwrap();
523    /// near.deploy("alice.testnet", wasm_code).await?;
524    /// # Ok(())
525    /// # }
526    /// ```
527    pub fn deploy(
528        &self,
529        account_id: impl AsRef<str>,
530        code: impl Into<Vec<u8>>,
531    ) -> TransactionBuilder {
532        self.transaction(account_id).deploy(code)
533    }
534
535    /// Add a full access key to an account.
536    pub fn add_full_access_key(
537        &self,
538        account_id: impl AsRef<str>,
539        public_key: PublicKey,
540    ) -> TransactionBuilder {
541        self.transaction(account_id).add_full_access_key(public_key)
542    }
543
544    /// Delete an access key from an account.
545    pub fn delete_key(
546        &self,
547        account_id: impl AsRef<str>,
548        public_key: PublicKey,
549    ) -> TransactionBuilder {
550        self.transaction(account_id).delete_key(public_key)
551    }
552
553    // ========================================================================
554    // Multi-Action Transactions
555    // ========================================================================
556
557    /// Create a transaction builder for multi-action transactions.
558    ///
559    /// This allows chaining multiple actions (transfers, function calls, account creation, etc.)
560    /// into a single atomic transaction. All actions either succeed together or fail together.
561    ///
562    /// # Example
563    ///
564    /// ```rust,no_run
565    /// # use near_kit::*;
566    /// # async fn example() -> Result<(), near_kit::Error> {
567    /// let near = Near::testnet()
568    ///     .credentials("ed25519:...", "alice.testnet")?
569    ///     .build();
570    ///
571    /// // Create a new sub-account with funding and a key
572    /// let new_public_key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse()?;
573    /// near.transaction("new.alice.testnet")
574    ///     .create_account()
575    ///     .transfer("5 NEAR")
576    ///     .add_full_access_key(new_public_key)
577    ///     .send()
578    ///     .await?;
579    ///
580    /// // Multiple function calls in one transaction
581    /// near.transaction("contract.testnet")
582    ///     .call("method1")
583    ///         .args(serde_json::json!({ "value": 1 }))
584    ///     .call("method2")
585    ///         .args(serde_json::json!({ "value": 2 }))
586    ///     .send()
587    ///     .await?;
588    /// # Ok(())
589    /// # }
590    /// ```
591    pub fn transaction(&self, receiver_id: impl AsRef<str>) -> TransactionBuilder {
592        let receiver_id = AccountId::parse_lenient(receiver_id);
593        TransactionBuilder::new(
594            self.rpc.clone(),
595            self.signer.clone(),
596            receiver_id,
597            self.max_nonce_retries,
598        )
599    }
600
601    /// Send a pre-signed transaction.
602    ///
603    /// Use this with transactions signed via `.sign()` for offline signing
604    /// or inspection before sending.
605    ///
606    /// # Example
607    ///
608    /// ```rust,no_run
609    /// # use near_kit::*;
610    /// # async fn example() -> Result<(), near_kit::Error> {
611    /// let near = Near::testnet()
612    ///     .credentials("ed25519:...", "alice.testnet")?
613    ///     .build();
614    ///
615    /// // Sign offline
616    /// let signed = near.transfer("bob.testnet", NearToken::near(1))
617    ///     .sign()
618    ///     .await?;
619    ///
620    /// // Send later
621    /// let outcome = near.send(&signed).await?;
622    /// # Ok(())
623    /// # }
624    /// ```
625    pub async fn send(
626        &self,
627        signed_tx: &crate::types::SignedTransaction,
628    ) -> Result<crate::types::FinalExecutionOutcome, Error> {
629        self.send_with_options(signed_tx, TxExecutionStatus::ExecutedOptimistic)
630            .await
631    }
632
633    /// Send a pre-signed transaction with custom wait options.
634    pub async fn send_with_options(
635        &self,
636        signed_tx: &crate::types::SignedTransaction,
637        wait_until: TxExecutionStatus,
638    ) -> Result<crate::types::FinalExecutionOutcome, Error> {
639        let response = self.rpc.send_tx(signed_tx, wait_until).await?;
640        let outcome = response.outcome.ok_or_else(|| {
641            Error::InvalidTransaction(format!(
642                "Transaction {} submitted with wait_until={:?} but no execution outcome \
643                 was returned. Use rpc().send_tx() for fire-and-forget submission.",
644                response.transaction_hash, wait_until,
645            ))
646        })?;
647
648        if outcome.is_failure() {
649            return Err(Error::TransactionFailed(
650                outcome.failure_message().unwrap_or_default(),
651            ));
652        }
653
654        Ok(outcome)
655    }
656
657    // ========================================================================
658    // Convenience methods
659    // ========================================================================
660
661    /// Call a view function with arguments (convenience method).
662    pub async fn view_with_args<T: DeserializeOwned + Send + 'static, A: serde::Serialize>(
663        &self,
664        contract_id: impl AsRef<str>,
665        method: &str,
666        args: &A,
667    ) -> Result<T, Error> {
668        let contract_id = AccountId::parse_lenient(contract_id);
669        ViewCall::new(self.rpc.clone(), contract_id, method.to_string())
670            .args(args)
671            .await
672    }
673
674    /// Call a function with arguments (convenience method).
675    pub async fn call_with_args<A: serde::Serialize>(
676        &self,
677        contract_id: impl AsRef<str>,
678        method: &str,
679        args: &A,
680    ) -> Result<crate::types::FinalExecutionOutcome, Error> {
681        self.call(contract_id, method).args(args).await
682    }
683
684    /// Call a function with full options (convenience method).
685    pub async fn call_with_options<A: serde::Serialize>(
686        &self,
687        contract_id: impl AsRef<str>,
688        method: &str,
689        args: &A,
690        gas: Gas,
691        deposit: NearToken,
692    ) -> Result<crate::types::FinalExecutionOutcome, Error> {
693        self.call(contract_id, method)
694            .args(args)
695            .gas(gas)
696            .deposit(deposit)
697            .await
698    }
699
700    // ========================================================================
701    // Typed Contract Interfaces
702    // ========================================================================
703
704    /// Create a typed contract client.
705    ///
706    /// This method creates a type-safe client for interacting with a contract,
707    /// using the interface defined via the `#[near_kit::contract]` macro.
708    ///
709    /// # Example
710    ///
711    /// ```ignore
712    /// use near_kit::*;
713    /// use serde::Serialize;
714    ///
715    /// #[near_kit::contract]
716    /// pub trait Counter {
717    ///     fn get_count(&self) -> u64;
718    ///     
719    ///     #[call]
720    ///     fn increment(&mut self);
721    ///     
722    ///     #[call]
723    ///     fn add(&mut self, args: AddArgs);
724    /// }
725    ///
726    /// #[derive(Serialize)]
727    /// pub struct AddArgs {
728    ///     pub value: u64,
729    /// }
730    ///
731    /// async fn example(near: &Near) -> Result<(), near_kit::Error> {
732    ///     let counter = near.contract::<Counter>("counter.testnet");
733    ///     
734    ///     // View call - type-safe!
735    ///     let count = counter.get_count().await?;
736    ///     
737    ///     // Change call - type-safe!
738    ///     counter.increment().await?;
739    ///     counter.add(AddArgs { value: 5 }).await?;
740    ///     
741    ///     Ok(())
742    /// }
743    /// ```
744    pub fn contract<T: crate::Contract + ?Sized>(
745        &self,
746        contract_id: impl AsRef<str>,
747    ) -> T::Client<'_> {
748        let contract_id = AccountId::parse_lenient(contract_id);
749        T::Client::new(self, contract_id)
750    }
751
752    // ========================================================================
753    // Token Helpers
754    // ========================================================================
755
756    /// Get a fungible token client for a NEP-141 contract.
757    ///
758    /// Accepts either a string/`AccountId` for raw addresses, or a [`KnownToken`]
759    /// constant (like [`tokens::USDC`]) which auto-resolves based on the network.
760    ///
761    /// [`KnownToken`]: crate::tokens::KnownToken
762    /// [`tokens::USDC`]: crate::tokens::USDC
763    ///
764    /// # Example
765    ///
766    /// ```rust,no_run
767    /// # use near_kit::*;
768    /// # async fn example() -> Result<(), near_kit::Error> {
769    /// let near = Near::mainnet().build();
770    ///
771    /// // Use a known token - auto-resolves based on network
772    /// let usdc = near.ft(tokens::USDC)?;
773    ///
774    /// // Or use a raw address
775    /// let custom = near.ft("custom-token.near")?;
776    ///
777    /// // Get metadata
778    /// let meta = usdc.metadata().await?;
779    /// println!("{} ({})", meta.name, meta.symbol);
780    ///
781    /// // Get balance - returns FtAmount for nice formatting
782    /// let balance = usdc.balance_of("alice.near").await?;
783    /// println!("Balance: {}", balance);  // e.g., "1.5 USDC"
784    /// # Ok(())
785    /// # }
786    /// ```
787    pub fn ft(
788        &self,
789        contract: impl crate::tokens::IntoContractId,
790    ) -> Result<crate::tokens::FungibleToken, Error> {
791        let contract_id = contract.into_contract_id(self.network)?;
792        Ok(crate::tokens::FungibleToken::new(
793            self.rpc.clone(),
794            self.signer.clone(),
795            contract_id,
796            self.max_nonce_retries,
797        ))
798    }
799
800    /// Get a non-fungible token client for a NEP-171 contract.
801    ///
802    /// Accepts either a string/`AccountId` for raw addresses, or a contract
803    /// identifier that implements [`IntoContractId`].
804    ///
805    /// [`IntoContractId`]: crate::tokens::IntoContractId
806    ///
807    /// # Example
808    ///
809    /// ```rust,no_run
810    /// # use near_kit::*;
811    /// # async fn example() -> Result<(), near_kit::Error> {
812    /// let near = Near::testnet().build();
813    /// let nft = near.nft("nft-contract.near")?;
814    ///
815    /// // Get a specific token
816    /// if let Some(token) = nft.token("token-123").await? {
817    ///     println!("Owner: {}", token.owner_id);
818    /// }
819    ///
820    /// // List tokens for an owner
821    /// let tokens = nft.tokens_for_owner("alice.near", None, Some(10)).await?;
822    /// # Ok(())
823    /// # }
824    /// ```
825    pub fn nft(
826        &self,
827        contract: impl crate::tokens::IntoContractId,
828    ) -> Result<crate::tokens::NonFungibleToken, Error> {
829        let contract_id = contract.into_contract_id(self.network)?;
830        Ok(crate::tokens::NonFungibleToken::new(
831            self.rpc.clone(),
832            self.signer.clone(),
833            contract_id,
834            self.max_nonce_retries,
835        ))
836    }
837}
838
839impl std::fmt::Debug for Near {
840    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
841        f.debug_struct("Near")
842            .field("rpc", &self.rpc)
843            .field("account_id", &self.account_id())
844            .finish()
845    }
846}
847
848/// Builder for creating a [`Near`] client.
849///
850/// # Example
851///
852/// ```rust,ignore
853/// use near_kit::*;
854///
855/// // Read-only client
856/// let near = Near::testnet().build();
857///
858/// // Client with credentials (secret key + account)
859/// let near = Near::testnet()
860///     .credentials("ed25519:...", "alice.testnet")?
861///     .build();
862///
863/// // Client with keystore
864/// let keystore = std::sync::Arc::new(InMemoryKeyStore::new());
865/// // ... add keys to keystore ...
866/// let near = Near::testnet()
867///     .keystore(keystore, "alice.testnet")?
868///     .build();
869/// ```
870pub struct NearBuilder {
871    rpc_url: String,
872    signer: Option<Arc<dyn Signer>>,
873    retry_config: RetryConfig,
874    network: Network,
875    max_nonce_retries: u32,
876}
877
878impl NearBuilder {
879    /// Create a new builder with the given RPC URL.
880    fn new(rpc_url: impl Into<String>, network: Network) -> Self {
881        Self {
882            rpc_url: rpc_url.into(),
883            signer: None,
884            retry_config: RetryConfig::default(),
885            network,
886            max_nonce_retries: 3,
887        }
888    }
889
890    /// Set the signer for transactions.
891    ///
892    /// The signer determines which account will sign transactions.
893    pub fn signer(mut self, signer: impl Signer + 'static) -> Self {
894        self.signer = Some(Arc::new(signer));
895        self
896    }
897
898    /// Set up signing using a private key string and account ID.
899    ///
900    /// This is a convenience method that creates an `InMemorySigner` for you.
901    ///
902    /// # Example
903    ///
904    /// ```rust,ignore
905    /// use near_kit::Near;
906    ///
907    /// let near = Near::testnet()
908    ///     .credentials("ed25519:...", "alice.testnet")?
909    ///     .build();
910    /// ```
911    pub fn credentials(
912        mut self,
913        private_key: impl AsRef<str>,
914        account_id: impl AsRef<str>,
915    ) -> Result<Self, Error> {
916        let signer = InMemorySigner::new(account_id, private_key)?;
917        self.signer = Some(Arc::new(signer));
918        Ok(self)
919    }
920
921    /// Set the retry configuration.
922    pub fn retry_config(mut self, config: RetryConfig) -> Self {
923        self.retry_config = config;
924        self
925    }
926
927    /// Set the maximum number of transaction send attempts on `InvalidNonce` errors.
928    ///
929    /// When a transaction fails with `InvalidNonce`, the client automatically
930    /// retries with the corrected nonce from the error response. This controls
931    /// the total number of send attempts (including the initial one) before
932    /// giving up. A value of `1` means no retries (only the initial attempt).
933    ///
934    /// Defaults to `3`. For high-contention relayer scenarios, consider setting
935    /// this higher (e.g., `u32::MAX`) and wrapping sends in `tokio::timeout`.
936    ///
937    /// # Panics
938    ///
939    /// Panics if `attempts` is `0`.
940    pub fn max_nonce_retries(mut self, attempts: u32) -> Self {
941        assert!(attempts > 0, "max_nonce_retries must be at least 1");
942        self.max_nonce_retries = attempts;
943        self
944    }
945
946    /// Build the client.
947    pub fn build(self) -> Near {
948        Near {
949            rpc: Arc::new(RpcClient::with_retry_config(
950                self.rpc_url,
951                self.retry_config,
952            )),
953            signer: self.signer,
954            network: self.network,
955            max_nonce_retries: self.max_nonce_retries,
956        }
957    }
958}
959
960impl From<NearBuilder> for Near {
961    fn from(builder: NearBuilder) -> Self {
962        builder.build()
963    }
964}
965
966// ============================================================================
967// near-sandbox integration (behind feature flag or for dev dependencies)
968// ============================================================================
969
970/// Default sandbox root account ID.
971pub const SANDBOX_ROOT_ACCOUNT: &str = "sandbox";
972
973/// Default sandbox root account private key.
974pub const SANDBOX_ROOT_PRIVATE_KEY: &str = "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB";
975
976#[cfg(feature = "sandbox")]
977impl SandboxNetwork for near_sandbox::Sandbox {
978    fn rpc_url(&self) -> &str {
979        &self.rpc_addr
980    }
981
982    fn root_account_id(&self) -> &str {
983        SANDBOX_ROOT_ACCOUNT
984    }
985
986    fn root_secret_key(&self) -> &str {
987        SANDBOX_ROOT_PRIVATE_KEY
988    }
989}
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994
995    // ========================================================================
996    // Near client tests
997    // ========================================================================
998
999    #[test]
1000    fn test_near_mainnet_builder() {
1001        let near = Near::mainnet().build();
1002        assert!(near.rpc_url().contains("fastnear") || near.rpc_url().contains("near"));
1003        assert!(near.account_id().is_none()); // No signer configured
1004    }
1005
1006    #[test]
1007    fn test_near_testnet_builder() {
1008        let near = Near::testnet().build();
1009        assert!(near.rpc_url().contains("fastnear") || near.rpc_url().contains("test"));
1010        assert!(near.account_id().is_none());
1011    }
1012
1013    #[test]
1014    fn test_near_custom_builder() {
1015        let near = Near::custom("https://custom-rpc.example.com").build();
1016        assert_eq!(near.rpc_url(), "https://custom-rpc.example.com");
1017    }
1018
1019    #[test]
1020    fn test_near_with_credentials() {
1021        let near = Near::testnet()
1022            .credentials(
1023                "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1024                "alice.testnet",
1025            )
1026            .unwrap()
1027            .build();
1028
1029        assert!(near.account_id().is_some());
1030        assert_eq!(near.account_id().unwrap().as_str(), "alice.testnet");
1031    }
1032
1033    #[test]
1034    fn test_near_with_signer() {
1035        let signer = InMemorySigner::new(
1036            "bob.testnet",
1037            "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1038        ).unwrap();
1039
1040        let near = Near::testnet().signer(signer).build();
1041
1042        assert!(near.account_id().is_some());
1043        assert_eq!(near.account_id().unwrap().as_str(), "bob.testnet");
1044    }
1045
1046    #[test]
1047    fn test_near_debug() {
1048        let near = Near::testnet().build();
1049        let debug = format!("{:?}", near);
1050        assert!(debug.contains("Near"));
1051        assert!(debug.contains("rpc"));
1052    }
1053
1054    #[test]
1055    fn test_near_rpc_accessor() {
1056        let near = Near::testnet().build();
1057        let rpc = near.rpc();
1058        assert!(!rpc.url().is_empty());
1059    }
1060
1061    // ========================================================================
1062    // NearBuilder tests
1063    // ========================================================================
1064
1065    #[test]
1066    fn test_near_builder_new() {
1067        let builder = NearBuilder::new("https://example.com", Network::Custom);
1068        let near = builder.build();
1069        assert_eq!(near.rpc_url(), "https://example.com");
1070    }
1071
1072    #[test]
1073    fn test_near_builder_retry_config() {
1074        let config = RetryConfig {
1075            max_retries: 10,
1076            initial_delay_ms: 200,
1077            max_delay_ms: 10000,
1078        };
1079        let near = Near::testnet().retry_config(config).build();
1080        // Can't directly test retry config, but we can verify it builds
1081        assert!(!near.rpc_url().is_empty());
1082    }
1083
1084    #[test]
1085    fn test_near_builder_from_trait() {
1086        let builder = Near::testnet();
1087        let near: Near = builder.into();
1088        assert!(!near.rpc_url().is_empty());
1089    }
1090
1091    #[test]
1092    fn test_near_builder_credentials_invalid_key() {
1093        let result = Near::testnet().credentials("invalid-key", "alice.testnet");
1094        assert!(result.is_err());
1095    }
1096
1097    #[test]
1098    fn test_near_builder_credentials_invalid_account() {
1099        // Empty account ID is invalid
1100        let result = Near::testnet().credentials(
1101            "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1102            "",
1103        );
1104        assert!(result.is_err());
1105    }
1106
1107    // ========================================================================
1108    // SandboxNetwork trait tests
1109    // ========================================================================
1110
1111    struct MockSandbox {
1112        rpc_url: String,
1113        root_account: String,
1114        root_key: String,
1115    }
1116
1117    impl SandboxNetwork for MockSandbox {
1118        fn rpc_url(&self) -> &str {
1119            &self.rpc_url
1120        }
1121
1122        fn root_account_id(&self) -> &str {
1123            &self.root_account
1124        }
1125
1126        fn root_secret_key(&self) -> &str {
1127            &self.root_key
1128        }
1129    }
1130
1131    #[test]
1132    fn test_sandbox_network_trait() {
1133        let mock = MockSandbox {
1134            rpc_url: "http://127.0.0.1:3030".to_string(),
1135            root_account: "sandbox".to_string(),
1136            root_key: SANDBOX_ROOT_PRIVATE_KEY.to_string(),
1137        };
1138
1139        let near = Near::sandbox(&mock);
1140        assert_eq!(near.rpc_url(), "http://127.0.0.1:3030");
1141        assert!(near.account_id().is_some());
1142        assert_eq!(near.account_id().unwrap().as_str(), "sandbox");
1143    }
1144
1145    // ========================================================================
1146    // Constant tests
1147    // ========================================================================
1148
1149    #[test]
1150    fn test_sandbox_constants() {
1151        assert_eq!(SANDBOX_ROOT_ACCOUNT, "sandbox");
1152        assert!(SANDBOX_ROOT_PRIVATE_KEY.starts_with("ed25519:"));
1153    }
1154
1155    // ========================================================================
1156    // Clone tests
1157    // ========================================================================
1158
1159    #[test]
1160    fn test_near_clone() {
1161        let near1 = Near::testnet().build();
1162        let near2 = near1.clone();
1163        assert_eq!(near1.rpc_url(), near2.rpc_url());
1164    }
1165
1166    #[test]
1167    fn test_near_with_signer_derived() {
1168        let near = Near::testnet().build();
1169        assert!(near.account_id().is_none());
1170
1171        let signer = InMemorySigner::new(
1172            "alice.testnet",
1173            "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1174        ).unwrap();
1175
1176        let alice = near.with_signer(signer);
1177        assert_eq!(alice.account_id().unwrap().as_str(), "alice.testnet");
1178        assert_eq!(alice.rpc_url(), near.rpc_url()); // Same transport
1179        assert!(near.account_id().is_none()); // Original unchanged
1180    }
1181
1182    #[test]
1183    fn test_near_with_signer_multiple_accounts() {
1184        let near = Near::testnet().build();
1185
1186        let alice = near.with_signer(InMemorySigner::new(
1187            "alice.testnet",
1188            "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1189        ).unwrap());
1190
1191        let bob = near.with_signer(InMemorySigner::new(
1192            "bob.testnet",
1193            "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1194        ).unwrap());
1195
1196        assert_eq!(alice.account_id().unwrap().as_str(), "alice.testnet");
1197        assert_eq!(bob.account_id().unwrap().as_str(), "bob.testnet");
1198        assert_eq!(alice.rpc_url(), bob.rpc_url()); // Shared transport
1199    }
1200
1201    // ========================================================================
1202    // from_env tests
1203    // ========================================================================
1204
1205    // NOTE: Environment variable tests are consolidated into a single test
1206    // because they modify global state and would race with each other if
1207    // run in parallel. Each scenario is tested sequentially within this test.
1208    #[test]
1209    fn test_from_env_scenarios() {
1210        // Helper to clean up env vars
1211        fn clear_env() {
1212            // SAFETY: This is a test and we control the execution
1213            unsafe {
1214                std::env::remove_var("NEAR_NETWORK");
1215                std::env::remove_var("NEAR_ACCOUNT_ID");
1216                std::env::remove_var("NEAR_PRIVATE_KEY");
1217            }
1218        }
1219
1220        // Scenario 1: No vars - defaults to testnet, read-only
1221        clear_env();
1222        {
1223            let near = Near::from_env().unwrap();
1224            assert!(
1225                near.rpc_url().contains("test") || near.rpc_url().contains("fastnear"),
1226                "Expected testnet URL, got: {}",
1227                near.rpc_url()
1228            );
1229            assert!(near.account_id().is_none());
1230        }
1231
1232        // Scenario 2: Mainnet network
1233        clear_env();
1234        unsafe {
1235            std::env::set_var("NEAR_NETWORK", "mainnet");
1236        }
1237        {
1238            let near = Near::from_env().unwrap();
1239            assert!(
1240                near.rpc_url().contains("mainnet") || near.rpc_url().contains("fastnear"),
1241                "Expected mainnet URL, got: {}",
1242                near.rpc_url()
1243            );
1244            assert!(near.account_id().is_none());
1245        }
1246
1247        // Scenario 3: Custom URL
1248        clear_env();
1249        unsafe {
1250            std::env::set_var("NEAR_NETWORK", "https://custom-rpc.example.com");
1251        }
1252        {
1253            let near = Near::from_env().unwrap();
1254            assert_eq!(near.rpc_url(), "https://custom-rpc.example.com");
1255        }
1256
1257        // Scenario 4: Full credentials
1258        clear_env();
1259        unsafe {
1260            std::env::set_var("NEAR_NETWORK", "testnet");
1261            std::env::set_var("NEAR_ACCOUNT_ID", "alice.testnet");
1262            std::env::set_var(
1263                "NEAR_PRIVATE_KEY",
1264                "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1265            );
1266        }
1267        {
1268            let near = Near::from_env().unwrap();
1269            assert!(near.account_id().is_some());
1270            assert_eq!(near.account_id().unwrap().as_str(), "alice.testnet");
1271        }
1272
1273        // Scenario 5: Account without key - should error
1274        clear_env();
1275        unsafe {
1276            std::env::set_var("NEAR_ACCOUNT_ID", "alice.testnet");
1277        }
1278        {
1279            let result = Near::from_env();
1280            assert!(
1281                result.is_err(),
1282                "Expected error when account set without key"
1283            );
1284            let err = result.unwrap_err();
1285            assert!(
1286                err.to_string().contains("NEAR_PRIVATE_KEY"),
1287                "Error should mention NEAR_PRIVATE_KEY: {}",
1288                err
1289            );
1290        }
1291
1292        // Scenario 6: Key without account - should error
1293        clear_env();
1294        unsafe {
1295            std::env::set_var(
1296                "NEAR_PRIVATE_KEY",
1297                "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
1298            );
1299        }
1300        {
1301            let result = Near::from_env();
1302            assert!(
1303                result.is_err(),
1304                "Expected error when key set without account"
1305            );
1306            let err = result.unwrap_err();
1307            assert!(
1308                err.to_string().contains("NEAR_ACCOUNT_ID"),
1309                "Error should mention NEAR_ACCOUNT_ID: {}",
1310                err
1311            );
1312        }
1313
1314        // Final cleanup
1315        clear_env();
1316    }
1317}