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