utility_workspaces/network/
sandbox.rs

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