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(), ¶ms)
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}