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