casper_client/cli/
transaction.rs

1use crate::cli::deploy::do_withdraw_amount_checks;
2use crate::cli::{get_block, query_global_state};
3#[cfg(feature = "std-fs-io")]
4use crate::read_transaction_file;
5#[cfg(feature = "std-fs-io")]
6use crate::rpcs::v2_0_0::speculative_exec_transaction::SpeculativeExecTxnResult;
7#[cfg(feature = "std-fs-io")]
8use crate::speculative_exec_txn;
9use crate::{
10    cli::{parse, CliError, TransactionBuilderParams, TransactionStrParams, TransactionV1Builder},
11    put_transaction as put_transaction_rpc_handler,
12    rpcs::results::PutTransactionResult,
13    SuccessResponse,
14};
15use casper_types::{
16    Digest, InitiatorAddr, Key, PublicKey, SecretKey, Transaction, TransactionArgs,
17    TransactionEntryPoint, TransactionInvocationTarget, TransactionRuntimeParams,
18    TransactionTarget, U512,
19};
20
21pub fn create_transaction(
22    builder_params: TransactionBuilderParams,
23    transaction_params: TransactionStrParams,
24    allow_unsigned_transaction: bool,
25) -> Result<Transaction, CliError> {
26    let chain_name = transaction_params.chain_name.to_string();
27
28    let maybe_secret_key = get_maybe_secret_key(
29        transaction_params.secret_key,
30        allow_unsigned_transaction,
31        "create_transaction",
32    )?;
33
34    let timestamp = parse::timestamp(transaction_params.timestamp)?;
35    let ttl = parse::ttl(transaction_params.ttl)?;
36    let maybe_session_account = parse::session_account(&transaction_params.initiator_addr)?;
37
38    let is_v2_wasm = matches!(&builder_params, TransactionBuilderParams::Session { runtime, .. } if matches!(runtime, &TransactionRuntimeParams::VmCasperV2 { .. }));
39
40    let mut transaction_builder = make_transaction_builder(builder_params)?;
41
42    transaction_builder = transaction_builder
43        .with_timestamp(timestamp)
44        .with_ttl(ttl)
45        .with_chain_name(chain_name);
46
47    if transaction_params.pricing_mode.is_empty() {
48        return Err(CliError::InvalidArgument {
49            context: "create_transaction (pricing_mode)",
50            error: "pricing_mode is required to be non empty".to_string(),
51        });
52    }
53
54    let pricing_mode = if transaction_params.pricing_mode.to_lowercase().as_str() == "reserved" {
55        let digest = Digest::from_hex(transaction_params.receipt).map_err(|error| {
56            CliError::FailedToParseDigest {
57                context: "pricing_digest",
58                error,
59            }
60        })?;
61
62        parse::pricing_mode(
63            transaction_params.pricing_mode,
64            transaction_params.payment_amount,
65            transaction_params.gas_price_tolerance,
66            transaction_params.additional_computation_factor,
67            transaction_params.standard_payment,
68            Some(digest),
69        )?
70    } else {
71        parse::pricing_mode(
72            transaction_params.pricing_mode,
73            transaction_params.payment_amount,
74            transaction_params.gas_price_tolerance,
75            transaction_params.additional_computation_factor,
76            transaction_params.standard_payment,
77            None,
78        )?
79    };
80
81    transaction_builder = transaction_builder.with_pricing_mode(pricing_mode);
82
83    let maybe_json_args = parse::args_json::session::parse(transaction_params.session_args_json)?;
84    let maybe_simple_args =
85        parse::arg_simple::session::parse(&transaction_params.session_args_simple)?;
86    let chunked = transaction_params.chunked_args;
87
88    let args = parse::args_from_simple_or_json(maybe_simple_args, maybe_json_args, chunked);
89    match args {
90        TransactionArgs::Named(named_args) => {
91            if !named_args.is_empty() {
92                transaction_builder = transaction_builder.with_runtime_args(named_args);
93            }
94        }
95        TransactionArgs::Bytesrepr(chunked_args) => {
96            transaction_builder = transaction_builder.with_chunked_args(chunked_args);
97        }
98    }
99
100    if is_v2_wasm {
101        if let Some(entry_point) = transaction_params.session_entry_point {
102            transaction_builder = transaction_builder
103                .with_entry_point(TransactionEntryPoint::Custom(entry_point.to_owned()));
104        }
105    }
106
107    if let Some(secret_key) = &maybe_secret_key {
108        transaction_builder = transaction_builder.with_secret_key(secret_key);
109    }
110
111    if let Some(account) = maybe_session_account {
112        transaction_builder =
113            transaction_builder.with_initiator_addr(InitiatorAddr::PublicKey(account));
114    }
115
116    let txn = transaction_builder.build().map_err(crate::Error::from)?;
117    Ok(Transaction::V1(txn))
118}
119
120/// Creates a [`Transaction`] and outputs it to a file or stdout if the `std-fs-io` feature is enabled.
121///
122/// As a file, the `Transaction` can subsequently be signed by other parties using [`sign_transaction_file`]
123/// and then sent to the network for execution using [`send_transaction_file`].
124///
125/// If the `std-fs-io` feature is NOT enabled, `maybe_output_path` and `force` are ignored.
126/// Otherwise, `maybe_output_path` specifies the output file path, or if empty, will print it to
127/// `stdout`.  If `force` is true, and a file exists at `maybe_output_path`, it will be
128/// overwritten.  If `force` is false and a file exists at `maybe_output_path`,
129/// [`crate::Error::FileAlreadyExists`] is returned and the file will not be written.
130pub fn make_transaction(
131    builder_params: TransactionBuilderParams,
132    transaction_params: TransactionStrParams<'_>,
133    #[allow(unused_variables)] force: bool,
134) -> Result<Transaction, CliError> {
135    let transaction = create_transaction(builder_params, transaction_params.clone(), true)?;
136    #[cfg(feature = "std-fs-io")]
137    {
138        let output = parse::output_kind(transaction_params.output_path, force);
139        crate::output_transaction(output, &transaction).map_err(CliError::from)?;
140    }
141    Ok(transaction)
142}
143
144/// Creates a [`Transaction`] and sends it to the network for execution.
145///
146/// `rpc_id_str` is the RPC ID to use for this request.
147/// `node_address` is the address of the node to send the request to.
148/// `verbosity_level` is the level of verbosity to use when outputting the response.
149pub async fn put_transaction(
150    rpc_id_str: &str,
151    node_address: &str,
152    verbosity_level: u64,
153    builder_params: TransactionBuilderParams<'_>,
154    transaction_params: TransactionStrParams<'_>,
155) -> Result<SuccessResponse<PutTransactionResult>, CliError> {
156    let rpc_id = parse::rpc_id(rpc_id_str);
157    let verbosity_level = parse::verbosity(verbosity_level);
158    let min_bid_override = transaction_params.min_bid_override;
159    let transaction = create_transaction(builder_params, transaction_params, false)?;
160    if let Err(err) =
161        check_auction_state_for_withdraw(node_address, 0, min_bid_override, &transaction).await
162    {
163        if !min_bid_override {
164            return Err(err);
165        } else {
166            println!("[WARN] Skipping withdraw amount checks {}", err)
167        }
168    };
169    put_transaction_rpc_handler(rpc_id, node_address, verbosity_level, transaction)
170        .await
171        .map_err(CliError::from)
172}
173///
174/// Reads a previously-saved [`TransactionV1`] from a file and sends it to the network for execution.
175///
176/// `rpc_id_str` is the RPC ID to use for this request. node_address is the address of the node to send the request to.
177/// verbosity_level is the level of verbosity to use when outputting the response.
178/// the input path is the path to the file containing the transaction to send.
179#[cfg(feature = "std-fs-io")]
180pub async fn send_transaction_file(
181    rpc_id_str: &str,
182    node_address: &str,
183    verbosity_level: u64,
184    input_path: &str,
185) -> Result<SuccessResponse<PutTransactionResult>, CliError> {
186    let rpc_id = parse::rpc_id(rpc_id_str);
187    let verbosity_level = parse::verbosity(verbosity_level);
188    let transaction = read_transaction_file(input_path)?;
189    put_transaction_rpc_handler(rpc_id, node_address, verbosity_level, transaction)
190        .await
191        .map_err(CliError::from)
192}
193
194///
195/// Reads a previously-saved [`TransactionV1`] from a file and sends it to the network for execution.
196///
197/// `rpc_id_str` is the RPC ID to use for this request. node_address is the address of the node to send the request to.
198/// verbosity_level is the level of verbosity to use when outputting the response.
199///  the input path is the path to the file containing the transaction to send.
200#[cfg(feature = "std-fs-io")]
201pub async fn speculative_send_transaction_file(
202    rpc_id_str: &str,
203    node_address: &str,
204    verbosity_level: u64,
205    input_path: &str,
206) -> Result<SuccessResponse<SpeculativeExecTxnResult>, CliError> {
207    let rpc_id = parse::rpc_id(rpc_id_str);
208    let verbosity_level = parse::verbosity(verbosity_level);
209    let transaction = read_transaction_file(input_path).unwrap();
210    speculative_exec_txn(rpc_id, node_address, verbosity_level, transaction)
211        .await
212        .map_err(CliError::from)
213}
214
215/// Reads a previously-saved [`TransactionV1`] from a file, cryptographically signs it, and outputs it to a
216/// file or stdout.
217///
218/// `maybe_output_path` specifies the output file path, or if empty, will print it to `stdout`.  If
219/// `force` is true, and a file exists at `maybe_output_path`, it will be overwritten.  If `force`
220/// is false and a file exists at `maybe_output_path`, [`crate::Error::FileAlreadyExists`] is returned
221/// and the file will not be written.
222#[cfg(feature = "std-fs-io")]
223pub fn sign_transaction_file(
224    input_path: &str,
225    secret_key_path: &str,
226    maybe_output_path: Option<&str>,
227    force: bool,
228) -> Result<(), CliError> {
229    let output = parse::output_kind(maybe_output_path.unwrap_or(""), force);
230    let secret_key = parse::secret_key_from_file(secret_key_path)?;
231    crate::sign_transaction_file(input_path, &secret_key, output).map_err(CliError::from)
232}
233
234pub fn make_transaction_builder(
235    transaction_builder_params: TransactionBuilderParams,
236) -> Result<TransactionV1Builder, CliError> {
237    match transaction_builder_params {
238        TransactionBuilderParams::AddBid {
239            public_key,
240            delegation_rate,
241            amount,
242            minimum_delegation_amount,
243            maximum_delegation_amount,
244            reserved_slots,
245        } => {
246            let transaction_builder = TransactionV1Builder::new_add_bid(
247                public_key,
248                delegation_rate,
249                amount,
250                minimum_delegation_amount,
251                maximum_delegation_amount,
252                reserved_slots,
253            )?;
254            Ok(transaction_builder)
255        }
256        TransactionBuilderParams::Delegate {
257            delegator,
258            validator,
259            amount,
260        } => {
261            let transaction_builder =
262                TransactionV1Builder::new_delegate(delegator, validator, amount)?;
263            Ok(transaction_builder)
264        }
265        TransactionBuilderParams::Undelegate {
266            delegator,
267            validator,
268            amount,
269        } => {
270            let transaction_builder =
271                TransactionV1Builder::new_undelegate(delegator, validator, amount)?;
272            Ok(transaction_builder)
273        }
274        TransactionBuilderParams::Redelegate {
275            delegator,
276            validator,
277            amount,
278            new_validator,
279        } => {
280            let transaction_builder =
281                TransactionV1Builder::new_redelegate(delegator, validator, amount, new_validator)?;
282            Ok(transaction_builder)
283        }
284        TransactionBuilderParams::InvocableEntity {
285            entity_hash,
286            entry_point,
287            runtime,
288        } => {
289            let transaction_builder = TransactionV1Builder::new_targeting_invocable_entity(
290                entity_hash,
291                entry_point,
292                runtime,
293            );
294            Ok(transaction_builder)
295        }
296        TransactionBuilderParams::InvocableEntityAlias {
297            entity_alias,
298            entry_point,
299            runtime,
300        } => {
301            let transaction_builder =
302                TransactionV1Builder::new_targeting_invocable_entity_via_alias(
303                    entity_alias,
304                    entry_point,
305                    runtime,
306                );
307            Ok(transaction_builder)
308        }
309        TransactionBuilderParams::Package {
310            package_hash,
311            maybe_entity_version,
312            entry_point,
313            runtime,
314        } => {
315            let transaction_builder = TransactionV1Builder::new_targeting_package(
316                package_hash,
317                maybe_entity_version,
318                entry_point,
319                runtime,
320            );
321            Ok(transaction_builder)
322        }
323        TransactionBuilderParams::PackageWithMajorVersion {
324            package_hash,
325            maybe_entity_version,
326            entry_point,
327            runtime,
328            major_protocol_version,
329        } => {
330            let transaction_builder = TransactionV1Builder::new_targeting_package_with_version_key(
331                package_hash,
332                maybe_entity_version,
333                major_protocol_version,
334                entry_point,
335                runtime,
336            );
337            Ok(transaction_builder)
338        }
339        TransactionBuilderParams::PackageAlias {
340            package_alias,
341            maybe_entity_version,
342            entry_point,
343            runtime,
344        } => {
345            let new_targeting_package_via_alias =
346                TransactionV1Builder::new_targeting_package_via_alias(
347                    package_alias,
348                    maybe_entity_version,
349                    entry_point,
350                    runtime,
351                );
352            let transaction_builder = new_targeting_package_via_alias;
353            Ok(transaction_builder)
354        }
355        TransactionBuilderParams::PackageAliasWithMajorVersion {
356            package_alias,
357            maybe_entity_version,
358            entry_point,
359            runtime,
360            major_protocol_version,
361        } => {
362            let new_targeting_package_via_alias =
363                TransactionV1Builder::new_targeting_package_via_alias_with_version_key(
364                    package_alias,
365                    maybe_entity_version,
366                    major_protocol_version,
367                    entry_point,
368                    runtime,
369                );
370            let transaction_builder = new_targeting_package_via_alias;
371            Ok(transaction_builder)
372        }
373        TransactionBuilderParams::Session {
374            is_install_upgrade,
375            transaction_bytes,
376            runtime,
377        } => {
378            let transaction_builder =
379                TransactionV1Builder::new_session(is_install_upgrade, transaction_bytes, runtime);
380            Ok(transaction_builder)
381        }
382        TransactionBuilderParams::Transfer {
383            maybe_source,
384            target,
385            amount,
386            maybe_id,
387        } => {
388            let transaction_builder =
389                TransactionV1Builder::new_transfer(amount, maybe_source, target, maybe_id)?;
390
391            Ok(transaction_builder)
392        }
393        TransactionBuilderParams::WithdrawBid {
394            public_key, amount, ..
395        } => {
396            let transaction_builder = TransactionV1Builder::new_withdraw_bid(public_key, amount)?;
397            Ok(transaction_builder)
398        }
399        TransactionBuilderParams::ActivateBid { validator } => {
400            let transaction_builder = TransactionV1Builder::new_activate_bid(validator)?;
401            Ok(transaction_builder)
402        }
403        TransactionBuilderParams::ChangeBidPublicKey {
404            public_key,
405            new_public_key,
406        } => {
407            let transaction_builder =
408                TransactionV1Builder::new_change_bid_public_key(public_key, new_public_key)?;
409            Ok(transaction_builder)
410        }
411        TransactionBuilderParams::AddReservations { reservations } => {
412            let transaction_builder = TransactionV1Builder::new_add_reservations(reservations)?;
413            Ok(transaction_builder)
414        }
415        TransactionBuilderParams::CancelReservations {
416            validator,
417            delegators,
418        } => {
419            let transaction_builder =
420                TransactionV1Builder::new_cancel_reservations(validator, delegators)?;
421            Ok(transaction_builder)
422        }
423    }
424}
425
426/// Retrieves a `SecretKey` based on the provided secret key string and configuration options.
427///
428/// * `secret_key` - A string representing the secret key. This can result in three outcomes:
429///     - If a valid secret key is provided and the `std-fs-io` feature is enabled, the `Result` contains `Some(SecretKey)`.
430///     - If `secret_key` is empty and `allow_unsigned_deploy` is `true`, the `Result` contains `None`.
431///     - If `secret_key` is empty and `allow_unsigned_deploy` is `false`, the `Result` contains an `Err` variant with `CliError::InvalidArgument`.
432/// * `allow_unsigned_deploy` - A boolean indicating whether unsigned deploys are allowed.
433///
434/// # Returns
435///
436/// Returns a `Result` containing an `Option<SecretKey>`.
437/// * If a valid secret key is provided and the `std-fs-io` feature is enabled, the `Result` contains `Some(SecretKey)`.
438/// * If the `std-fs-io` feature is disabled, the `Result` contains `Some(SecretKey)` parsed from the provided file.
439/// * If `secret_key` is empty and `allow_unsigned_deploy` is `true`, the `Result` contains `None`.
440/// * If `secret_key` is empty and `allow_unsigned_deploy` is `false`, an `Err` variant with `CliError::InvalidArgument` is returned.
441///
442/// # Errors
443///
444/// Returns an `Err` variant with a `CliError::Core` or `CliError::InvalidArgument` if there are issues with parsing the secret key.
445pub fn get_maybe_secret_key(
446    secret_key: &str,
447    allow_unsigned_deploy: bool,
448    context: &'static str,
449) -> Result<Option<SecretKey>, CliError> {
450    match (secret_key.is_empty(), allow_unsigned_deploy) {
451        (false, _) => {
452            #[cfg(feature = "std-fs-io")]
453            {
454                Ok(Some(parse::secret_key_from_file(secret_key)?))
455            }
456            #[cfg(not(feature = "std-fs-io"))]
457            {
458                let secret_key = SecretKey::from_pem(secret_key).map_err(|error| {
459                    CliError::Core(crate::Error::CryptoError { context, error })
460                })?;
461                Ok(Some(secret_key))
462            }
463        }
464        (true, true) => Ok(None),
465        (true, false) => Err(CliError::InvalidArgument {
466            context,
467            error: "No secret key provided and unsigned deploys are not allowed".to_string(),
468        }),
469    }
470}
471
472async fn check_auction_state_for_withdraw(
473    node_address: &str,
474    verbosity_level: u64,
475    min_bid_override: bool,
476    transaction: &Transaction,
477) -> Result<(), CliError> {
478    let state_root_hash = *get_block("", node_address, 0, "")
479        .await?
480        .result
481        .block_with_signatures
482        .ok_or_else(|| CliError::FailedToGetStateRootHash)?
483        .block
484        .state_root_hash();
485    let encoded_hash = base16::encode_lower(&state_root_hash);
486    match transaction {
487        Transaction::Deploy(_) => return Ok(()),
488        Transaction::V1(transaction_v1) => {
489            let entry_point = transaction_v1
490                .deserialize_field::<TransactionEntryPoint>(2)
491                .map_err(|err| {
492                    CliError::FailedToParseTransactionPayloadField(format!("{:?}", err))
493                })?;
494            let do_amount_checks = match entry_point {
495                TransactionEntryPoint::WithdrawBid => true,
496                TransactionEntryPoint::Custom(name) => {
497                    if *"withdraw_bid" != name {
498                        // Entry point is not withdraw bid exiting
499                        return Ok(());
500                    }
501                    let transaction_invocation_target = transaction_v1
502                        .payload()
503                        .deserialize_field::<TransactionTarget>(1)
504                        .map_err(|err| {
505                            CliError::FailedToParseTransactionPayloadField(format!("{:?}", err))
506                        })?;
507                    let registry = crate::cli::get_system_hash_registry(
508                        node_address,
509                        verbosity_level,
510                        &encoded_hash,
511                    )
512                    .await?;
513                    let auction_hash_addr = *registry
514                        .get("auction")
515                        .ok_or_else(|| CliError::MissingAuctionHash)?;
516
517                    let do_amount_checks = if let TransactionTarget::Stored { id, .. } =
518                        transaction_invocation_target
519                    {
520                        match id {
521                            TransactionInvocationTarget::ByHash(hash) => hash == auction_hash_addr,
522                            TransactionInvocationTarget::ByName(name) => {
523                                let base_key =
524                                    Key::Account(transaction_v1.initiator_addr().account_hash());
525                                let account = query_global_state(
526                                    "",
527                                    node_address,
528                                    0,
529                                    "",
530                                    &encoded_hash,
531                                    &base_key.to_formatted_string(),
532                                    "",
533                                )
534                                .await?
535                                .result
536                                .stored_value
537                                .into_account()
538                                .ok_or_else(|| CliError::UnexpectedStoredValue)?;
539                                let key = account.named_keys().get(&name);
540                                match key {
541                                    Some(key) => match *key {
542                                        Key::Hash(addr) => addr == auction_hash_addr,
543                                        Key::AddressableEntity(addr) => {
544                                            addr.value() == auction_hash_addr
545                                        }
546                                        _ => false,
547                                    },
548                                    None => false,
549                                }
550                            }
551                            TransactionInvocationTarget::ByPackageHash { addr, .. } => {
552                                let key = Key::Hash(auction_hash_addr);
553                                let package_addr = query_global_state(
554                                    "",
555                                    node_address,
556                                    verbosity_level,
557                                    "",
558                                    &encoded_hash,
559                                    &key.to_formatted_string(),
560                                    "",
561                                )
562                                .await?
563                                .result
564                                .stored_value
565                                .as_contract()
566                                .ok_or_else(|| CliError::FailedToGetSystemHashRegistry)?
567                                .contract_package_hash()
568                                .value();
569                                package_addr == addr
570                            }
571                            TransactionInvocationTarget::ByPackageName { name, .. } => {
572                                let base_key =
573                                    Key::Account(transaction_v1.initiator_addr().account_hash());
574                                let account = query_global_state(
575                                    "",
576                                    node_address,
577                                    0,
578                                    "",
579                                    &encoded_hash,
580                                    &base_key.to_formatted_string(),
581                                    "",
582                                )
583                                .await?
584                                .result
585                                .stored_value
586                                .into_account()
587                                .ok_or_else(|| CliError::UnexpectedStoredValue)?;
588                                let key = account.named_keys().get(&name);
589                                match key {
590                                    Some(key) => match *key {
591                                        Key::Hash(addr) => {
592                                            let key = Key::Hash(auction_hash_addr);
593                                            let package_addr = query_global_state(
594                                                "",
595                                                node_address,
596                                                verbosity_level,
597                                                "",
598                                                &encoded_hash,
599                                                &key.to_formatted_string(),
600                                                "",
601                                            )
602                                            .await?
603                                            .result
604                                            .stored_value
605                                            .as_contract()
606                                            .ok_or_else(|| CliError::FailedToGetSystemHashRegistry)?
607                                            .contract_package_hash()
608                                            .value();
609                                            addr == package_addr
610                                        }
611                                        Key::SmartContract(addr) => {
612                                            let key = Key::Hash(auction_hash_addr);
613                                            let package_addr = query_global_state(
614                                                "",
615                                                node_address,
616                                                verbosity_level,
617                                                "",
618                                                &encoded_hash,
619                                                &key.to_formatted_string(),
620                                                "",
621                                            )
622                                            .await?
623                                            .result
624                                            .stored_value
625                                            .as_contract()
626                                            .ok_or_else(|| CliError::FailedToGetSystemHashRegistry)?
627                                            .contract_package_hash()
628                                            .value();
629                                            addr == package_addr
630                                        }
631                                        _ => false,
632                                    },
633                                    None => false,
634                                }
635                            }
636                        }
637                    } else {
638                        false
639                    };
640
641                    do_amount_checks
642                }
643                _ => false,
644            };
645            if do_amount_checks {
646                let args = transaction_v1
647                    .payload()
648                    .deserialize_field::<TransactionArgs>(0)
649                    .map_err(|err| {
650                        CliError::FailedToParseTransactionPayloadField(format!("{:?}", err))
651                    })?;
652                if let Some(named_args) = args.into_named() {
653                    let amount = named_args
654                        .get("amount")
655                        .ok_or_else(|| {
656                            CliError::InvalidCLValue("failed to get amount arg".to_string())
657                        })?
658                        .to_t::<U512>()
659                        .map_err(|err| CliError::InvalidCLValue(err.to_string()))?;
660
661                    let public_key = named_args
662                        .get("public_key")
663                        .ok_or_else(|| {
664                            CliError::InvalidCLValue("failed to get public key arg".to_string())
665                        })?
666                        .to_t::<PublicKey>()
667                        .map_err(|err| CliError::InvalidCLValue(err.to_string()))?;
668
669                    do_withdraw_amount_checks(node_address, 0, public_key, amount, min_bid_override)
670                        .await?
671                }
672            } else {
673                println!("Skipping amount checks for withdraw bid")
674            }
675        }
676    }
677    Ok(())
678}