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.as_yoctonear(),
172                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: stake.as_yoctonear(),
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(
236                TransferAction {
237                    deposit: deposit.as_yoctonear(),
238                }
239                .into(),
240            );
241        }
242        self
243    }
244
245    async fn transact_raw(self) -> Result<FinalExecutionOutcomeView> {
246        let view = send_batch_tx_and_retry(
247            self.worker.client(),
248            &self.signer,
249            &self.receiver_id,
250            self.actions?,
251        )
252        .await?;
253
254        if !self.worker.tx_callbacks.is_empty() {
255            let total_gas_burnt = view.transaction_outcome.outcome.gas_burnt
256                + view
257                    .receipts_outcome
258                    .iter()
259                    .map(|t| t.outcome.gas_burnt)
260                    .sum::<u64>();
261
262            for callback in self.worker.tx_callbacks {
263                callback(Gas::from_gas(total_gas_burnt))?;
264            }
265        }
266
267        Ok(view)
268    }
269
270    /// Process the transaction, and return the result of the execution.
271    pub async fn transact(self) -> Result<ExecutionFinalResult> {
272        self.transact_raw()
273            .await
274            .map(ExecutionFinalResult::from_view)
275    }
276
277    /// Send the transaction to the network to be processed. This will be done asynchronously
278    /// without waiting for the transaction to complete. This returns us a [`TransactionStatus`]
279    /// for which we can call into [`status`] and/or `.await` to retrieve info about whether
280    /// the transaction has been completed or not. Note that `.await` will wait till completion
281    /// of the transaction.
282    ///
283    /// [`status`]: TransactionStatus::status
284    pub async fn transact_async(self) -> Result<TransactionStatus> {
285        send_batch_tx_async_and_retry(self.worker, &self.signer, &self.receiver_id, self.actions?)
286            .await
287    }
288}
289
290/// Similar to a [`Transaction`], but more specific to making a call into a contract.
291/// Note, only one call can be made per `CallTransaction`.
292pub struct CallTransaction {
293    worker: Worker<dyn Network>,
294    signer: InMemorySigner,
295    contract_id: AccountId,
296    function: Function,
297}
298
299impl CallTransaction {
300    pub(crate) fn new(
301        worker: Worker<dyn Network>,
302        contract_id: AccountId,
303        signer: InMemorySigner,
304        function: &str,
305    ) -> Self {
306        Self {
307            worker,
308            signer,
309            contract_id,
310            function: Function::new(function),
311        }
312    }
313
314    /// Provide the arguments for the call. These args are serialized bytes from either
315    /// a JSON or Borsh serializable set of arguments. To use the more specific versions
316    /// with better quality of life, use `args_json` or `args_borsh`.
317    pub fn args(mut self, args: Vec<u8>) -> Self {
318        self.function = self.function.args(args);
319        self
320    }
321
322    /// Similar to `args`, specify an argument that is JSON serializable and can be
323    /// accepted by the equivalent contract. Recommend to use something like
324    /// `serde_json::json!` macro to easily serialize the arguments.
325    pub fn args_json<U: serde::Serialize>(mut self, args: U) -> Self {
326        self.function = self.function.args_json(args);
327        self
328    }
329
330    /// Similar to `args`, specify an argument that is borsh serializable and can be
331    /// accepted by the equivalent contract.
332    pub fn args_borsh<U: borsh::BorshSerialize>(mut self, args: U) -> Self {
333        self.function = self.function.args_borsh(args);
334        self
335    }
336
337    /// Specify the amount of tokens to be deposited where `deposit` is the amount of
338    /// tokens in yocto near.
339    pub fn deposit(mut self, deposit: NearToken) -> Self {
340        self.function = self.function.deposit(deposit);
341        self
342    }
343
344    /// Specify the amount of gas to be used where `gas` is the amount of gas in yocto near.
345    pub fn gas(mut self, gas: NearGas) -> Self {
346        self.function = self.function.gas(gas);
347        self
348    }
349
350    /// Use the maximum amount of gas possible to perform this transaction.
351    pub fn max_gas(self) -> Self {
352        self.gas(MAX_GAS)
353    }
354
355    /// Finally, send the transaction to the network. This will consume the `CallTransaction`
356    /// object and return us the execution details, along with any errors if the transaction
357    /// failed in any process along the way.
358    pub async fn transact(self) -> Result<ExecutionFinalResult> {
359        let txn = self
360            .worker
361            .client()
362            .call(
363                &self.signer,
364                &self.contract_id,
365                self.function.name.to_string(),
366                self.function.args?,
367                self.function.gas.as_gas(),
368                self.function.deposit,
369            )
370            .await
371            .map(ExecutionFinalResult::from_view)?;
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_yoctonear(),
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: NearToken,
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: NearToken::from_yoctonear(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: NearToken) -> 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.near.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: near_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 rpc_resp = self
517            .worker
518            .client()
519            .tx_async_status(
520                &self.sender_id,
521                near_primitives::hash::CryptoHash(self.hash.0),
522                TxExecutionStatus::Included,
523            )
524            .await;
525
526        let rpc_resp = match rpc_resp {
527            Ok(rpc_resp) => rpc_resp,
528            Err(err) => match err {
529                JsonRpcError::ServerError(JsonRpcServerError::HandlerError(
530                    RpcTransactionError::UnknownTransaction { .. },
531                )) => return Ok(Poll::Pending),
532                other => return Err(RpcErrorCode::BroadcastTxFailure.custom(other)),
533            },
534        };
535
536        if matches!(rpc_resp.final_execution_status, TxExecutionStatus::Included) {
537            return Ok(Poll::Pending);
538        }
539
540        let Some(final_outcome) = rpc_resp.final_execution_outcome else {
541            // final execution outcome is not available yet.
542            return Ok(Poll::Pending);
543        };
544
545        let outcome = final_outcome.into_outcome();
546
547        match outcome.status {
548            near_primitives::views::FinalExecutionStatus::NotStarted => return Ok(Poll::Pending),
549            near_primitives::views::FinalExecutionStatus::Started => return Ok(Poll::Pending),
550            _ => (),
551        }
552
553        Ok(Poll::Ready(ExecutionFinalResult::from_view(outcome)))
554    }
555
556    /// Wait until the completion of the transaction by polling [`TransactionStatus::status`].
557    pub(crate) async fn wait(self) -> Result<ExecutionFinalResult> {
558        loop {
559            match self.status().await? {
560                Poll::Ready(val) => break Ok(val),
561                Poll::Pending => (),
562            }
563
564            tokio::time::sleep(std::time::Duration::from_millis(300)).await;
565        }
566    }
567
568    /// Get the [`AccountId`] of the account that initiated this transaction.
569    pub fn sender_id(&self) -> &AccountId {
570        &self.sender_id
571    }
572
573    /// Reference [`CryptoHash`] to the submitted transaction, pending completion.
574    pub fn hash(&self) -> &CryptoHash {
575        &self.hash
576    }
577}
578
579impl fmt::Debug for TransactionStatus {
580    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581        f.debug_struct("TransactionStatus")
582            .field("sender_id", &self.sender_id)
583            .field("hash", &self.hash)
584            .finish()
585    }
586}
587
588impl IntoFuture for TransactionStatus {
589    type Output = Result<ExecutionFinalResult>;
590    type IntoFuture = Pin<Box<dyn std::future::Future<Output = Self::Output>>>;
591
592    fn into_future(self) -> Self::IntoFuture {
593        Box::pin(async { self.wait().await })
594    }
595}