use color_eyre::eyre::{Context, ContextCompat};
use inquire::CustomType;
use interactive_clap::ToCliArgs;
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
#[interactive_clap(input_context = crate::GlobalContext)]
#[interactive_clap(output_context = TransactionInfoContext)]
pub struct TransactionInfo {
transaction_hash: crate::types::crypto_hash::CryptoHash,
#[interactive_clap(named_arg)]
network_config: crate::network::Network,
}
#[derive(Clone)]
pub struct TransactionInfoContext(crate::network::NetworkContext);
impl TransactionInfoContext {
pub fn from_previous_context(
previous_context: crate::GlobalContext,
scope: &<TransactionInfo as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
) -> color_eyre::eyre::Result<Self> {
use super::construct_transaction::{
CliConstructTransaction, CliDirectReceiver, CliReceiverMode, add_action_1, skip_action,
};
use super::{CliTransactionActions, CliTransactionCommands};
let on_after_getting_network_callback: crate::network::OnAfterGettingNetworkCallback =
std::sync::Arc::new({
let tx_hash: near_primitives::hash::CryptoHash = scope.transaction_hash.into();
move |network_config: &crate::config::NetworkConfig| {
let query_view_transaction_status = super::view_status::get_transaction_info(network_config, tx_hash)?
.final_execution_outcome
.wrap_err_with(|| {
format!(
"Failed to get the final execution outcome for the transaction {tx_hash}"
)
})?
.into_outcome();
let mut prepopulated_transaction = crate::commands::PrepopulatedTransaction {
signer_id: query_view_transaction_status.transaction.signer_id,
receiver_id: query_view_transaction_status.transaction.receiver_id,
actions: query_view_transaction_status
.transaction
.actions
.into_iter()
.map(near_primitives::transaction::Action::try_from)
.collect::<Result<Vec<near_primitives::transaction::Action>, _>>()
.expect("Internal error: can not convert the action_view to action."),
};
tracing::info!(
parent: &tracing::Span::none(),
"Transaction {}:{}",
query_view_transaction_status.transaction.hash,
crate::common::indent_payload(&crate::common::print_unsigned_transaction(
&prepopulated_transaction,
))
);
if prepopulated_transaction.actions.len() == 1
&& let near_primitives::transaction::Action::Delegate(
signed_delegate_action,
) = &prepopulated_transaction.actions[0]
{
prepopulated_transaction = crate::commands::PrepopulatedTransaction {
signer_id: signed_delegate_action.delegate_action.sender_id.clone(),
receiver_id: signed_delegate_action.delegate_action.receiver_id.clone(),
actions: signed_delegate_action.delegate_action.get_actions(),
};
}
let cmd =
crate::commands::CliTopLevelCommand::Transaction(CliTransactionCommands {
transaction_actions: Some(CliTransactionActions::ConstructTransaction(
CliConstructTransaction {
sender_account_id: Some(
prepopulated_transaction.signer_id.into(),
),
receiver: Some(CliReceiverMode::ReceiverId(
CliDirectReceiver {
receiver_account_id: Some(
prepopulated_transaction.receiver_id.clone().into(),
),
next_actions: None,
},
)),
},
)),
});
let mut cmd_cli_args = cmd.to_cli_args();
for transaction_action in prepopulated_transaction.actions {
let next_actions = add_action_1::CliNextAction::AddAction(
add_action_1::add_action::CliAddAction {
action: action_transformation(
transaction_action,
prepopulated_transaction.receiver_id.clone(),
network_config,
near_primitives::types::BlockReference::BlockId(
near_primitives::types::BlockId::Hash(
query_view_transaction_status
.transaction_outcome
.block_hash,
),
),
)?,
},
);
cmd_cli_args.extend(next_actions.to_cli_args());
}
let skip_action = add_action_1::CliNextAction::Skip(skip_action::CliSkipAction {
network_config: Some(
skip_action::ClapNamedArgNetworkForTransactionArgsForSkipAction::NetworkConfig(
crate::network_for_transaction::CliNetworkForTransactionArgs {
network_name: Some(network_config.network_name.clone()),
transaction_signature_options: None,
},
),
),
});
cmd_cli_args.extend(skip_action.to_cli_args());
let near_cli_exec_path = crate::common::get_near_exec_path();
if let crate::Verbosity::Interactive | crate::Verbosity::TeachMe =
previous_context.verbosity
{
eprintln!(
"Here is your console command to run archive transaction. You can to edit it or re-run (printed to stdout):"
)
}
println!(
"{}",
shell_words::join(std::iter::once(near_cli_exec_path).chain(cmd_cli_args))
);
Ok(())
}
});
Ok(Self(crate::network::NetworkContext {
config: previous_context.config,
interacting_with_account_ids: vec![],
on_after_getting_network_callback,
}))
}
}
impl From<TransactionInfoContext> for crate::network::NetworkContext {
fn from(item: TransactionInfoContext) -> Self {
item.0
}
}
fn action_transformation(
archival_action: near_primitives::transaction::Action,
receiver_id: near_primitives::types::AccountId,
network_config: &crate::config::NetworkConfig,
block_reference: near_primitives::types::BlockReference,
) -> color_eyre::eyre::Result<
Option<super::construct_transaction::add_action_1::add_action::CliActionSubcommand>,
> {
use near_primitives::transaction::Action;
use super::construct_transaction::add_action_1::add_action;
match archival_action {
Action::CreateAccount(_) => {
Ok(Some(add_action::CliActionSubcommand::CreateAccount(
add_action::create_account::CliCreateAccountAction {
next_action: None
}
)))
}
Action::DeleteAccount(delete_account_action) => {
Ok(Some(add_action::CliActionSubcommand::DeleteAccount(
add_action::delete_account::CliDeleteAccountAction {
beneficiary_id: Some(delete_account_action.beneficiary_id.into()),
next_action: None
}
)))
}
Action::AddKey(add_key_action) => {
Ok(Some(add_action::CliActionSubcommand::AddKey(
add_action::add_key::CliAddKeyAction {
permission: get_access_key_permission(add_key_action.public_key, add_key_action.access_key.permission)?
}
)))
}
Action::DeleteKey(delete_key_action) => {
Ok(Some(add_action::CliActionSubcommand::DeleteKey(
add_action::delete_key::CliDeleteKeyAction {
public_key: Some(delete_key_action.public_key.into()),
next_action: None
}
)))
}
Action::Transfer(transfer_action) => {
Ok(Some(add_action::CliActionSubcommand::Transfer(
add_action::transfer::CliTransferAction {
amount_in_near: Some(transfer_action.deposit.into()),
next_action: None
}
)))
}
Action::DeployContract(deploy_contract_action) => {
let file_path = CustomType::<crate::types::path_buf::PathBuf>::new("Enter the file path where to save the contract:")
.with_starting_input("reconstruct-transaction-deploy-code.wasm")
.prompt()?;
download_code(
&crate::commands::contract::download_wasm::ContractType::Regular(receiver_id),
network_config,
block_reference,
&file_path,
&near_primitives::hash::CryptoHash::hash_bytes(&deploy_contract_action.code)
)?;
Ok(Some(add_action::CliActionSubcommand::DeployContract(
add_action::deploy_contract::CliDeployContractAction {
use_file: Some(add_action::deploy_contract::ClapNamedArgContractFileForDeployContractAction::UseFile(
add_action::deploy_contract::CliContractFile {
file_path: Some(file_path),
initialize: Some(add_action::deploy_contract::initialize_mode::CliInitializeMode::WithoutInitCall(
add_action::deploy_contract::initialize_mode::CliNoInitialize {
next_action: None
}
))
}
)),
}
)))
}
Action::FunctionCall(function_call_action) => {
Ok(Some(add_action::CliActionSubcommand::FunctionCall(
add_action::call_function::CliFunctionCallAction {
function_name: Some(function_call_action.method_name),
function_args_type: Some(crate::commands::contract::call_function::call_function_args_type::FunctionArgsType::TextArgs),
function_args: Some(String::from_utf8(function_call_action.args)?),
prepaid_gas: Some(add_action::call_function::ClapNamedArgPrepaidGasForFunctionCallAction::PrepaidGas(
add_action::call_function::CliPrepaidGas {
gas: Some(near_gas::NearGas::from_gas(function_call_action.gas.as_gas())),
attached_deposit: Some(add_action::call_function::ClapNamedArgDepositForPrepaidGas::AttachedDeposit(
add_action::call_function::CliDeposit {
deposit: Some(function_call_action.deposit.into()),
next_action: None
}
))
}
))
}
)))
}
Action::Stake(stake_action) => {
Ok(Some(add_action::CliActionSubcommand::Stake(
add_action::stake::CliStakeAction {
stake_amount: Some(stake_action.stake.into()),
public_key: Some(stake_action.public_key.into()),
next_action: None
}
)))
}
Action::Delegate(_) => {
panic!("Internal error: Delegate action should have been handled before calling action_transformation.");
}
Action::DeployGlobalContract(action) => {
let file_path = CustomType::<crate::types::path_buf::PathBuf>::new("Enter the file path where to save the contract:")
.with_starting_input("reconstruct-transaction-deploy-code.wasm")
.prompt()?;
let code_hash = near_primitives::hash::CryptoHash::try_from(action.code.as_ref()).map_err(|_| {
color_eyre::Report::msg("Internal error: Failed to calculate code hash from the deploy global contract action code.".to_string())
})?;
let contract_type = match action.deploy_mode {
near_primitives::action::GlobalContractDeployMode::AccountId => {
&crate::commands::contract::download_wasm::ContractType::GlobalContractByAccountId {
account_id: receiver_id.clone(),
code_hash: Some(code_hash)
}
}
near_primitives::action::GlobalContractDeployMode::CodeHash => {
&crate::commands::contract::download_wasm::ContractType::GlobalContractByCodeHash(code_hash)
}
};
download_code(
contract_type,
network_config,
block_reference,
&file_path,
&code_hash
)?;
let mode = match action.deploy_mode {
near_primitives::action::GlobalContractDeployMode::AccountId => add_action::deploy_global_contract::CliDeployGlobalMode::AsGlobalAccountId(
add_action::deploy_global_contract::CliNextCommand {
next_action: None
}
),
near_primitives::action::GlobalContractDeployMode::CodeHash => add_action::deploy_global_contract::CliDeployGlobalMode::AsGlobalHash(
add_action::deploy_global_contract::CliNextCommand {
next_action: None
}
),
};
Ok(Some(add_action::CliActionSubcommand::DeployGlobalContract(
add_action::deploy_global_contract::CliDeployGlobalContractAction {
file_path: Some(file_path),
mode: Some(mode)
}
)))
}
Action::UseGlobalContract(use_global_contract_action) => {
let mode = match use_global_contract_action.contract_identifier {
near_primitives::action::GlobalContractIdentifier::CodeHash(hash) => add_action::use_global_contract::CliUseGlobalActionMode::UseGlobalHash(
add_action::use_global_contract::CliUseHashAction {
hash: Some(crate::types::crypto_hash::CryptoHash(hash)),
initialize: Some(add_action::deploy_contract::initialize_mode::CliInitializeMode::WithoutInitCall(
add_action::deploy_contract::initialize_mode::CliNoInitialize {
next_action: None
}
))
}
),
near_primitives::action::GlobalContractIdentifier::AccountId(account_id) => add_action::use_global_contract::CliUseGlobalActionMode::UseGlobalAccountId(
add_action::use_global_contract::CliUseAccountIdAction {
account_id: Some(crate::types::account_id::AccountId(account_id)),
initialize: Some(add_action::deploy_contract::initialize_mode::CliInitializeMode::WithoutInitCall(
add_action::deploy_contract::initialize_mode::CliNoInitialize {
next_action: None
}
))
}
),
};
Ok(Some(add_action::CliActionSubcommand::UseGlobalContract(
add_action::use_global_contract::CliUseGlobalContractAction {
mode: Some(mode)
}
)))
}
Action::DeterministicStateInit(_deterministic_state_action) => {
Err(color_eyre::eyre::eyre!("Deterministic state init is not yet implemented"))
}
Action::TransferToGasKey(_) | Action::WithdrawFromGasKey(_) => {
Err(color_eyre::eyre::eyre!("Gas key actions are not yet supported in transaction reconstruction"))
}
}
}
fn get_access_key_permission(
public_key: near_crypto::PublicKey,
access_key_permission: near_primitives::account::AccessKeyPermission,
) -> color_eyre::eyre::Result<
Option<super::construct_transaction::add_action_1::add_action::add_key::CliAccessKeyPermission>,
> {
use super::construct_transaction::add_action_1::add_action::add_key;
match access_key_permission {
near_primitives::account::AccessKeyPermission::FullAccess => {
Ok(Some(add_key::CliAccessKeyPermission::GrantFullAccess(
add_key::access_key_type::CliFullAccessType {
access_key_mode: Some(add_key::CliAccessKeyMode::UseManuallyProvidedPublicKey(
add_key::use_public_key::CliAddAccessKeyAction {
public_key: Some(public_key.into()),
next_action: None,
},
)),
},
)))
}
near_primitives::account::AccessKeyPermission::FunctionCall(
near_primitives::account::FunctionCallPermission {
allowance,
receiver_id,
method_names,
},
) => Ok(Some(
add_key::CliAccessKeyPermission::GrantFunctionCallAccess(
add_key::access_key_type::CliFunctionCallType {
allowance: {
match allowance {
Some(allowance) => {
Some(crate::types::near_allowance::NearAllowance::from_near(
allowance.into(),
))
}
None => Some(crate::types::near_allowance::NearAllowance::unlimited()),
}
},
contract_account_id: Some(receiver_id.parse()?),
function_names: Some(crate::types::vec_string::VecString(method_names)),
access_key_mode: Some(add_key::CliAccessKeyMode::UseManuallyProvidedPublicKey(
add_key::use_public_key::CliAddAccessKeyAction {
public_key: Some(public_key.into()),
next_action: None,
},
)),
},
),
)),
near_primitives::account::AccessKeyPermission::GasKeyFunctionCall(_, _)
| near_primitives::account::AccessKeyPermission::GasKeyFullAccess(_) => {
Err(color_eyre::eyre::eyre!(
"Gas key permissions are not yet supported in transaction reconstruction"
))
}
}
}
fn download_code(
contract_type: &crate::commands::contract::download_wasm::ContractType,
network_config: &crate::config::NetworkConfig,
block_reference: near_primitives::types::BlockReference,
file_path: &crate::types::path_buf::PathBuf,
hash_to_match: &near_primitives::hash::CryptoHash,
) -> color_eyre::eyre::Result<()> {
let code = crate::commands::contract::download_wasm::get_code(
contract_type,
network_config,
block_reference,
).map_err(|e| {
color_eyre::Report::msg(format!("Couldn't fetch the code. Please verify that you are using the archival node in the `network_connection.*.rpc_url` field of the `config.toml` file. You can see the list of RPC providers at https://docs.near.org/api/rpc/providers.\nError: {e}"))
})?;
let code_hash = near_primitives::hash::CryptoHash::hash_bytes(&code);
tracing::info!(
parent: &tracing::Span::none(),
"The code for <{}> was downloaded successfully with hash <{}>",
contract_type,
code_hash,
);
if &code_hash != hash_to_match {
return Err(color_eyre::Report::msg("The code hash of the contract deploy action does not match the code that we retrieved from the archive node.".to_string()));
}
std::fs::write(file_path, code).wrap_err(format!(
"Failed to write the deploy command code to file: '{file_path}' in the current folder"
))?;
tracing::info!(
parent: &tracing::Span::none(),
"The file `{}` with contract code of `{}` was downloaded successfully",
file_path,
contract_type,
);
Ok(())
}