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