1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
use near_jsonrpc_client::methods::sandbox_patch_state::RpcSandboxPatchStateRequest;
use near_primitives::state_record::StateRecord;
use near_primitives::types::{BlockId, BlockReference};
use near_token::NearToken;
use crate::error::SandboxErrorCode;
use crate::network::{Sandbox, DEV_ACCOUNT_SEED};
use crate::types::account::AccountDetails;
use crate::types::{BlockHeight, KeyType, PublicKey, SecretKey};
use crate::{AccessKey, AccountDetailsPatch, Result};
use crate::{AccountId, Contract, CryptoHash, InMemorySigner, Network, Worker};
/// A [`Transaction`]-like object that allows us to specify details about importing
/// a contract from a different network into our sandbox local network. This creates
/// a new [`Transaction`] to be committed to the sandbox network once `transact()`
/// has been called. This does not commit any new transactions from the network
/// this object is importing from.
///
/// [`Transaction`]: crate::operations::Transaction
pub struct ImportContractTransaction<'a> {
account_id: &'a AccountId,
from_network: Worker<dyn Network>,
into_network: Worker<Sandbox>,
/// Whether to grab data down from the other contract or not
import_data: bool,
/// Initial balance of the account. If None, uses what is specified
/// from the other account instead.
initial_balance: Option<NearToken>,
block_ref: Option<BlockReference>,
/// AccountId if specified, will be the destination account to clone the contract to.
into_account_id: Option<AccountId>,
}
impl<'a> ImportContractTransaction<'a> {
pub(crate) fn new(
account_id: &'a AccountId,
from_network: Worker<dyn Network>,
into_network: Worker<Sandbox>,
) -> Self {
ImportContractTransaction {
account_id,
from_network,
into_network,
import_data: false,
initial_balance: None,
block_ref: None,
into_account_id: None,
}
}
/// Specify at which block height to import the contract from. This is usable with
/// any network this object is importing from, but be aware that only archival
/// networks will have the full history while networks like mainnet or testnet
/// only has the history from 5 or less epochs ago.
pub fn block_height(mut self, block_height: BlockHeight) -> Self {
self.block_ref = Some(BlockId::Height(block_height).into());
self
}
/// Specify at which block hash to import the contract from. This is usable with
/// any network this object is importing from, but be aware that only archival
/// networks will have the full history while networks like mainnet or testnet
/// only has the history from 5 or less epochs ago.
pub fn block_hash(mut self, block_hash: CryptoHash) -> Self {
self.block_ref =
Some(BlockId::Hash(near_primitives::hash::CryptoHash(block_hash.0)).into());
self
}
/// Along with importing the contract code, this will import the state from the
/// contract itself. This is useful for testing current network state or state
/// at a specific block. Note that there is a limit of 50kb of state data that
/// can be pulled down using the usual RPC service. To get beyond this, our own
/// RPC node has to be spun up and used instead.
pub fn with_data(mut self) -> Self {
self.import_data = true;
self
}
/// Specifies the balance of the contract. This will override the balance currently
/// on the network this transaction is importing from.
pub fn initial_balance(mut self, initial_balance: NearToken) -> Self {
self.initial_balance = Some(initial_balance);
self
}
/// Sets the destination [`AccountId`] where the import will be transacted to.
/// This function is provided so users can import to a different [`AccountId`]
/// than the one initially provided to import from.
pub fn dest_account_id(mut self, account_id: &AccountId) -> Self {
self.into_account_id = Some(account_id.clone());
self
}
/// Process the transaction, and return the result of the execution.
pub async fn transact(self) -> Result<Contract> {
let from_account_id = self.account_id;
let into_account_id = self.into_account_id.as_ref().unwrap_or(from_account_id);
let sk = SecretKey::from_seed(KeyType::ED25519, DEV_ACCOUNT_SEED);
let pk = sk.public_key();
let signer = InMemorySigner::from_secret_key(into_account_id.clone(), sk);
let block_ref = self.block_ref.unwrap_or_else(BlockReference::latest);
let mut account_view = self
.from_network
.view_account(from_account_id)
.block_reference(block_ref.clone())
.await?;
let code_hash = account_view.code_hash;
if let Some(initial_balance) = self.initial_balance {
account_view.balance = initial_balance;
}
let mut patch = PatchTransaction::new(&self.into_network, into_account_id.clone())
.account(account_view.into())
.access_key(pk, AccessKey::full_access());
if code_hash != CryptoHash::default() {
let code = self
.from_network
.view_code(from_account_id)
.block_reference(block_ref.clone())
.await?;
patch = patch.code(&code);
}
if self.import_data {
let states = self
.from_network
.view_state(from_account_id)
.block_reference(block_ref)
.await?;
patch = patch.states(
states
.iter()
.map(|(key, value)| (key.as_slice(), value.as_slice())),
);
}
patch.transact().await?;
Ok(Contract::new(signer, self.into_network.coerce()))
}
}
/// Internal enum for determining whether to update the account on chain
/// or to patch an entire account.
enum AccountUpdate {
Update(AccountDetailsPatch),
FromCurrent(Box<dyn Fn(AccountDetails) -> AccountDetailsPatch + Send>),
}
pub struct PatchTransaction {
account_id: AccountId,
records: Vec<StateRecord>,
worker: Worker<Sandbox>,
account_updates: Vec<AccountUpdate>,
code_hash_update: Option<CryptoHash>,
}
impl PatchTransaction {
pub(crate) fn new(worker: &Worker<Sandbox>, account_id: AccountId) -> Self {
Self {
account_id,
records: vec![],
worker: worker.clone(),
account_updates: vec![],
code_hash_update: None,
}
}
/// Patch and overwrite the info contained inside an [`crate::Account`] in sandbox.
pub fn account(mut self, account: AccountDetailsPatch) -> Self {
self.account_updates.push(AccountUpdate::Update(account));
self
}
/// Patch and overwrite the info contained inside an [`crate::Account`] in sandbox. This
/// will allow us to fetch the current details on the chain and allow us to update
/// the account details w.r.t to them.
pub fn account_from_current<F>(mut self, f: F) -> Self
where
F: Fn(AccountDetails) -> AccountDetailsPatch + Send + 'static,
{
self.account_updates
.push(AccountUpdate::FromCurrent(Box::new(f)));
self
}
/// Patch the access keys of an account. This will add or overwrite the current access key
/// contained in sandbox with the access key we specify.
pub fn access_key(mut self, pk: PublicKey, ak: AccessKey) -> Self {
self.records.push(StateRecord::AccessKey {
account_id: self.account_id.clone(),
public_key: pk.into(),
access_key: ak.into(),
});
self
}
/// Patch the access keys of an account. This will add or overwrite the current access keys
/// contained in sandbox with a list of access keys we specify.
///
/// Similar to [`PatchTransaction::access_key`], but allows us to specify multiple access keys
pub fn access_keys<I>(mut self, access_keys: I) -> Self
where
I: IntoIterator<Item = (PublicKey, AccessKey)>,
{
// Move account_id out of self struct so we can appease borrow checker.
// We'll put it back in after we're done.
let account_id = self.account_id;
self.records.extend(
access_keys
.into_iter()
.map(|(pk, ak)| StateRecord::AccessKey {
account_id: account_id.clone(),
public_key: pk.into(),
access_key: ak.into(),
}),
);
self.account_id = account_id;
self
}
/// Sets the code for this account. This will overwrite the current code contained in the account.
/// Note that if a patch for [`Self::account`] or [`Self::account_from_current`] is specified, the code hash
/// in those will be overwritten with the code hash of the code we specify here.
pub fn code(mut self, wasm_bytes: &[u8]) -> Self {
self.code_hash_update = Some(CryptoHash::hash_bytes(wasm_bytes));
self.records.push(StateRecord::Contract {
account_id: self.account_id.clone(),
code: wasm_bytes.to_vec(),
});
self
}
/// Patch state into the sandbox network, given a prefix key and value. This will allow us
/// to set contract state that we have acquired in some manner, where we are able to test
/// random cases that are hard to come up naturally as state evolves.
pub fn state(mut self, key: &[u8], value: &[u8]) -> Self {
self.records.push(StateRecord::Data {
account_id: self.account_id.clone(),
data_key: key.to_vec().into(),
value: value.to_vec().into(),
});
self
}
/// Patch a series of states into the sandbox network. Similar to [`PatchTransaction::state`],
/// but allows us to specify multiple state patches at once.
pub fn states<'b, 'c, I>(mut self, states: I) -> Self
where
I: IntoIterator<Item = (&'b [u8], &'c [u8])>,
{
// Move account_id out of self struct so we can appease borrow checker.
// We'll put it back in after we're done.
let account_id = self.account_id;
self.records
.extend(states.into_iter().map(|(key, value)| StateRecord::Data {
account_id: account_id.clone(),
data_key: key.to_vec().into(),
value: value.to_vec().into(),
}));
self.account_id = account_id;
self
}
/// Perform the state patch transaction into the sandbox network.
pub async fn transact(mut self) -> Result<()> {
// NOTE: updating the account is done here because we need to fetch the current
// account details from the chain. This is an async operation so it is deferred
// till the transact function.
let account_patch = if !self.account_updates.is_empty() {
let mut account = AccountDetailsPatch::default();
for update in self.account_updates {
// reduce the updates into a single account details patch
account.reduce(match update {
AccountUpdate::Update(account) => account,
AccountUpdate::FromCurrent(f) => {
let account = self.worker.view_account(&self.account_id).await?;
f(account)
}
});
}
// Update the code hash if the user supplied a code patch.
if let Some(code_hash) = self.code_hash_update.take() {
account.code_hash = Some(code_hash);
}
Some(account)
} else if let Some(code_hash) = self.code_hash_update {
// No account patch, but we have a code patch. We need to fetch the current account
// to reflect the code hash change.
let mut account = self.worker.view_account(&self.account_id).await?;
account.code_hash = code_hash;
Some(account.into())
} else {
None
};
// Account patch should be the first entry in the records, since the account might not
// exist yet and the consequent patches might lookup the account on the chain.
let records = if let Some(account) = account_patch {
let account: AccountDetails = account.into();
let mut records = vec![StateRecord::Account {
account_id: self.account_id.clone(),
account: account.into_near_account(),
}];
records.extend(self.records);
records
} else {
self.records
};
self.worker
.client()
.query(&RpcSandboxPatchStateRequest {
records: records.clone(),
})
.await
.map_err(|err| SandboxErrorCode::PatchStateFailure.custom(err))?;
self.worker
.client()
.query(&RpcSandboxPatchStateRequest { records })
.await
.map_err(|err| SandboxErrorCode::PatchStateFailure.custom(err))?;
Ok(())
}
}