unc_workspaces/
operations.rs

1//! All operation types that are generated/used when making transactions or view calls.
2
3use crate::error::{ErrorKind, RpcErrorCode};
4use crate::result::{Execution, ExecutionFinalResult, Result, ViewResultDetails};
5use crate::rpc::client::{
6    send_batch_tx_and_retry, send_batch_tx_async_and_retry, DEFAULT_CALL_DEPOSIT,
7    DEFAULT_CALL_FN_GAS,
8};
9use crate::rpc::query::{Query, ViewFunction};
10use crate::types::{
11    AccessKey, AccountId, Gas, InMemorySigner, KeyType, UncToken, PublicKey, SecretKey,
12};
13use crate::worker::Worker;
14use crate::{Account, CryptoHash, Network};
15
16use unc_account_id::ParseAccountError;
17use unc_gas::UncGas;
18use unc_jsonrpc_client::errors::{JsonRpcError, JsonRpcServerError};
19use unc_jsonrpc_client::methods::tx::RpcTransactionError;
20use unc_primitives::borsh;
21use unc_primitives::transaction::{
22    Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction,
23    DeployContractAction, FunctionCallAction, PledgeAction, TransferAction,
24};
25use unc_primitives::views::FinalExecutionOutcomeView;
26use std::convert::TryInto;
27use std::fmt;
28use std::future::IntoFuture;
29use std::pin::Pin;
30use std::task::Poll;
31
32const MAX_GAS: UncGas = UncGas::from_tgas(300);
33
34/// A set of arguments we can provide to a transaction, containing
35/// the function name, arguments, the amount of gas to use and deposit.
36#[derive(Debug)]
37pub struct Function {
38    pub(crate) name: String,
39    pub(crate) args: Result<Vec<u8>>,
40    pub(crate) deposit: UncToken,
41    pub(crate) gas: Gas,
42}
43
44impl Function {
45    /// Initialize a new instance of [`Function`], tied to a specific function on a
46    /// contract that lives directly on a contract we've specified in [`Transaction`].
47    pub fn new(name: &str) -> Self {
48        Self {
49            name: name.into(),
50            args: Ok(vec![]),
51            deposit: DEFAULT_CALL_DEPOSIT,
52            gas: DEFAULT_CALL_FN_GAS,
53        }
54    }
55
56    /// Provide the arguments for the call. These args are serialized bytes from either
57    /// a JSON or Borsh serializable set of arguments. To use the more specific versions
58    /// with better quality of life, use `args_json` or `args_borsh`.
59    pub fn args(mut self, args: Vec<u8>) -> Self {
60        if self.args.is_err() {
61            return self;
62        }
63        self.args = Ok(args);
64        self
65    }
66
67    /// Similar to `args`, specify an argument that is JSON serializable and can be
68    /// accepted by the equivalent contract. Recommend to use something like
69    /// `serde_json::json!` macro to easily serialize the arguments.
70    pub fn args_json<U: serde::Serialize>(mut self, args: U) -> Self {
71        match serde_json::to_vec(&args) {
72            Ok(args) => self.args = Ok(args),
73            Err(e) => self.args = Err(ErrorKind::DataConversion.custom(e)),
74        }
75        self
76    }
77
78    /// Similar to `args`, specify an argument that is borsh serializable and can be
79    /// accepted by the equivalent contract.
80    pub fn args_borsh<U: borsh::BorshSerialize>(mut self, args: U) -> Self {
81        match borsh::to_vec(&args) {
82            Ok(args) => self.args = Ok(args),
83            Err(e) => self.args = Err(ErrorKind::DataConversion.custom(e)),
84        }
85        self
86    }
87
88    /// Specify the amount of tokens to be deposited where `deposit` is the amount of
89    /// tokens in atto unc.
90    pub fn deposit(mut self, deposit: UncToken) -> Self {
91        self.deposit = deposit;
92        self
93    }
94
95    /// Specify the amount of gas to be used.
96    pub fn gas(mut self, gas: Gas) -> Self {
97        self.gas = gas;
98        self
99    }
100
101    /// Use the maximum amount of gas possible to perform this function call into the contract.
102    pub fn max_gas(self) -> Self {
103        self.gas(MAX_GAS)
104    }
105}
106
107/// A builder-like object that will allow specifying various actions to be performed
108/// in a single transaction. For details on each of the actions, find them in
109/// [UNC transactions](https://docs.unc.org/docs/concepts/transaction).
110///
111/// All actions are performed on the account specified by `receiver_id`. This object
112/// is most commonly constructed from [`Account::batch`] or [`Contract::batch`],
113/// where `receiver_id` is specified in the `Account::batch` while `Contract::id()`
114/// is used by default for `Contract::batch`.
115///
116/// [`Contract::batch`]: crate::Contract::batch
117pub struct Transaction {
118    worker: Worker<dyn Network>,
119    signer: InMemorySigner,
120    receiver_id: AccountId,
121    // Result used to defer errors in argument parsing to later when calling into transact
122    actions: Result<Vec<Action>>,
123}
124
125impl Transaction {
126    pub(crate) fn new(
127        worker: Worker<dyn Network>,
128        signer: InMemorySigner,
129        receiver_id: AccountId,
130    ) -> Self {
131        Self {
132            worker,
133            signer,
134            receiver_id,
135            actions: Ok(Vec::new()),
136        }
137    }
138
139    /// Adds a key to the `receiver_id`'s account, where the public key can be used
140    /// later to delete the same key.
141    pub fn add_key(mut self, pk: PublicKey, ak: AccessKey) -> Self {
142        if let Ok(actions) = &mut self.actions {
143            actions.push(
144                AddKeyAction {
145                    public_key: pk.into(),
146                    access_key: ak.into(),
147                }
148                .into(),
149            );
150        }
151
152        self
153    }
154
155    /// Call into the `receiver_id`'s contract with the specific function arguments.
156    pub fn call(mut self, function: Function) -> Self {
157        let args = match function.args {
158            Ok(args) => args,
159            Err(err) => {
160                self.actions = Err(err);
161                return self;
162            }
163        };
164
165        if let Ok(actions) = &mut self.actions {
166            actions.push(Action::FunctionCall(Box::new(FunctionCallAction {
167                method_name: function.name.to_string(),
168                args,
169                deposit: function.deposit.as_attounc(),
170                gas: function.gas.as_gas(),
171            })));
172        }
173
174        self
175    }
176
177    /// Create a new account with the account id being `receiver_id`.
178    pub fn create_account(mut self) -> Self {
179        if let Ok(actions) = &mut self.actions {
180            actions.push(CreateAccountAction {}.into());
181        }
182        self
183    }
184
185    /// Deletes the `receiver_id`'s account. The beneficiary specified by
186    /// `beneficiary_id` will receive the funds of the account deleted.
187    pub fn delete_account(mut self, beneficiary_id: &AccountId) -> Self {
188        if let Ok(actions) = &mut self.actions {
189            actions.push(
190                DeleteAccountAction {
191                    beneficiary_id: beneficiary_id.clone(),
192                }
193                .into(),
194            );
195        }
196        self
197    }
198
199    /// Deletes a key from the `receiver_id`'s account, where the public key is
200    /// associated with the access key to be deleted.
201    pub fn delete_key(mut self, pk: PublicKey) -> Self {
202        if let Ok(actions) = &mut self.actions {
203            actions.push(DeleteKeyAction { public_key: pk.0 }.into());
204        }
205        self
206    }
207
208    /// Deploy contract code or WASM bytes to the `receiver_id`'s account.
209    pub fn deploy(mut self, code: &[u8]) -> Self {
210        if let Ok(actions) = &mut self.actions {
211            actions.push(DeployContractAction { code: code.into() }.into());
212        }
213        self
214    }
215
216    /// An action which stakes the signer's tokens and setups a validator public key.
217    pub fn stake(mut self, stake: UncToken, pk: PublicKey) -> Self {
218        if let Ok(actions) = &mut self.actions {
219            actions.push(
220                PledgeAction {
221                    pledge: stake.as_attounc(),
222                    public_key: pk.0,
223                }
224                .into(),
225            );
226        }
227        self
228    }
229
230    /// Transfer `deposit` amount from `signer`'s account into `receiver_id`'s account.
231    pub fn transfer(mut self, deposit: UncToken) -> Self {
232        if let Ok(actions) = &mut self.actions {
233            actions.push(
234                TransferAction {
235                    deposit: deposit.as_attounc(),
236                }
237                .into(),
238            );
239        }
240        self
241    }
242
243    async fn transact_raw(self) -> Result<FinalExecutionOutcomeView> {
244        let view = send_batch_tx_and_retry(
245            self.worker.client(),
246            &self.signer,
247            &self.receiver_id,
248            self.actions?,
249        )
250        .await?;
251
252        if !self.worker.tx_callbacks.is_empty() {
253            let total_gas_burnt = view.transaction_outcome.outcome.gas_burnt
254                + view
255                    .receipts_outcome
256                    .iter()
257                    .map(|t| t.outcome.gas_burnt)
258                    .sum::<u64>();
259
260            for callback in self.worker.tx_callbacks {
261                callback(Gas::from_gas(total_gas_burnt))?;
262            }
263        }
264
265        Ok(view)
266    }
267
268    /// Process the transaction, and return the result of the execution.
269    pub async fn transact(self) -> Result<ExecutionFinalResult> {
270        self.transact_raw()
271            .await
272            .map(ExecutionFinalResult::from_view)
273            .map_err(crate::error::Error::from)
274    }
275
276    /// Send the transaction to the network to be processed. This will be done asynchronously
277    /// without waiting for the transaction to complete. This returns us a [`TransactionStatus`]
278    /// for which we can call into [`status`] and/or `.await` to retrieve info about whether
279    /// the transaction has been completed or not. Note that `.await` will wait till completion
280    /// of the transaction.
281    ///
282    /// [`status`]: TransactionStatus::status
283    pub async fn transact_async(self) -> Result<TransactionStatus> {
284        send_batch_tx_async_and_retry(self.worker, &self.signer, &self.receiver_id, self.actions?)
285            .await
286    }
287}
288
289/// Similar to a [`Transaction`], but more specific to making a call into a contract.
290/// Note, only one call can be made per `CallTransaction`.
291pub struct CallTransaction {
292    worker: Worker<dyn Network>,
293    signer: InMemorySigner,
294    contract_id: AccountId,
295    function: Function,
296}
297
298impl CallTransaction {
299    pub(crate) fn new(
300        worker: Worker<dyn Network>,
301        contract_id: AccountId,
302        signer: InMemorySigner,
303        function: &str,
304    ) -> Self {
305        Self {
306            worker,
307            signer,
308            contract_id,
309            function: Function::new(function),
310        }
311    }
312
313    /// Provide the arguments for the call. These args are serialized bytes from either
314    /// a JSON or Borsh serializable set of arguments. To use the more specific versions
315    /// with better quality of life, use `args_json` or `args_borsh`.
316    pub fn args(mut self, args: Vec<u8>) -> Self {
317        self.function = self.function.args(args);
318        self
319    }
320
321    /// Similar to `args`, specify an argument that is JSON serializable and can be
322    /// accepted by the equivalent contract. Recommend to use something like
323    /// `serde_json::json!` macro to easily serialize the arguments.
324    pub fn args_json<U: serde::Serialize>(mut self, args: U) -> Self {
325        self.function = self.function.args_json(args);
326        self
327    }
328
329    /// Similar to `args`, specify an argument that is borsh serializable and can be
330    /// accepted by the equivalent contract.
331    pub fn args_borsh<U: borsh::BorshSerialize>(mut self, args: U) -> Self {
332        self.function = self.function.args_borsh(args);
333        self
334    }
335
336    /// Specify the amount of tokens to be deposited where `deposit` is the amount of
337    /// tokens in atto unc.
338    pub fn deposit(mut self, deposit: UncToken) -> Self {
339        self.function = self.function.deposit(deposit);
340        self
341    }
342
343    /// Specify the amount of gas to be used where `gas` is the amount of gas in atto unc.
344    pub fn gas(mut self, gas: UncGas) -> Self {
345        self.function = self.function.gas(gas);
346        self
347    }
348
349    /// Use the maximum amount of gas possible to perform this transaction.
350    pub fn max_gas(self) -> Self {
351        self.gas(MAX_GAS)
352    }
353
354    /// Finally, send the transaction to the network. This will consume the `CallTransaction`
355    /// object and return us the execution details, along with any errors if the transaction
356    /// failed in any process along the way.
357    pub async fn transact(self) -> Result<ExecutionFinalResult> {
358        let txn = self
359            .worker
360            .client()
361            .call(
362                &self.signer,
363                &self.contract_id,
364                self.function.name.to_string(),
365                self.function.args?,
366                self.function.gas.as_gas(),
367                self.function.deposit,
368            )
369            .await
370            .map(ExecutionFinalResult::from_view)
371            .map_err(crate::error::Error::from)?;
372
373        for callback in self.worker.tx_callbacks.iter() {
374            callback(txn.total_gas_burnt)?;
375        }
376        Ok(txn)
377    }
378
379    /// Send the transaction to the network to be processed. This will be done asynchronously
380    /// without waiting for the transaction to complete. This returns us a [`TransactionStatus`]
381    /// for which we can call into [`status`] and/or `.await` to retrieve info about whether
382    /// the transaction has been completed or not. Note that `.await` will wait till completion
383    /// of the transaction.
384    ///
385    /// [`status`]: TransactionStatus::status
386    pub async fn transact_async(self) -> Result<TransactionStatus> {
387        send_batch_tx_async_and_retry(
388            self.worker,
389            &self.signer,
390            &self.contract_id,
391            vec![FunctionCallAction {
392                args: self.function.args?,
393                method_name: self.function.name,
394                gas: self.function.gas.as_gas(),
395                deposit: self.function.deposit.as_attounc(),
396            }
397            .into()],
398        )
399        .await
400    }
401
402    /// Instead of transacting the transaction, call into the specified view function.
403    pub async fn view(self) -> Result<ViewResultDetails> {
404        Query::new(
405            self.worker.client(),
406            ViewFunction {
407                account_id: self.contract_id.clone(),
408                function: self.function,
409            },
410        )
411        .await
412    }
413}
414
415/// Similar to a [`Transaction`], but more specific to creating an account.
416/// This transaction will create a new account with the specified `receiver_id`
417pub struct CreateAccountTransaction<'a, 'b> {
418    worker: &'a Worker<dyn Network>,
419    signer: InMemorySigner,
420    parent_id: AccountId,
421    new_account_id: &'b str,
422
423    initial_balance: UncToken,
424    secret_key: Option<SecretKey>,
425}
426
427impl<'a, 'b> CreateAccountTransaction<'a, 'b> {
428    pub(crate) fn new(
429        worker: &'a Worker<dyn Network>,
430        signer: InMemorySigner,
431        parent_id: AccountId,
432        new_account_id: &'b str,
433    ) -> Self {
434        Self {
435            worker,
436            signer,
437            parent_id,
438            new_account_id,
439            initial_balance: UncToken::from_attounc(100000000000000000000000u128),
440            secret_key: None,
441        }
442    }
443
444    /// Specifies the initial balance of the new account. Amount directly taken out
445    /// from the caller/signer of this transaction.
446    pub fn initial_balance(mut self, initial_balance: UncToken) -> Self {
447        self.initial_balance = initial_balance;
448        self
449    }
450
451    /// Set the secret key of the new account.
452    pub fn keys(mut self, secret_key: SecretKey) -> Self {
453        self.secret_key = Some(secret_key);
454        self
455    }
456
457    /// Send the transaction to the network. This will consume the `CreateAccountTransaction`
458    /// and give us back the details of the execution and finally the new [`Account`] object.
459    pub async fn transact(self) -> Result<Execution<Account>> {
460        let sk = self
461            .secret_key
462            .unwrap_or_else(|| SecretKey::from_seed(KeyType::ED25519, "subaccount.seed"));
463        let id: AccountId = format!("{}.{}", self.new_account_id, self.parent_id)
464            .try_into()
465            .map_err(|e: ParseAccountError| ErrorKind::DataConversion.custom(e))?;
466
467        let outcome = self
468            .worker
469            .client()
470            .create_account(&self.signer, &id, sk.public_key(), self.initial_balance)
471            .await?;
472
473        let signer = InMemorySigner::from_secret_key(id, sk);
474        let account = Account::new(signer, self.worker.clone());
475        let details = ExecutionFinalResult::from_view(outcome);
476
477        for callback in self.worker.tx_callbacks.iter() {
478            callback(details.total_gas_burnt)?;
479        }
480
481        Ok(Execution {
482            result: account,
483            details,
484        })
485    }
486}
487
488/// `TransactionStatus` object relating to an [`asynchronous transaction`] on the network.
489/// Used to query into the status of the Transaction for whether it has completed or not.
490///
491/// [`asynchronous transaction`]: https://docs.unc.org/api/rpc/transactions#send-transaction-async
492#[must_use]
493pub struct TransactionStatus {
494    worker: Worker<dyn Network>,
495    sender_id: AccountId,
496    hash: CryptoHash,
497}
498
499impl TransactionStatus {
500    pub(crate) fn new(
501        worker: Worker<dyn Network>,
502        id: AccountId,
503        hash: unc_primitives::hash::CryptoHash,
504    ) -> Self {
505        Self {
506            worker,
507            sender_id: id,
508            hash: CryptoHash(hash.0),
509        }
510    }
511
512    /// Checks the status of the transaction. If an `Err` is returned, then the transaction
513    /// is in an unexpected state. The error should have further context. Otherwise, if an
514    /// `Ok` value with [`Poll::Pending`] is returned, then the transaction has not finished.
515    pub async fn status(&self) -> Result<Poll<ExecutionFinalResult>> {
516        let result = self
517            .worker
518            .client()
519            .tx_async_status(
520                &self.sender_id,
521                unc_primitives::hash::CryptoHash(self.hash.0),
522            )
523            .await
524            .map(ExecutionFinalResult::from_view);
525
526        match result {
527            Ok(result) => Ok(Poll::Ready(result)),
528            Err(err) => match err {
529                JsonRpcError::ServerError(JsonRpcServerError::HandlerError(
530                    RpcTransactionError::UnknownTransaction { .. },
531                )) => Ok(Poll::Pending),
532                other => Err(RpcErrorCode::BroadcastTxFailure.custom(other)),
533            },
534        }
535    }
536
537    /// Wait until the completion of the transaction by polling [`TransactionStatus::status`].
538    pub(crate) async fn wait(self) -> Result<ExecutionFinalResult> {
539        loop {
540            match self.status().await? {
541                Poll::Ready(val) => break Ok(val),
542                Poll::Pending => (),
543            }
544
545            tokio::time::sleep(std::time::Duration::from_millis(300)).await;
546        }
547    }
548
549    /// Get the [`AccountId`] of the account that initiated this transaction.
550    pub fn sender_id(&self) -> &AccountId {
551        &self.sender_id
552    }
553
554    /// Reference [`CryptoHash`] to the submitted transaction, pending completion.
555    pub fn hash(&self) -> &CryptoHash {
556        &self.hash
557    }
558}
559
560impl fmt::Debug for TransactionStatus {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        f.debug_struct("TransactionStatus")
563            .field("sender_id", &self.sender_id)
564            .field("hash", &self.hash)
565            .finish()
566    }
567}
568
569impl IntoFuture for TransactionStatus {
570    type Output = Result<ExecutionFinalResult>;
571    type IntoFuture = Pin<Box<dyn std::future::Future<Output = Self::Output>>>;
572
573    fn into_future(self) -> Self::IntoFuture {
574        Box::pin(async { self.wait().await })
575    }
576}