unc_workspaces/rpc/
patch.rs

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