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