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