diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs new file mode 100644 index 0000000000..6fd33e527d --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use clap::ArgGroup; diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_with_state.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_with_state.rs new file mode 100644 index 0000000000..4e11ccc579 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_with_state.rs @@ -0,0 +1,56 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::commands::prepare::create_importer_contract; +use crate::commands::set_state::set_importer_state; +use crate::commands::swap_contract::swap_contract; +use clap::ArgGroup; +use clap::Args; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +#[clap(group(ArgGroup::new("contract").required(true)))] +pub struct InitialiseWithStateArgs { + /// Path to the .wasm file with the importer contract + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" + #[clap(long)] + pub importer_contract_path: Option, + + /// Path to the file containing state dump of a cosmwasm contract + #[clap(long)] + pub raw_state: PathBuf, + + /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_code_id: Option, + + /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_path: Option, + + #[clap(long)] + pub migrate_msg: Option, +} + +pub async fn initialise_with_state( + args: InitialiseWithStateArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + let importer_address = create_importer_contract(args.importer_contract_path, &client).await?; + set_importer_state(args.raw_state, Some(importer_address.clone()), &client).await?; + swap_contract( + args.target_contract_code_id, + args.target_contract_path, + Some(importer_address.clone()), + args.migrate_msg, + &client, + ) + .await?; + + info!("the contract is ready at {importer_address}"); + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs new file mode 100644 index 0000000000..cd81aa3a14 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs @@ -0,0 +1,78 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::commands::initialise_with_state::{initialise_with_state, InitialiseWithStateArgs}; +use crate::commands::prepare::{execute_prepare_contract, PrepareArgs}; +use crate::commands::set_state::{execute_set_state, SetStateArgs}; +use crate::commands::swap_contract::{execute_swap_contract, SwapContractArgs}; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; +use std::path::PathBuf; +use std::sync::OnceLock; + +pub mod initialise_mixnet_vesting_with_states; +pub mod initialise_with_state; +pub mod prepare; +pub mod set_state; +pub mod swap_contract; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub struct Cli { + /// Path pointing to an env file that configures the CLI. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + #[clap(long)] + pub(crate) mnemonic: bip39::Mnemonic, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +pub(crate) enum Commands { + /// Upload and instantiates the importer contract + PrepareContract(PrepareArgs), + + /// Set the state of the previously instantiated importer contract with the provided state dump + SetState(SetStateArgs), + + /// Swap the importer contract code with the one corresponding to the previously uploaded state dump + SwapContract(SwapContractArgs), + + /// Combines the functionalities of `prepare-contract`, `set-state` and `swap-contract` + InitialiseWithState(InitialiseWithStateArgs), +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + let network_details = NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + let nyxd_url = network_details + .endpoints + .first() + .expect("network details are not defined") + .nyxd_url + .as_str(); + + let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + client_config, + nyxd_url, + self.mnemonic, + )?; + match self.command { + Commands::PrepareContract(args) => execute_prepare_contract(args, client).await, + Commands::SetState(args) => execute_set_state(args, client).await, + Commands::SwapContract(args) => execute_swap_contract(args, client).await, + Commands::InitialiseWithState(args) => initialise_with_state(args, client).await, + } + } +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/prepare.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/prepare.rs new file mode 100644 index 0000000000..40856ff3f4 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/prepare.rs @@ -0,0 +1,82 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::importer_contract_path; +use crate::state::CachedState; +use clap::Args; +use importer_contract::contract::EmptyMessage; +use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::fs; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +pub struct PrepareArgs { + /// Path to the .wasm file with the importer contract + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" + #[clap(long)] + pub importer_contract_path: Option, +} + +pub async fn create_importer_contract( + explicit_contract_path: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result { + info!("attempting to create the importer contract"); + + let importer_path = importer_contract_path(explicit_contract_path)?; + info!( + "going to use the following importer contract: '{}'", + fs::canonicalize(&importer_path)?.display() + ); + + let mut data = Vec::new(); + File::open(importer_path)?.read_to_end(&mut data)?; + + let res = client.upload(data, "", None).await?; + let importer_code_id = res.code_id; + info!( + " ✅ uploaded the importer contract in {}", + res.transaction_hash + ); + + let res = client + .instantiate( + importer_code_id, + &EmptyMessage {}, + "importer-contract".into(), + "", + Some(InstantiateOptions::default().with_admin(client.address())), + None, + ) + .await?; + let importer_address = res.contract_address; + info!( + " ✅ instantiated the importer contract in {}", + res.transaction_hash + ); + + info!("IMPORTER CONTRACT ADDRESS: {importer_address}"); + + CachedState { + importer_address: importer_address.clone(), + state_imported: false, + } + .save()?; + + Ok(importer_address) +} + +pub async fn execute_prepare_contract( + args: PrepareArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + create_importer_contract(args.importer_contract_path, &client).await?; + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/set_state.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/set_state.rs new file mode 100644 index 0000000000..0989243202 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/set_state.rs @@ -0,0 +1,126 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::importer_contract_address; +use crate::state::CachedState; +use anyhow::bail; +use clap::Args; +use importer_contract::{base85rs, ExecuteMsg}; +use nym_validator_client::nyxd::cosmwasm_client::types::Model; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::fs::File; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +pub struct SetStateArgs { + /// Explicit address of the initialised importer contract. + /// If not set, the value from the cached state will be attempted to be used + #[clap(long)] + pub importer_contract_address: Option, + + /// Path to the file containing state dump of a cosmwasm contract + #[clap(long)] + pub raw_state: PathBuf, +} + +fn approximate_size(pair: &Model) -> usize { + base85rs::encode(&pair.key).len() + base85rs::encode(&pair.value).len() +} + +fn models_to_exec(data: Vec) -> ExecuteMsg { + let pairs = data + .into_iter() + .map(|kv| (kv.key, kv.value)) + .collect::>(); + + pairs.into() +} + +fn split_into_importable_execute_msgs( + kv_pairs: Vec, + approximate_max_chunk: usize, +) -> Vec { + let mut chunks: Vec = Vec::new(); + + let mut current_wip_chunk = Vec::new(); + let mut current_chunk_size = 0; + for kv in kv_pairs { + if current_chunk_size + approximate_size(&kv) > approximate_max_chunk { + let taken = std::mem::take(&mut current_wip_chunk); + chunks.push(models_to_exec(taken)); + current_chunk_size = 0; + } + current_chunk_size += approximate_size(&kv); + current_wip_chunk.push(kv); + } + + if !current_wip_chunk.is_empty() { + chunks.push(models_to_exec(current_wip_chunk)) + } + chunks +} + +pub async fn set_importer_state( + state_dump_path: PathBuf, + explicit_importer_address: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + info!("attempting to set the importer contract state"); + + // this is the value that we found to be optimal during v1->v2 mixnet migration + const MAX_CHUNK_SIZE: usize = 350 * 1000; + + let importer_address = importer_contract_address(explicit_importer_address)?; + + if let Ok(state) = CachedState::load() { + if state.state_imported && state.importer_address == importer_address { + bail!("the state has already been imported for {importer_address}") + } + } + + let dump_file = File::open(state_dump_path)?; + info!("attempting to decode the state dump. for bigger contracts this might take a while..."); + let kv_pairs: Vec = serde_json::from_reader(&dump_file)?; + + info!("there are {} key-value pairs to import", kv_pairs.len()); + info!("attempting to split them into {MAX_CHUNK_SIZE}B chunks ExecuteMsgs..."); + + let chunks = split_into_importable_execute_msgs(kv_pairs, MAX_CHUNK_SIZE); + info!("obtained {} execute msgs", chunks.len()); + + let total = chunks.len(); + for (i, msg) in chunks.into_iter().enumerate() { + info!("executing message {}/{total}...", i + 1); + let res = client + .execute( + &importer_address, + &msg, + None, + "importing contract state", + Vec::new(), + ) + .await?; + info!(" ✅ OK: {}", res.transaction_hash); + } + + info!("Finished migrating storage to {importer_address}!"); + + CachedState { + importer_address, + state_imported: true, + } + .save()?; + + Ok(()) +} + +pub async fn execute_set_state( + args: SetStateArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + set_importer_state(args.raw_state, args.importer_contract_address, &client).await?; + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/swap_contract.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/swap_contract.rs new file mode 100644 index 0000000000..da969cab40 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/swap_contract.rs @@ -0,0 +1,104 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::importer_contract_address; +use crate::state::CachedState; +use anyhow::bail; +use clap::ArgGroup; +use clap::Args; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +#[clap(group(ArgGroup::new("contract").required(true)))] +pub struct SwapContractArgs { + /// Explicit address of the initialised importer contract. + /// If not set, the value from the cached state will be attempted to be used + #[clap(long)] + pub importer_contract_address: Option, + + /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_code_id: Option, + + /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_path: Option, + + /// The custom migrate message used for migrating into target contract. + /// If none is provided an empty object will be used instead, i.e. '{}' + #[clap(long)] + pub migrate_msg: Option, +} + +pub async fn swap_contract( + target_code_id: Option, + target_contract_path: Option, + explicit_importer_address: Option, + migrate_msg: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + info!("attempting to swap the contract code"); + + let importer_address = importer_contract_address(explicit_importer_address)?; + + if let Ok(state) = CachedState::load() { + if !state.state_imported && state.importer_address == importer_address { + bail!("the state hasn't been imported for {importer_address}") + } + } + + // one of those must have been set via clap + let code_id = match target_code_id { + Some(explicit) => explicit, + None => { + // upload the contract + let mut data = Vec::new(); + File::open(target_contract_path.unwrap())?.read_to_end(&mut data)?; + + let res = client.upload(data, "", None).await?; + info!( + " ✅ uploaded the target contract in {}", + res.transaction_hash + ); + res.code_id + } + }; + + let migrate_msg = migrate_msg.unwrap_or(serde_json::Value::Object(Default::default())); + let res = client + .migrate( + &importer_address, + code_id, + &migrate_msg, + "migrating into target contract", + None, + ) + .await?; + info!( + " ✅ migrated into the target contract: {}", + res.transaction_hash + ); + + Ok(()) +} + +pub async fn execute_swap_contract( + args: SwapContractArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + swap_contract( + args.target_contract_code_id, + args.target_contract_path, + args.importer_contract_address, + args.migrate_msg, + &client, + ) + .await?; + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/helpers.rs b/tools/internal/contract-state-importer/importer-cli/src/helpers.rs new file mode 100644 index 0000000000..7f876c181c --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/helpers.rs @@ -0,0 +1,51 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::CachedState; +use anyhow::bail; +use nym_validator_client::nyxd::AccountId; +use std::env::current_dir; +use std::fs; +use std::path::PathBuf; +use tracing::debug; + +// this only works if the cli is called from somewhere within the nym directory +// (which realistically is going to be the case most of the time) +pub fn importer_contract_path(explicit: Option) -> anyhow::Result { + if let Some(explicit) = explicit { + return Ok(explicit); + } + + for ancestor in current_dir()?.ancestors() { + debug!("checking {:?}", fs::canonicalize(ancestor)); + for content in ancestor.read_dir()? { + let dir_entry = content?; + let Ok(name) = dir_entry.file_name().into_string() else { + continue; + }; + + if name == "target" { + let maybe_contract_path = dir_entry + .path() + .join("wasm32-unknown-unknown") + .join("release") + .join("importer_contract.wasm"); + + if maybe_contract_path.exists() { + return Ok(maybe_contract_path); + } + } + } + } + + bail!("could not find importer_contract.wasm") +} + +pub fn importer_contract_address(explicit: Option) -> anyhow::Result { + if let Some(explicit) = explicit { + return Ok(explicit); + } + + let state = CachedState::load()?; + Ok(state.importer_address) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/main.rs b/tools/internal/contract-state-importer/importer-cli/src/main.rs index 842217363c..6962f3c092 100644 --- a/tools/internal/contract-state-importer/importer-cli/src/main.rs +++ b/tools/internal/contract-state-importer/importer-cli/src/main.rs @@ -1,471 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use anyhow::bail; -use clap::ArgGroup; -use clap::{Args, Parser, Subcommand}; -use importer_contract::contract::EmptyMessage; -use importer_contract::{base85rs, ExecuteMsg}; -use nym_bin_common::bin_info; +use crate::commands::Cli; +use clap::Parser; use nym_bin_common::logging::setup_tracing_logger; -use nym_network_defaults::{setup_env, NymNetworkDetails}; -use nym_validator_client::nyxd::cosmwasm_client::types::{InstantiateOptions, Model}; -use nym_validator_client::nyxd::AccountId; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; -use serde::{Deserialize, Serialize}; -use std::env::current_dir; -use std::fs; -use std::fs::{create_dir_all, File}; -use std::io::Read; -use std::path::PathBuf; -use std::sync::OnceLock; -use tracing::{debug, info}; +use nym_network_defaults::setup_env; -fn pretty_build_info_static() -> &'static str { - static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); - PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) -} - -#[derive(Parser)] -#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] -pub struct Cli { - /// Path pointing to an env file that configures the CLI. - #[clap(short, long)] - pub(crate) config_env_file: Option, - - #[clap(long)] - pub(crate) mnemonic: bip39::Mnemonic, - - #[clap(subcommand)] - command: Commands, -} - -#[derive(Serialize, Deserialize)] -pub struct CachedState { - pub importer_address: AccountId, - pub state_imported: bool, -} - -impl CachedState { - pub fn save(&self) -> anyhow::Result<()> { - let path = cached_state_file(); - if let Some(parent) = path.parent() { - create_dir_all(parent)?; - } - let file = File::create(&path)?; - serde_json::to_writer_pretty(file, self)?; - - info!("saved cached details to {}", path.display()); - Ok(()) - } - - pub fn load() -> anyhow::Result { - let file = File::open(cached_state_file())?; - Ok(serde_json::from_reader(&file)?) - } -} - -fn cached_state_file() -> PathBuf { - dirs::cache_dir() - .unwrap() - .join("contract-state-importer") - .join(".state.json") -} - -// this only works if the cli is called from somewhere within the nym directory -// (which realistically is going to be the case most of the time) -fn importer_contract_path(explicit: Option) -> anyhow::Result { - if let Some(explicit) = explicit { - return Ok(explicit); - } - - for ancestor in current_dir()?.ancestors() { - debug!("checking {:?}", fs::canonicalize(ancestor)); - for content in ancestor.read_dir()? { - let dir_entry = content?; - let Ok(name) = dir_entry.file_name().into_string() else { - continue; - }; - - if name == "target" { - let maybe_contract_path = dir_entry - .path() - .join("wasm32-unknown-unknown") - .join("release") - .join("importer_contract.wasm"); - - if maybe_contract_path.exists() { - return Ok(maybe_contract_path); - } - } - } - } - - bail!("could not find importer_contract.wasm") -} - -fn importer_contract_address(explicit: Option) -> anyhow::Result { - if let Some(explicit) = explicit { - return Ok(explicit); - } - - let state = CachedState::load()?; - Ok(state.importer_address) -} - -#[derive(Args, Clone)] -pub struct PrepareArgs { - /// Path to the .wasm file with the importer contract - /// If not provided, the CLI will attempt to traverse the parent directories until it finds - /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" - #[clap(long)] - pub importer_contract_path: Option, -} - -#[derive(Args, Clone)] -pub struct SetStateArgs { - /// Explicit address of the initialised importer contract. - /// If not set, the value from the cached state will be attempted to be used - #[clap(long)] - pub importer_contract_address: Option, - - /// Path to the file containing state dump of a cosmwasm contract - #[clap(long)] - pub raw_state: PathBuf, -} - -#[derive(Args, Clone)] -#[clap(group(ArgGroup::new("contract").required(true)))] -pub struct SwapContractArgs { - /// Explicit address of the initialised importer contract. - /// If not set, the value from the cached state will be attempted to be used - #[clap(long)] - pub importer_contract_address: Option, - - /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_code_id: Option, - - /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_path: Option, - - /// The custom migrate message used for migrating into target contract. - /// If none is provided an empty object will be used instead, i.e. '{}' - #[clap(long)] - pub migrate_msg: Option, -} - -#[derive(Args, Clone)] -#[clap(group(ArgGroup::new("contract").required(true)))] -pub struct InitialiseWithStateArgs { - /// Path to the .wasm file with the importer contract - /// If not provided, the CLI will attempt to traverse the parent directories until it finds - /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" - #[clap(long)] - pub importer_contract_path: Option, - - /// Path to the file containing state dump of a cosmwasm contract - #[clap(long)] - pub raw_state: PathBuf, - - /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_code_id: Option, - - /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_path: Option, - - #[clap(long)] - pub migrate_msg: Option, -} - -#[derive(Subcommand)] -pub(crate) enum Commands { - /// Upload and instantiates the importer contract - PrepareContract(PrepareArgs), - - /// Set the state of the previously instantiated importer contract with the provided state dump - SetState(SetStateArgs), - - /// Swap the importer contract code with the one corresponding to the previously uploaded state dump - SwapContract(SwapContractArgs), - - /// Combines the functionalities of `prepare-contract`, `set-state` and `swap-contract` - InitialiseWithState(InitialiseWithStateArgs), -} - -async fn create_importer_contract( - explicit_contract_path: Option, - client: &DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result { - info!("attempting to create the importer contract"); - - let importer_path = importer_contract_path(explicit_contract_path)?; - info!( - "going to use the following importer contract: '{}'", - fs::canonicalize(&importer_path)?.display() - ); - - let mut data = Vec::new(); - File::open(importer_path)?.read_to_end(&mut data)?; - - let res = client.upload(data, "", None).await?; - let importer_code_id = res.code_id; - info!( - " ✅ uploaded the importer contract in {}", - res.transaction_hash - ); - - let res = client - .instantiate( - importer_code_id, - &EmptyMessage {}, - "importer-contract".into(), - "", - Some(InstantiateOptions::default().with_admin(client.address())), - None, - ) - .await?; - let importer_address = res.contract_address; - info!( - " ✅ instantiated the importer contract in {}", - res.transaction_hash - ); - - info!("IMPORTER CONTRACT ADDRESS: {importer_address}"); - - CachedState { - importer_address: importer_address.clone(), - state_imported: false, - } - .save()?; - - Ok(importer_address) -} - -async fn execute_prepare_contract( - args: PrepareArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - create_importer_contract(args.importer_contract_path, &client).await?; - - Ok(()) -} - -fn approximate_size(pair: &Model) -> usize { - base85rs::encode(&pair.key).len() + base85rs::encode(&pair.value).len() -} - -fn models_to_exec(data: Vec) -> ExecuteMsg { - let pairs = data - .into_iter() - .map(|kv| (kv.key, kv.value)) - .collect::>(); - - pairs.into() -} - -fn split_into_importable_execute_msgs( - kv_pairs: Vec, - approximate_max_chunk: usize, -) -> Vec { - let mut chunks: Vec = Vec::new(); - - let mut current_wip_chunk = Vec::new(); - let mut current_chunk_size = 0; - for kv in kv_pairs { - if current_chunk_size + approximate_size(&kv) > approximate_max_chunk { - let taken = std::mem::take(&mut current_wip_chunk); - chunks.push(models_to_exec(taken)); - current_chunk_size = 0; - } - current_chunk_size += approximate_size(&kv); - current_wip_chunk.push(kv); - } - - if !current_wip_chunk.is_empty() { - chunks.push(models_to_exec(current_wip_chunk)) - } - chunks -} - -async fn set_importer_state( - state_dump_path: PathBuf, - explicit_importer_address: Option, - client: &DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - info!("attempting to set the importer contract state"); - - // this is the value that we found to be optimal during v1->v2 mixnet migration - const MAX_CHUNK_SIZE: usize = 350 * 1000; - - let importer_address = importer_contract_address(explicit_importer_address)?; - - if let Ok(state) = CachedState::load() { - if state.state_imported && state.importer_address == importer_address { - bail!("the state has already been imported for {importer_address}") - } - } - - let dump_file = File::open(state_dump_path)?; - info!("attempting to decode the state dump. for bigger contracts this might take a while..."); - let kv_pairs: Vec = serde_json::from_reader(&dump_file)?; - - info!("there are {} key-value pairs to import", kv_pairs.len()); - info!("attempting to split them into {MAX_CHUNK_SIZE}B chunks ExecuteMsgs..."); - - let chunks = split_into_importable_execute_msgs(kv_pairs, MAX_CHUNK_SIZE); - info!("obtained {} execute msgs", chunks.len()); - - let total = chunks.len(); - for (i, msg) in chunks.into_iter().enumerate() { - info!("executing message {}/{total}...", i + 1); - let res = client - .execute( - &importer_address, - &msg, - None, - "importing contract state", - Vec::new(), - ) - .await?; - info!(" ✅ OK: {}", res.transaction_hash); - } - - info!("Finished migrating storage to {importer_address}!"); - - CachedState { - importer_address, - state_imported: true, - } - .save()?; - - Ok(()) -} - -async fn execute_set_state( - args: SetStateArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - set_importer_state(args.raw_state, args.importer_contract_address, &client).await?; - - Ok(()) -} - -async fn swap_contract( - target_code_id: Option, - target_contract_path: Option, - explicit_importer_address: Option, - migrate_msg: Option, - client: &DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - info!("attempting to swap the contract code"); - - let importer_address = importer_contract_address(explicit_importer_address)?; - - if let Ok(state) = CachedState::load() { - if !state.state_imported && state.importer_address == importer_address { - bail!("the state hasn't been imported for {importer_address}") - } - } - - // one of those must have been set via clap - let code_id = match target_code_id { - Some(explicit) => explicit, - None => { - // upload the contract - let mut data = Vec::new(); - File::open(target_contract_path.unwrap())?.read_to_end(&mut data)?; - - let res = client.upload(data, "", None).await?; - info!( - " ✅ uploaded the target contract in {}", - res.transaction_hash - ); - res.code_id - } - }; - - let migrate_msg = migrate_msg.unwrap_or(serde_json::Value::Object(Default::default())); - let res = client - .migrate( - &importer_address, - code_id, - &migrate_msg, - "migrating into target contract", - None, - ) - .await?; - info!( - " ✅ migrated into the target contract: {}", - res.transaction_hash - ); - - Ok(()) -} - -async fn execute_swap_contract( - args: SwapContractArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - swap_contract( - args.target_contract_code_id, - args.target_contract_path, - args.importer_contract_address, - args.migrate_msg, - &client, - ) - .await?; - - Ok(()) -} - -async fn initialise_with_state( - args: InitialiseWithStateArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - let importer_address = create_importer_contract(args.importer_contract_path, &client).await?; - set_importer_state(args.raw_state, Some(importer_address.clone()), &client).await?; - swap_contract( - args.target_contract_code_id, - args.target_contract_path, - Some(importer_address.clone()), - args.migrate_msg, - &client, - ) - .await?; - - info!("the contract is ready at {importer_address}"); - - Ok(()) -} - -impl Cli { - pub async fn execute(self) -> anyhow::Result<()> { - let network_details = NymNetworkDetails::new_from_env(); - let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; - let nyxd_url = network_details - .endpoints - .first() - .expect("network details are not defined") - .nyxd_url - .as_str(); - - let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - client_config, - nyxd_url, - self.mnemonic, - )?; - match self.command { - Commands::PrepareContract(args) => execute_prepare_contract(args, client).await, - Commands::SetState(args) => execute_set_state(args, client).await, - Commands::SwapContract(args) => execute_swap_contract(args, client).await, - Commands::InitialiseWithState(args) => initialise_with_state(args, client).await, - } - } -} +pub mod commands; +pub mod helpers; +pub mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/tools/internal/contract-state-importer/importer-cli/src/state.rs b/tools/internal/contract-state-importer/importer-cli/src/state.rs new file mode 100644 index 0000000000..499632d543 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/state.rs @@ -0,0 +1,40 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_validator_client::nyxd::AccountId; +use serde::{Deserialize, Serialize}; +use std::fs::{create_dir_all, File}; +use std::path::PathBuf; +use tracing::info; + +#[derive(Serialize, Deserialize)] +pub struct CachedState { + pub importer_address: AccountId, + pub state_imported: bool, +} + +impl CachedState { + pub fn save(&self) -> anyhow::Result<()> { + let path = cached_state_file(); + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + let file = File::create(&path)?; + serde_json::to_writer_pretty(file, self)?; + + info!("saved cached details to {}", path.display()); + Ok(()) + } + + pub fn load() -> anyhow::Result { + let file = File::open(cached_state_file())?; + Ok(serde_json::from_reader(&file)?) + } +} + +pub fn cached_state_file() -> PathBuf { + dirs::cache_dir() + .unwrap() + .join("contract-state-importer") + .join(".state.json") +}