use std::sync::Arc;
use serde::de::DeserializeOwned;
use crate::contract::ContractClient;
use crate::error::Error;
use crate::types::{AccountId, Gas, IntoNearToken, NearToken, Network, PublicKey, SecretKey};
use super::query::{AccessKeysQuery, AccountExistsQuery, AccountQuery, BalanceQuery, ViewCall};
use super::rpc::{MAINNET, RetryConfig, RpcClient, TESTNET};
use super::signer::{InMemorySigner, Signer};
use super::transaction::{CallBuilder, TransactionBuilder};
use crate::types::TxExecutionStatus;
pub trait SandboxNetwork {
fn rpc_url(&self) -> &str;
fn root_account_id(&self) -> &str;
fn root_secret_key(&self) -> &str;
}
#[derive(Clone)]
pub struct Near {
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
network: Network,
max_nonce_retries: u32,
}
impl Near {
pub fn mainnet() -> NearBuilder {
NearBuilder::new(MAINNET.rpc_url, Network::Mainnet)
}
pub fn testnet() -> NearBuilder {
NearBuilder::new(TESTNET.rpc_url, Network::Testnet)
}
pub fn custom(rpc_url: impl Into<String>) -> NearBuilder {
NearBuilder::new(rpc_url, Network::Custom)
}
pub fn from_env() -> Result<Near, Error> {
let network = std::env::var("NEAR_NETWORK").ok();
let account_id = std::env::var("NEAR_ACCOUNT_ID").ok();
let private_key = std::env::var("NEAR_PRIVATE_KEY").ok();
let mut builder = match network.as_deref() {
Some("mainnet") => Near::mainnet(),
Some("testnet") | None => Near::testnet(),
Some(url) => Near::custom(url),
};
match (account_id, private_key) {
(Some(account), Some(key)) => {
builder = builder.credentials(&key, &account)?;
}
(Some(_), None) => {
return Err(Error::Config(
"NEAR_ACCOUNT_ID is set but NEAR_PRIVATE_KEY is missing".into(),
));
}
(None, Some(_)) => {
return Err(Error::Config(
"NEAR_PRIVATE_KEY is set but NEAR_ACCOUNT_ID is missing".into(),
));
}
(None, None) => {
}
}
Ok(builder.build())
}
pub fn sandbox(network: &impl SandboxNetwork) -> Near {
let secret_key: SecretKey = network
.root_secret_key()
.parse()
.expect("sandbox should provide valid secret key");
let account_id: AccountId = network
.root_account_id()
.parse()
.expect("sandbox should provide valid account id");
let signer = InMemorySigner::from_secret_key(account_id, secret_key);
Near {
rpc: Arc::new(RpcClient::new(network.rpc_url())),
signer: Some(Arc::new(signer)),
network: Network::Sandbox,
max_nonce_retries: 3,
}
}
pub fn rpc(&self) -> &RpcClient {
&self.rpc
}
pub fn rpc_url(&self) -> &str {
self.rpc.url()
}
pub fn account_id(&self) -> Option<&AccountId> {
self.signer.as_ref().map(|s| s.account_id())
}
pub fn network(&self) -> Network {
self.network
}
pub fn with_signer(&self, signer: impl Signer + 'static) -> Near {
Near {
rpc: self.rpc.clone(),
signer: Some(Arc::new(signer)),
network: self.network,
max_nonce_retries: self.max_nonce_retries,
}
}
pub fn balance(&self, account_id: impl AsRef<str>) -> BalanceQuery {
let account_id = AccountId::parse_lenient(account_id);
BalanceQuery::new(self.rpc.clone(), account_id)
}
pub fn account(&self, account_id: impl AsRef<str>) -> AccountQuery {
let account_id = AccountId::parse_lenient(account_id);
AccountQuery::new(self.rpc.clone(), account_id)
}
pub fn account_exists(&self, account_id: impl AsRef<str>) -> AccountExistsQuery {
let account_id = AccountId::parse_lenient(account_id);
AccountExistsQuery::new(self.rpc.clone(), account_id)
}
pub fn view<T>(&self, contract_id: impl AsRef<str>, method: &str) -> ViewCall<T> {
let contract_id = AccountId::parse_lenient(contract_id);
ViewCall::new(self.rpc.clone(), contract_id, method.to_string())
}
pub fn access_keys(&self, account_id: impl AsRef<str>) -> AccessKeysQuery {
let account_id = AccountId::parse_lenient(account_id);
AccessKeysQuery::new(self.rpc.clone(), account_id)
}
pub async fn sign_message(
&self,
params: crate::types::nep413::SignMessageParams,
) -> Result<crate::types::nep413::SignedMessage, Error> {
let signer = self.signer.as_ref().ok_or(Error::NoSigner)?;
let key = signer.key();
key.sign_nep413(signer.account_id(), ¶ms)
.await
.map_err(Error::Signing)
}
pub fn transfer(
&self,
receiver: impl AsRef<str>,
amount: impl IntoNearToken,
) -> TransactionBuilder {
self.transaction(receiver).transfer(amount)
}
pub fn call(&self, contract_id: impl AsRef<str>, method: &str) -> CallBuilder {
self.transaction(contract_id).call(method)
}
pub fn deploy(
&self,
account_id: impl AsRef<str>,
code: impl Into<Vec<u8>>,
) -> TransactionBuilder {
self.transaction(account_id).deploy(code)
}
pub fn add_full_access_key(
&self,
account_id: impl AsRef<str>,
public_key: PublicKey,
) -> TransactionBuilder {
self.transaction(account_id).add_full_access_key(public_key)
}
pub fn delete_key(
&self,
account_id: impl AsRef<str>,
public_key: PublicKey,
) -> TransactionBuilder {
self.transaction(account_id).delete_key(public_key)
}
pub fn transaction(&self, receiver_id: impl AsRef<str>) -> TransactionBuilder {
let receiver_id = AccountId::parse_lenient(receiver_id);
TransactionBuilder::new(
self.rpc.clone(),
self.signer.clone(),
receiver_id,
self.max_nonce_retries,
)
}
pub async fn send(
&self,
signed_tx: &crate::types::SignedTransaction,
) -> Result<crate::types::FinalExecutionOutcome, Error> {
self.send_with_options(signed_tx, TxExecutionStatus::ExecutedOptimistic)
.await
}
pub async fn send_with_options(
&self,
signed_tx: &crate::types::SignedTransaction,
wait_until: TxExecutionStatus,
) -> Result<crate::types::FinalExecutionOutcome, Error> {
let response = self.rpc.send_tx(signed_tx, wait_until).await?;
let outcome = response.outcome.ok_or_else(|| {
Error::InvalidTransaction(format!(
"Transaction {} submitted with wait_until={:?} but no execution outcome \
was returned. Use rpc().send_tx() for fire-and-forget submission.",
response.transaction_hash, wait_until,
))
})?;
if outcome.is_failure() {
return Err(Error::TransactionFailed(
outcome.failure_message().unwrap_or_default(),
));
}
Ok(outcome)
}
pub async fn view_with_args<T: DeserializeOwned + Send + 'static, A: serde::Serialize>(
&self,
contract_id: impl AsRef<str>,
method: &str,
args: &A,
) -> Result<T, Error> {
let contract_id = AccountId::parse_lenient(contract_id);
ViewCall::new(self.rpc.clone(), contract_id, method.to_string())
.args(args)
.await
}
pub async fn call_with_args<A: serde::Serialize>(
&self,
contract_id: impl AsRef<str>,
method: &str,
args: &A,
) -> Result<crate::types::FinalExecutionOutcome, Error> {
self.call(contract_id, method).args(args).await
}
pub async fn call_with_options<A: serde::Serialize>(
&self,
contract_id: impl AsRef<str>,
method: &str,
args: &A,
gas: Gas,
deposit: NearToken,
) -> Result<crate::types::FinalExecutionOutcome, Error> {
self.call(contract_id, method)
.args(args)
.gas(gas)
.deposit(deposit)
.await
}
pub fn contract<T: crate::Contract + ?Sized>(
&self,
contract_id: impl AsRef<str>,
) -> T::Client<'_> {
let contract_id = AccountId::parse_lenient(contract_id);
T::Client::new(self, contract_id)
}
pub fn ft(
&self,
contract: impl crate::tokens::IntoContractId,
) -> Result<crate::tokens::FungibleToken, Error> {
let contract_id = contract.into_contract_id(self.network)?;
Ok(crate::tokens::FungibleToken::new(
self.rpc.clone(),
self.signer.clone(),
contract_id,
self.max_nonce_retries,
))
}
pub fn nft(
&self,
contract: impl crate::tokens::IntoContractId,
) -> Result<crate::tokens::NonFungibleToken, Error> {
let contract_id = contract.into_contract_id(self.network)?;
Ok(crate::tokens::NonFungibleToken::new(
self.rpc.clone(),
self.signer.clone(),
contract_id,
self.max_nonce_retries,
))
}
}
impl std::fmt::Debug for Near {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Near")
.field("rpc", &self.rpc)
.field("account_id", &self.account_id())
.finish()
}
}
pub struct NearBuilder {
rpc_url: String,
signer: Option<Arc<dyn Signer>>,
retry_config: RetryConfig,
network: Network,
max_nonce_retries: u32,
}
impl NearBuilder {
fn new(rpc_url: impl Into<String>, network: Network) -> Self {
Self {
rpc_url: rpc_url.into(),
signer: None,
retry_config: RetryConfig::default(),
network,
max_nonce_retries: 3,
}
}
pub fn signer(mut self, signer: impl Signer + 'static) -> Self {
self.signer = Some(Arc::new(signer));
self
}
pub fn credentials(
mut self,
private_key: impl AsRef<str>,
account_id: impl AsRef<str>,
) -> Result<Self, Error> {
let signer = InMemorySigner::new(account_id, private_key)?;
self.signer = Some(Arc::new(signer));
Ok(self)
}
pub fn retry_config(mut self, config: RetryConfig) -> Self {
self.retry_config = config;
self
}
pub fn max_nonce_retries(mut self, attempts: u32) -> Self {
assert!(attempts > 0, "max_nonce_retries must be at least 1");
self.max_nonce_retries = attempts;
self
}
pub fn build(self) -> Near {
Near {
rpc: Arc::new(RpcClient::with_retry_config(
self.rpc_url,
self.retry_config,
)),
signer: self.signer,
network: self.network,
max_nonce_retries: self.max_nonce_retries,
}
}
}
impl From<NearBuilder> for Near {
fn from(builder: NearBuilder) -> Self {
builder.build()
}
}
pub const SANDBOX_ROOT_ACCOUNT: &str = "sandbox";
pub const SANDBOX_ROOT_PRIVATE_KEY: &str = "ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB";
#[cfg(feature = "sandbox")]
impl SandboxNetwork for near_sandbox::Sandbox {
fn rpc_url(&self) -> &str {
&self.rpc_addr
}
fn root_account_id(&self) -> &str {
SANDBOX_ROOT_ACCOUNT
}
fn root_secret_key(&self) -> &str {
SANDBOX_ROOT_PRIVATE_KEY
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_near_mainnet_builder() {
let near = Near::mainnet().build();
assert!(near.rpc_url().contains("fastnear") || near.rpc_url().contains("near"));
assert!(near.account_id().is_none()); }
#[test]
fn test_near_testnet_builder() {
let near = Near::testnet().build();
assert!(near.rpc_url().contains("fastnear") || near.rpc_url().contains("test"));
assert!(near.account_id().is_none());
}
#[test]
fn test_near_custom_builder() {
let near = Near::custom("https://custom-rpc.example.com").build();
assert_eq!(near.rpc_url(), "https://custom-rpc.example.com");
}
#[test]
fn test_near_with_credentials() {
let near = Near::testnet()
.credentials(
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
"alice.testnet",
)
.unwrap()
.build();
assert!(near.account_id().is_some());
assert_eq!(near.account_id().unwrap().as_str(), "alice.testnet");
}
#[test]
fn test_near_with_signer() {
let signer = InMemorySigner::new(
"bob.testnet",
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
).unwrap();
let near = Near::testnet().signer(signer).build();
assert!(near.account_id().is_some());
assert_eq!(near.account_id().unwrap().as_str(), "bob.testnet");
}
#[test]
fn test_near_debug() {
let near = Near::testnet().build();
let debug = format!("{:?}", near);
assert!(debug.contains("Near"));
assert!(debug.contains("rpc"));
}
#[test]
fn test_near_rpc_accessor() {
let near = Near::testnet().build();
let rpc = near.rpc();
assert!(!rpc.url().is_empty());
}
#[test]
fn test_near_builder_new() {
let builder = NearBuilder::new("https://example.com", Network::Custom);
let near = builder.build();
assert_eq!(near.rpc_url(), "https://example.com");
}
#[test]
fn test_near_builder_retry_config() {
let config = RetryConfig {
max_retries: 10,
initial_delay_ms: 200,
max_delay_ms: 10000,
};
let near = Near::testnet().retry_config(config).build();
assert!(!near.rpc_url().is_empty());
}
#[test]
fn test_near_builder_from_trait() {
let builder = Near::testnet();
let near: Near = builder.into();
assert!(!near.rpc_url().is_empty());
}
#[test]
fn test_near_builder_credentials_invalid_key() {
let result = Near::testnet().credentials("invalid-key", "alice.testnet");
assert!(result.is_err());
}
#[test]
fn test_near_builder_credentials_invalid_account() {
let result = Near::testnet().credentials(
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
"",
);
assert!(result.is_err());
}
struct MockSandbox {
rpc_url: String,
root_account: String,
root_key: String,
}
impl SandboxNetwork for MockSandbox {
fn rpc_url(&self) -> &str {
&self.rpc_url
}
fn root_account_id(&self) -> &str {
&self.root_account
}
fn root_secret_key(&self) -> &str {
&self.root_key
}
}
#[test]
fn test_sandbox_network_trait() {
let mock = MockSandbox {
rpc_url: "http://127.0.0.1:3030".to_string(),
root_account: "sandbox".to_string(),
root_key: SANDBOX_ROOT_PRIVATE_KEY.to_string(),
};
let near = Near::sandbox(&mock);
assert_eq!(near.rpc_url(), "http://127.0.0.1:3030");
assert!(near.account_id().is_some());
assert_eq!(near.account_id().unwrap().as_str(), "sandbox");
}
#[test]
fn test_sandbox_constants() {
assert_eq!(SANDBOX_ROOT_ACCOUNT, "sandbox");
assert!(SANDBOX_ROOT_PRIVATE_KEY.starts_with("ed25519:"));
}
#[test]
fn test_near_clone() {
let near1 = Near::testnet().build();
let near2 = near1.clone();
assert_eq!(near1.rpc_url(), near2.rpc_url());
}
#[test]
fn test_near_with_signer_derived() {
let near = Near::testnet().build();
assert!(near.account_id().is_none());
let signer = InMemorySigner::new(
"alice.testnet",
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
).unwrap();
let alice = near.with_signer(signer);
assert_eq!(alice.account_id().unwrap().as_str(), "alice.testnet");
assert_eq!(alice.rpc_url(), near.rpc_url()); assert!(near.account_id().is_none()); }
#[test]
fn test_near_with_signer_multiple_accounts() {
let near = Near::testnet().build();
let alice = near.with_signer(InMemorySigner::new(
"alice.testnet",
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
).unwrap());
let bob = near.with_signer(InMemorySigner::new(
"bob.testnet",
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
).unwrap());
assert_eq!(alice.account_id().unwrap().as_str(), "alice.testnet");
assert_eq!(bob.account_id().unwrap().as_str(), "bob.testnet");
assert_eq!(alice.rpc_url(), bob.rpc_url()); }
#[test]
fn test_from_env_scenarios() {
fn clear_env() {
unsafe {
std::env::remove_var("NEAR_NETWORK");
std::env::remove_var("NEAR_ACCOUNT_ID");
std::env::remove_var("NEAR_PRIVATE_KEY");
}
}
clear_env();
{
let near = Near::from_env().unwrap();
assert!(
near.rpc_url().contains("test") || near.rpc_url().contains("fastnear"),
"Expected testnet URL, got: {}",
near.rpc_url()
);
assert!(near.account_id().is_none());
}
clear_env();
unsafe {
std::env::set_var("NEAR_NETWORK", "mainnet");
}
{
let near = Near::from_env().unwrap();
assert!(
near.rpc_url().contains("mainnet") || near.rpc_url().contains("fastnear"),
"Expected mainnet URL, got: {}",
near.rpc_url()
);
assert!(near.account_id().is_none());
}
clear_env();
unsafe {
std::env::set_var("NEAR_NETWORK", "https://custom-rpc.example.com");
}
{
let near = Near::from_env().unwrap();
assert_eq!(near.rpc_url(), "https://custom-rpc.example.com");
}
clear_env();
unsafe {
std::env::set_var("NEAR_NETWORK", "testnet");
std::env::set_var("NEAR_ACCOUNT_ID", "alice.testnet");
std::env::set_var(
"NEAR_PRIVATE_KEY",
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
);
}
{
let near = Near::from_env().unwrap();
assert!(near.account_id().is_some());
assert_eq!(near.account_id().unwrap().as_str(), "alice.testnet");
}
clear_env();
unsafe {
std::env::set_var("NEAR_ACCOUNT_ID", "alice.testnet");
}
{
let result = Near::from_env();
assert!(
result.is_err(),
"Expected error when account set without key"
);
let err = result.unwrap_err();
assert!(
err.to_string().contains("NEAR_PRIVATE_KEY"),
"Error should mention NEAR_PRIVATE_KEY: {}",
err
);
}
clear_env();
unsafe {
std::env::set_var(
"NEAR_PRIVATE_KEY",
"ed25519:3tgdk2wPraJzT4nsTuf86UX41xgPNk3MHnq8epARMdBNs29AFEztAuaQ7iHddDfXG9F2RzV1XNQYgJyAyoW51UBB",
);
}
{
let result = Near::from_env();
assert!(
result.is_err(),
"Expected error when key set without account"
);
let err = result.unwrap_err();
assert!(
err.to_string().contains("NEAR_ACCOUNT_ID"),
"Error should mention NEAR_ACCOUNT_ID: {}",
err
);
}
clear_env();
}
}