near_workspaces/rpc/
patch.rs

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