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