diff --git a/examples/src/spooning.rs b/examples/src/spooning.rs index 5caf7938..3d987978 100644 --- a/examples/src/spooning.rs +++ b/examples/src/spooning.rs @@ -97,11 +97,7 @@ async fn main() -> anyhow::Result<()> { // Patch our testnet STATE into our local sandbox: worker - .patch_state( - sandbox_contract.id(), - "STATE".as_bytes(), - &status_msg.try_to_vec()?, - ) + .patch_state(sandbox_contract.id(), "STATE", status_msg.try_to_vec()?) .await?; // Now grab the state to see that it has indeed been patched: diff --git a/workspaces/build.rs b/workspaces/build.rs index b31f84ca..abd7f602 100644 --- a/workspaces/build.rs +++ b/workspaces/build.rs @@ -1,8 +1,9 @@ fn main() { let doc_build = cfg!(doc) || std::env::var("DOCS_RS").is_ok(); if !doc_build && cfg!(feature = "install") { - // TODO Update commit to stable version once binaries are published correctly - near_sandbox_utils::install_with_version("master/97c0410de519ecaca369aaee26f0ca5eb9e7de06") - .expect("Could not install sandbox"); + match near_sandbox_utils::ensure_sandbox_bin(){ + Ok(p) => println!("Successfully installed sandbox in: {:?}", p), + Err(e) => panic!("Could not install sandbox\nReason: {:?}", e), + } } } diff --git a/workspaces/src/network/mod.rs b/workspaces/src/network/mod.rs index e2367907..af860334 100644 --- a/workspaces/src/network/mod.rs +++ b/workspaces/src/network/mod.rs @@ -10,6 +10,9 @@ mod server; mod testnet; pub(crate) mod variants; +pub(crate) use sandbox::PatchAccessKeyTransaction; +pub(crate) use sandbox::PatchStateAccountTransaction; +pub(crate) use sandbox::PatchStateTransaction; pub(crate) use variants::DEV_ACCOUNT_SEED; pub use self::betanet::Betanet; diff --git a/workspaces/src/network/sandbox.rs b/workspaces/src/network/sandbox.rs index 9af15fd2..dcf27204 100644 --- a/workspaces/src/network/sandbox.rs +++ b/workspaces/src/network/sandbox.rs @@ -4,7 +4,10 @@ use std::str::FromStr; use async_trait::async_trait; use near_jsonrpc_client::methods::sandbox_fast_forward::RpcSandboxFastForwardRequest; use near_jsonrpc_client::methods::sandbox_patch_state::RpcSandboxPatchStateRequest; +use near_primitives::hash::CryptoHash; use near_primitives::state_record::StateRecord; +use near_primitives::views::AccountView; +use std::iter::IntoIterator; use super::{AllowDevAccountCreation, NetworkClient, NetworkInfo, TopLevelAccountCreator}; use crate::network::server::SandboxServer; @@ -12,7 +15,7 @@ use crate::network::Info; use crate::result::CallExecution; use crate::rpc::client::Client; use crate::rpc::patch::ImportContractTransaction; -use crate::types::{AccountId, Balance, InMemorySigner, SecretKey}; +use crate::types::{AccountId, Balance, InMemorySigner, Nonce, SecretKey, StorageUsage}; use crate::{Account, Contract, Network, Worker}; // Constant taken from nearcore crate to avoid dependency @@ -135,21 +138,78 @@ impl Sandbox { ImportContractTransaction::new(id.to_owned(), worker.client(), self.client()) } - pub(crate) async fn patch_state( + pub(crate) fn patch_state(&self, account_id: AccountId) -> PatchStateTransaction { + PatchStateTransaction::new(self, account_id) + } + + pub(crate) fn patch_account(&self, account_id: AccountId) -> PatchStateAccountTransaction { + PatchStateAccountTransaction::new(self, account_id) + } + + pub(crate) fn patch_access_key( &self, - contract_id: &AccountId, - key: &[u8], - value: &[u8], - ) -> anyhow::Result<()> { - let state = StateRecord::Data { - account_id: contract_id.to_owned(), + account_id: AccountId, + public_key: crate::types::PublicKey, + ) -> PatchAccessKeyTransaction { + PatchAccessKeyTransaction::new(self, account_id, public_key) + } + + // shall we expose convenience patch methods here for consistent API? + + pub(crate) async fn fast_forward(&self, delta_height: u64) -> anyhow::Result<()> { + // NOTE: RpcSandboxFastForwardResponse is an empty struct with no fields, so don't do anything with it: + self.client() + // TODO: replace this with the `query` variant when RpcSandboxFastForwardRequest impls Debug + .query_nolog(&RpcSandboxFastForwardRequest { delta_height }) + .await + .map_err(|err| anyhow::anyhow!("Failed to fast forward: {:?}", err))?; + + Ok(()) + } +} + +//todo: review naming +#[must_use = "don't forget to .apply() this `SandboxPatchStateBuilder`"] +pub struct PatchStateTransaction<'s> { + sandbox: &'s Sandbox, + account_id: AccountId, + records: Vec, +} + +impl<'s> PatchStateTransaction<'s> { + pub fn new(sandbox: &'s Sandbox, account_id: AccountId) -> Self { + PatchStateTransaction { + sandbox, + account_id, + records: Vec::with_capacity(4), + } + } + + pub fn data(mut self, key: &[u8], value: &[u8]) -> Self { + let data = StateRecord::Data { + account_id: self.account_id.clone(), data_key: key.to_vec(), value: value.to_vec(), }; - let records = vec![state]; + self.records.push(data); + self + } + + pub fn data_multiple( + mut self, + kvs: impl IntoIterator, + ) -> Self { + self.extend(kvs); + self + } + + + pub async fn transact(self) -> anyhow::Result<()> { + let records = self.records; // NOTE: RpcSandboxPatchStateResponse is an empty struct with no fields, so don't do anything with it: let _patch_resp = self + .sandbox .client() .query(&RpcSandboxPatchStateRequest { records }) .await @@ -157,14 +217,179 @@ impl Sandbox { Ok(()) } +} - pub(crate) async fn fast_forward(&self, delta_height: u64) -> anyhow::Result<()> { - // NOTE: RpcSandboxFastForwardResponse is an empty struct with no fields, so don't do anything with it: - self.client() - // TODO: replace this with the `query` variant when RpcSandboxFastForwardRequest impls Debug - .query_nolog(&RpcSandboxFastForwardRequest { delta_height }) +impl<'s> std::iter::Extend<(&'s [u8], &'s [u8])> for PatchStateTransaction<'s>{ + fn extend>(&mut self, iter: T) { + let Self { + ref mut records, + ref account_id, + .. + } = self; + records.extend(iter.into_iter().map(|(key, value)| StateRecord::Data { + account_id: account_id.clone(), + data_key: key.to_vec(), + value: value.to_vec(), + })); + } +} + +#[must_use = "don't forget to .apply() this `SandboxPatchStateAccountBuilder`"] +pub struct PatchStateAccountTransaction<'s> { + sandbox: &'s Sandbox, + account_id: AccountId, + amount: Option, + locked: Option, + code_hash: Option, + storage_usage: Option, +} + +impl<'s> PatchStateAccountTransaction<'s> { + pub const fn new(sandbox: &'s Sandbox, account_id: AccountId) -> Self { + Self { + sandbox, + account_id, + amount: None, + locked: None, + code_hash: None, + storage_usage: None, + } + } + + pub const fn amount(mut self, amount: Balance) -> Self { + self.amount = Some(amount); + self + } + + pub const fn locked(mut self, locked: Balance) -> Self { + self.locked = Some(locked); + self + } + + pub const fn code_hash(mut self, code_hash: CryptoHash) -> Self { + self.code_hash = Some(code_hash); + self + } + + pub const fn storage_usage(mut self, storage_usage: StorageUsage) -> Self { + self.storage_usage = Some(storage_usage); + self + } + + pub async fn apply(self) -> anyhow::Result<()> { + let account_view = self + .sandbox + .client() + .view_account(self.account_id.clone(), None); + + let AccountView { + amount: previous_amount, + locked: previous_locked, + code_hash: previous_code_hash, + storage_usage: previous_storage_usage, + .. + } = account_view .await - .map_err(|err| anyhow::anyhow!("Failed to fast forward: {:?}", err))?; + .map_err(|err| anyhow::anyhow!("Failed to read account: {:?}", err))?; + + let account = StateRecord::Account { + account_id: self.account_id.clone(), + account: near_primitives::account::Account::new( + self.amount.unwrap_or(previous_amount), + self.locked.unwrap_or(previous_locked), + self.code_hash.unwrap_or(previous_code_hash), + self.storage_usage.unwrap_or(previous_storage_usage), + ), + }; + + let records = vec![account]; + + // NOTE: RpcSandboxPatchStateResponse is an empty struct with no fields, so don't do anything with it: + let _patch_resp = self + .sandbox + .client() + .query(&RpcSandboxPatchStateRequest { records }) + .await + .map_err(|err| anyhow::anyhow!("Failed to patch state: {:?}", err))?; + + Ok(()) + } +} + +#[must_use = "don't forget to .apply() this `SandboxPatchStateAccountBuilder`"] +pub struct PatchAccessKeyTransaction<'s> { + sandbox: &'s Sandbox, + account_id: AccountId, + public_key: crate::types::PublicKey, + nonce: Nonce, +} + +impl<'s> PatchAccessKeyTransaction<'s> { + pub const fn new( + sandbox: &'s Sandbox, + account_id: AccountId, + public_key: crate::types::PublicKey, + ) -> Self { + Self { + sandbox, + account_id, + public_key, + nonce: 0, + } + } + + pub const fn nonce(mut self, nonce: Nonce) -> Self { + self.nonce = nonce; + self + } + + pub async fn full_access(self) -> anyhow::Result<()> { + let mut access_key = near_primitives::account::AccessKey::full_access(); + access_key.nonce = self.nonce; + let access_key = StateRecord::AccessKey { + account_id: self.account_id, + public_key: self.public_key.into(), + access_key, + }; + + let records = vec![access_key]; + + // NOTE: RpcSandboxPatchStateResponse is an empty struct with no fields, so don't do anything with it: + let _patch_resp = self + .sandbox + .client() + .query(&RpcSandboxPatchStateRequest { records }) + .await + .map_err(|err| anyhow::anyhow!("Failed to patch state: {:?}", err))?; + + Ok(()) + } + + pub async fn function_call_access( + self, + receiver_id: &AccountId, + method_names: &[&str], + allowance: Option, + ) -> anyhow::Result<()> { + let mut access_key: near_primitives::account::AccessKey = + crate::types::AccessKey::function_call_access(receiver_id, method_names, allowance) + .into(); + access_key.nonce = self.nonce; + let access_key = StateRecord::AccessKey { + account_id: self.account_id, + public_key: self.public_key.into(), + access_key, + }; + + let records = vec![access_key]; + + // NOTE: RpcSandboxPatchStateResponse is an empty struct with no fields, so don't do anything with it: + let _patch_resp = self + .sandbox + .client() + .query(&RpcSandboxPatchStateRequest { records }) + .await + .map_err(|err| anyhow::anyhow!("Failed to patch state: {:?}", err))?; Ok(()) } diff --git a/workspaces/src/types/mod.rs b/workspaces/src/types/mod.rs index 5f1e06ac..815e2772 100644 --- a/workspaces/src/types/mod.rs +++ b/workspaces/src/types/mod.rs @@ -28,6 +28,8 @@ pub type Balance = u128; /// Height of a specific block pub type BlockHeight = u64; +/// StorageUsage is used to count the amount of storage used by a contract. +pub type StorageUsage = u64; /// Key types supported for either a [`SecretKey`] or [`PublicKey`] #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[non_exhaustive] diff --git a/workspaces/src/worker/impls.rs b/workspaces/src/worker/impls.rs index 62478f2b..200c1e47 100644 --- a/workspaces/src/worker/impls.rs +++ b/workspaces/src/worker/impls.rs @@ -1,9 +1,12 @@ -use crate::network::{AllowDevAccountCreation, NetworkClient, NetworkInfo, TopLevelAccountCreator}; -use crate::network::{Info, Sandbox}; +use crate::network::{ + AllowDevAccountCreation, NetworkClient, NetworkInfo, PatchAccessKeyTransaction, + TopLevelAccountCreator, +}; +use crate::network::{Info, Sandbox, PatchStateAccountTransaction, PatchStateTransaction}; use crate::result::{CallExecution, CallExecutionDetails, ViewResultDetails}; use crate::rpc::client::{Client, DEFAULT_CALL_DEPOSIT, DEFAULT_CALL_FN_GAS}; use crate::rpc::patch::ImportContractTransaction; -use crate::types::{AccountId, Gas, InMemorySigner, SecretKey}; +use crate::types::{AccountId, Gas, InMemorySigner, PublicKey, SecretKey}; use crate::worker::Worker; use crate::{Account, Block, Contract}; use crate::{AccountDetails, Network}; @@ -174,6 +177,18 @@ impl Worker { self.workspace.import_contract(id, worker) } + /// Patch state into the sandbox network, using builder pattern. This will allow us to set + /// state that we have acquired in some manner. This allows us to test random cases that + /// are hard to come up naturally as state evolves. + pub fn patch_state_builder(&self, account_id: &AccountId) -> PatchStateTransaction { + self.workspace.patch_state(account_id.clone()) + } + + /// Patch account state using builder pattern + pub fn patch_account(&self, account_id: &AccountId) -> PatchStateAccountTransaction { + self.workspace.patch_account(account_id.clone()) + } + /// Patch state into the sandbox network, given a key and value. This will allow us to set /// state that we have acquired in some manner. This allows us to test random cases that /// are hard to come up naturally as state evolves. @@ -183,7 +198,35 @@ impl Worker { key: &[u8], value: &[u8], ) -> anyhow::Result<()> { - self.workspace.patch_state(contract_id, key, value).await + self.workspace + .patch_state(contract_id.clone()) + .data(key, value) + .transact() + .await?; + Ok(()) + } + + /// Patch state into the sandbox network. Same as `patch_state` but accepts a sequence of key value pairs + pub async fn patch_state_multiple<'s>( + &'s self, + account_id: &AccountId, + kvs: impl IntoIterator, + ) -> anyhow::Result<()> { + self.workspace + .patch_state(account_id.clone()) + .data_multiple(kvs) + .transact() + .await?; + Ok(()) + } + + pub fn patch_access_key( + &self, + account_id: &AccountId, + public_key: &PublicKey, + ) -> PatchAccessKeyTransaction { + self.workspace + .patch_access_key(account_id.clone(), public_key.clone()) } /// Fast forward to a point in the future. The delta block height is supplied to tell the diff --git a/workspaces/tests/patch_state.rs b/workspaces/tests/patch_state.rs index 1c46c745..d870201c 100644 --- a/workspaces/tests/patch_state.rs +++ b/workspaces/tests/patch_state.rs @@ -21,20 +21,20 @@ struct StatusMessage { } async fn view_status_state( - worker: Worker, + worker: &Worker, ) -> anyhow::Result<(AccountId, StatusMessage)> { let wasm = std::fs::read(STATUS_MSG_WASM_FILEPATH)?; let contract = worker.dev_deploy(&wasm).await.unwrap(); contract - .call(&worker, "set_status") + .call(worker, "set_status") .args_json(json!({ "message": "hello", }))? .transact() .await?; - let mut state_items = contract.view_state(&worker, None).await?; + let mut state_items = contract.view_state(worker, None).await?; let state = state_items .remove(b"STATE".as_slice()) .ok_or_else(|| anyhow::anyhow!("Could not retrieve STATE"))?; @@ -46,7 +46,7 @@ async fn view_status_state( #[test(tokio::test)] async fn test_view_state() -> anyhow::Result<()> { let worker = workspaces::sandbox().await?; - let (contract_id, status_msg) = view_status_state(worker).await?; + let (contract_id, status_msg) = view_status_state(&worker).await?; assert_eq!( status_msg, @@ -64,14 +64,14 @@ async fn test_view_state() -> anyhow::Result<()> { #[test(tokio::test)] async fn test_patch_state() -> anyhow::Result<()> { let worker = workspaces::sandbox().await?; - let (contract_id, mut status_msg) = view_status_state(worker.clone()).await?; + let (contract_id, mut status_msg) = view_status_state(&worker).await?; status_msg.records.push(Record { k: "alice.near".to_string(), v: "hello world".to_string(), }); worker - .patch_state(&contract_id, "STATE".as_bytes(), &status_msg.try_to_vec()?) + .patch_state(&contract_id, b"STATE", &status_msg.try_to_vec()?) .await?; let status: String = worker @@ -91,3 +91,18 @@ async fn test_patch_state() -> anyhow::Result<()> { Ok(()) } + +#[test(tokio::test)] +#[ignore] +async fn patch_state_builder() -> anyhow::Result<()> { + let worker = workspaces::sandbox().await?; + let id: AccountId = "nino.near".parse()?; + worker + .patch_account(&id) + .amount(1) + .locked(0) + .apply() + .await?; + + Ok(()) +}