near_workspaces/network/
sandbox.rs

1use std::convert::TryFrom;
2use std::path::PathBuf;
3
4use async_trait::async_trait;
5use near_jsonrpc_client::methods::sandbox_fast_forward::RpcSandboxFastForwardRequest;
6use near_jsonrpc_client::methods::sandbox_patch_state::RpcSandboxPatchStateRequest;
7use near_primitives::state_record::StateRecord;
8use near_sandbox_utils as sandbox;
9
10use super::builder::{FromNetworkBuilder, NetworkBuilder};
11use super::server::ValidatorKey;
12use super::{NetworkClient, NetworkInfo, RootAccountSubaccountCreator, TopLevelAccountCreator};
13use crate::error::SandboxErrorCode;
14use crate::network::server::SandboxServer;
15use crate::network::Info;
16use crate::result::{Execution, ExecutionFinalResult, Result};
17use crate::rpc::client::Client;
18use crate::types::{AccountId, InMemorySigner, NearToken, SecretKey};
19use crate::{Account, Contract, Network, Worker};
20
21// Constant taken from nearcore crate to avoid dependency
22const DEFAULT_DEPOSIT: NearToken = NearToken::from_near(100);
23/// Local sandboxed environment/network
24///
25/// Can be used to test without interacting with
26/// networks that are online such as mainnet and testnet. Look at [`workspaces::sandbox`]
27/// for how to spin up a sandboxed network and interact with it.
28///
29/// [`workspaces::sandbox`]: crate::sandbox
30pub struct Sandbox {
31    pub(crate) server: SandboxServer,
32    client: Client,
33    info: Info,
34    version: Option<String>,
35}
36
37impl Sandbox {
38    pub(crate) fn root_signer(&self) -> Result<InMemorySigner> {
39        InMemorySigner::try_from(self.server.validator_key.clone())
40    }
41
42    pub(crate) fn registrar_signer(&self) -> Result<InMemorySigner> {
43        match &self.server.validator_key {
44            ValidatorKey::HomeDir(home_dir) => {
45                let path = home_dir.join("registrar.json");
46                InMemorySigner::from_file(&path)
47            }
48            ValidatorKey::Known(account_id, secret_key) => Ok(InMemorySigner::from_secret_key(
49                account_id.clone(),
50                secret_key.clone(),
51            )),
52        }
53    }
54
55    pub(crate) async fn from_builder_with_version(
56        build: NetworkBuilder<'_, Self>,
57        version: &str,
58    ) -> Result<Self> {
59        // Check the conditions of the provided rpc_url and validator_key
60        let mut server = match (build.rpc_addr, build.validator_key) {
61            // Connect to a provided sandbox:
62            (Some(rpc_url), Some(validator_key)) => SandboxServer::new(rpc_url, validator_key)?,
63
64            // Spawn a new sandbox since rpc_url and home_dir weren't specified:
65            (None, None) => SandboxServer::run_new_with_version(version).await?,
66
67            // Missing inputted parameters for sandbox:
68            (Some(rpc_url), None) => {
69                return Err(SandboxErrorCode::InitFailure.message(format!(
70                    "Custom rpc_url={rpc_url} requires validator_key set."
71                )));
72            }
73            (None, Some(validator_key)) => {
74                return Err(SandboxErrorCode::InitFailure.message(format!(
75                    "Custom validator_key={validator_key:?} requires rpc_url set."
76                )));
77            }
78        };
79
80        let client = Client::new(&server.rpc_addr(), build.api_key)?;
81        client.wait_for_rpc().await?;
82
83        // Server locks some ports on startup due to potential port collision, so we need
84        // to unlock the lockfiles after RPC is ready. Not necessarily needed here since
85        // they get unlocked anyways on the server's drop, but it is nice to clean up the
86        // lockfiles as soon as possible.
87        server.unlock_lockfiles()?;
88
89        let root_id = InMemorySigner::try_from(server.validator_key.clone())?.account_id;
90
91        let info = Info {
92            name: build.name.into(),
93            root_id,
94            keystore_path: PathBuf::from(".near-credentials/sandbox/"),
95            rpc_url: url::Url::parse(&server.rpc_addr()).expect("url is hardcoded"),
96        };
97
98        Ok(Self {
99            server,
100            client,
101            info,
102            version: Some(version.to_string()),
103        })
104    }
105}
106
107impl std::fmt::Debug for Sandbox {
108    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
109        f.debug_struct("Sandbox")
110            .field("root_id", &self.info.root_id)
111            .field("rpc_url", &self.info.rpc_url)
112            .field("rpc_port", &self.server.rpc_port())
113            .field("net_port", &self.server.net_port())
114            .field("version", &self.version)
115            .finish()
116    }
117}
118
119#[async_trait]
120impl FromNetworkBuilder for Sandbox {
121    async fn from_builder<'a>(build: NetworkBuilder<'a, Self>) -> Result<Self> {
122        Self::from_builder_with_version(build, sandbox::DEFAULT_NEAR_SANDBOX_VERSION).await
123    }
124}
125
126#[async_trait]
127impl TopLevelAccountCreator for Sandbox {
128    async fn create_tla(
129        &self,
130        worker: Worker<dyn Network>,
131        id: AccountId,
132        sk: SecretKey,
133    ) -> Result<Execution<Account>> {
134        let root_signer = self.registrar_signer()?;
135        let outcome = self
136            .client()
137            .create_account(&root_signer, &id, sk.public_key(), DEFAULT_DEPOSIT)
138            .await?;
139        let signer = InMemorySigner::from_secret_key(id, sk);
140        Ok(Execution {
141            result: Account::new(signer, worker),
142            details: ExecutionFinalResult::from_view(outcome),
143        })
144    }
145
146    async fn create_tla_and_deploy(
147        &self,
148        worker: Worker<dyn Network>,
149        id: AccountId,
150        sk: SecretKey,
151        wasm: &[u8],
152    ) -> Result<Execution<Contract>> {
153        let root_signer = self.registrar_signer()?;
154        let outcome = self
155            .client()
156            .create_account_and_deploy(
157                &root_signer,
158                &id,
159                sk.public_key(),
160                DEFAULT_DEPOSIT,
161                wasm.into(),
162            )
163            .await?;
164        let signer = InMemorySigner::from_secret_key(id, sk);
165        Ok(Execution {
166            result: Contract::new(signer, worker),
167            details: ExecutionFinalResult::from_view(outcome),
168        })
169    }
170}
171
172#[async_trait]
173impl RootAccountSubaccountCreator for Sandbox {
174    fn root_account_id(&self) -> Result<AccountId> {
175        Ok(self.root_signer()?.account_id)
176    }
177
178    async fn create_root_account_subaccount(
179        &self,
180        worker: Worker<dyn Network>,
181        subaccount_prefix: AccountId,
182        sk: SecretKey,
183    ) -> Result<Execution<Account>> {
184        let id = self.compute_subaccount_id(subaccount_prefix)?;
185        let root_signer = self.root_signer()?;
186        let outcome = self
187            .client()
188            .create_account(&root_signer, &id, sk.public_key(), DEFAULT_DEPOSIT)
189            .await?;
190        let signer = InMemorySigner::from_secret_key(id, sk);
191        Ok(Execution {
192            result: Account::new(signer, worker),
193            details: ExecutionFinalResult::from_view(outcome),
194        })
195    }
196    async fn create_root_account_subaccount_and_deploy(
197        &self,
198        worker: Worker<dyn Network>,
199        subaccount_prefix: AccountId,
200        sk: SecretKey,
201        wasm: &[u8],
202    ) -> Result<Execution<Contract>> {
203        let id = self.compute_subaccount_id(subaccount_prefix)?;
204        let root_signer = self.root_signer()?;
205        let outcome = self
206            .client()
207            .create_account_and_deploy(
208                &root_signer,
209                &id,
210                sk.public_key(),
211                DEFAULT_DEPOSIT,
212                wasm.into(),
213            )
214            .await?;
215        let signer = InMemorySigner::from_secret_key(id, sk);
216        Ok(Execution {
217            result: Contract::new(signer, worker),
218            details: ExecutionFinalResult::from_view(outcome),
219        })
220    }
221}
222
223impl NetworkClient for Sandbox {
224    fn client(&self) -> &Client {
225        &self.client
226    }
227}
228
229impl NetworkInfo for Sandbox {
230    fn info(&self) -> &Info {
231        &self.info
232    }
233}
234
235impl Sandbox {
236    pub(crate) async fn patch_state(
237        &self,
238        contract_id: &AccountId,
239        key: &[u8],
240        value: &[u8],
241    ) -> Result<()> {
242        let state = StateRecord::Data {
243            account_id: contract_id.to_owned(),
244            data_key: key.to_vec().into(),
245            value: value.to_vec().into(),
246        };
247        let records = vec![state];
248
249        // NOTE: RpcSandboxPatchStateResponse is an empty struct with no fields, so don't do anything with it:
250        let _patch_resp = self
251            .client()
252            .query(&RpcSandboxPatchStateRequest { records })
253            .await
254            .map_err(|e| SandboxErrorCode::PatchStateFailure.custom(e))?;
255
256        Ok(())
257    }
258
259    pub(crate) async fn fast_forward(&self, delta_height: u64) -> Result<()> {
260        // NOTE: RpcSandboxFastForwardResponse is an empty struct with no fields, so don't do anything with it:
261        self.client()
262            // TODO: replace this with the `query` variant when RpcSandboxFastForwardRequest impls Debug
263            .query_nolog(&RpcSandboxFastForwardRequest { delta_height })
264            .await
265            .map_err(|e| SandboxErrorCode::FastForwardFailure.custom(e))?;
266
267        Ok(())
268    }
269}