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