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