diff --git a/Cargo.lock b/Cargo.lock index 23f75f575f..00d23fa76e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "base85rs" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87678d33a2af71f019ed11f52db246ca6c5557edee2cccbe689676d1ad9c6b5a" + [[package]] name = "basic-toml" version = "0.1.9" @@ -3299,6 +3305,34 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "importer-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "clap 4.5.18", + "dirs 5.0.1", + "importer-contract", + "nym-bin-common", + "nym-network-defaults", + "nym-validator-client", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "importer-contract" +version = "0.1.0" +dependencies = [ + "base85rs", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", +] + [[package]] name = "indenter" version = "0.3.3" @@ -5898,6 +5932,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -6334,6 +6369,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost 0.12.6", "reqwest 0.12.4", diff --git a/Cargo.toml b/Cargo.toml index ab9b668e93..19f0ef8f00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,8 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", "tools/echo-server", + "tools/internal/contract-state-importer/importer-cli", + "tools/internal/contract-state-importer/importer-contract", ] default-members = [ diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 6e8dd55772..d17b100672 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -20,6 +20,7 @@ nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } +nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } nym-http-api-client = { path = "../../../common/http-api-client" } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 47859ea0d1..5cea0a1ba2 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -5,7 +5,7 @@ use crate::nyxd; use crate::nyxd::coin::Coin; use crate::nyxd::cosmwasm_client::helpers::{create_pagination, next_page_key}; use crate::nyxd::cosmwasm_client::types::{ - Account, CodeDetails, Contract, ContractCodeId, SequenceResponse, SimulateResponse, + Account, CodeDetails, Contract, ContractCodeId, Model, SequenceResponse, SimulateResponse, }; use crate::nyxd::error::NyxdError; use crate::nyxd::Query; @@ -21,11 +21,11 @@ use cosmrs::proto::cosmos::tx::v1beta1::{ SimulateRequest, SimulateResponse as ProtoSimulateResponse, }; use cosmrs::proto::cosmwasm::wasm::v1::{ - QueryCodeRequest, QueryCodeResponse, QueryCodesRequest, QueryCodesResponse, - QueryContractHistoryRequest, QueryContractHistoryResponse, QueryContractInfoRequest, - QueryContractInfoResponse, QueryContractsByCodeRequest, QueryContractsByCodeResponse, - QueryRawContractStateRequest, QueryRawContractStateResponse, QuerySmartContractStateRequest, - QuerySmartContractStateResponse, + QueryAllContractStateRequest, QueryAllContractStateResponse, QueryCodeRequest, + QueryCodeResponse, QueryCodesRequest, QueryCodesResponse, QueryContractHistoryRequest, + QueryContractHistoryResponse, QueryContractInfoRequest, QueryContractInfoResponse, + QueryContractsByCodeRequest, QueryContractsByCodeResponse, QueryRawContractStateRequest, + QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse, }; use cosmrs::tendermint::{block, chain, Hash}; use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; @@ -444,6 +444,38 @@ pub trait CosmWasmClient: TendermintRpcClient { .collect::>()?) } + async fn query_all_contract_state(&self, address: &AccountId) -> Result, NyxdError> { + let path = Some("/cosmwasm.wasm.v1.Query/AllContractState".to_owned()); + + let mut models = Vec::new(); + let mut pagination = None; + + loop { + let req = QueryAllContractStateRequest { + address: address.to_string(), + pagination, + }; + + let mut res = self + .make_abci_query::<_, QueryAllContractStateResponse>(path.clone(), req) + .await?; + + let empty_response = res.models.is_empty(); + models.append(&mut res.models); + + if empty_response { + break; + } + if let Some(next_key) = next_page_key(res.pagination) { + pagination = Some(create_pagination(next_key)) + } else { + break; + } + } + + Ok(models.into_iter().map(Into::into).collect()) + } + async fn query_contract_raw( &self, address: &AccountId, diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs index 564a17441e..26003c1d29 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs @@ -27,13 +27,34 @@ use cosmrs::vesting::{ }; use cosmrs::{AccountId, Any, Coin as CosmosCoin}; use prost::Message; -use serde::Serialize; +use serde::{Deserialize, Serialize}; pub use cosmrs::abci::GasInfo; pub use cosmrs::abci::MsgResponse; pub type ContractCodeId = u64; +// yet another thing to put in cosmrs +#[derive(Serialize, Deserialize)] +pub struct Model { + #[serde(with = "nym_serde_helpers::hex")] + pub key: Vec, + + #[serde(with = "nym_serde_helpers::base64")] + pub value: Vec, +} + +// follow the cosmwasm serialisation format, i.e. hex for key and base64 for value + +impl From for Model { + fn from(model: cosmrs::proto::cosmwasm::wasm::v1::Model) -> Self { + Model { + key: model.key, + value: model.value, + } + } +} + #[derive(Serialize)] pub struct EmptyMsg {} diff --git a/common/commands/src/validator/cosmwasm/mod.rs b/common/commands/src/validator/cosmwasm/mod.rs index 43dcf08951..2fbd63a535 100644 --- a/common/commands/src/validator/cosmwasm/mod.rs +++ b/common/commands/src/validator/cosmwasm/mod.rs @@ -7,13 +7,14 @@ pub mod execute_contract; pub mod generators; pub mod init_contract; pub mod migrate_contract; +pub mod raw_contract_state; pub mod upload_contract; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] pub struct Cosmwasm { #[clap(subcommand)] - pub command: Option, + pub command: CosmwasmCommands, } #[derive(Debug, Subcommand)] @@ -28,4 +29,6 @@ pub enum CosmwasmCommands { Migrate(crate::validator::cosmwasm::migrate_contract::Args), /// Execute a WASM smart contract method Execute(crate::validator::cosmwasm::execute_contract::Args), + /// Obtain raw contract state of a cosmwasm smart contract + RawContractState(crate::validator::cosmwasm::raw_contract_state::Args), } diff --git a/common/commands/src/validator/cosmwasm/raw_contract_state.rs b/common/commands/src/validator/cosmwasm/raw_contract_state.rs new file mode 100644 index 0000000000..29771b7274 --- /dev/null +++ b/common/commands/src/validator/cosmwasm/raw_contract_state.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::QueryClient; +use clap::Parser; +use cosmrs::AccountId; +use log::trace; +use nym_validator_client::nyxd::CosmWasmClient; +use std::fs; +use std::fs::File; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long, value_parser)] + #[clap(help = "The address of contract to get the state of")] + pub contract: AccountId, + + #[clap(short, long)] + #[clap(help = "Output file for the retrieved contract state")] + pub output: PathBuf, +} + +pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { + trace!("args: {args:?}"); + + let output = File::create(&args.output)?; + let raw = client.query_all_contract_state(&args.contract).await?; + + serde_json::to_writer(output, &raw)?; + println!( + "wrote {} key-value from {} pairs into '{}'", + raw.len(), + args.contract, + fs::canonicalize(args.output)?.display() + ); + + Ok(()) +} diff --git a/common/serde-helpers/Cargo.toml b/common/serde-helpers/Cargo.toml index a45ed0afbd..bc9de1862e 100644 --- a/common/serde-helpers/Cargo.toml +++ b/common/serde-helpers/Cargo.toml @@ -13,11 +13,13 @@ license.workspace = true [dependencies] serde = { workspace = true } +hex = { workspace = true, optional = true } bs58 = { workspace = true, optional = true } base64 = { workspace = true, optional = true } time = { workspace = true, features = ["formatting", "parsing"], optional = true } [features] +hex = ["dep:hex"] bs58 = ["dep:bs58"] base64 = ["dep:base64"] date = ["time"] \ No newline at end of file diff --git a/common/serde-helpers/src/lib.rs b/common/serde-helpers/src/lib.rs index fd37be86b8..07ad83face 100644 --- a/common/serde-helpers/src/lib.rs +++ b/common/serde-helpers/src/lib.rs @@ -32,6 +32,20 @@ pub mod bs58 { } } +#[cfg(feature = "hex")] +pub mod hex { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&::hex::encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + ::hex::decode(&s).map_err(serde::de::Error::custom) + } +} + #[cfg(feature = "date")] pub mod date { use serde::ser::Error; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index f65e796297..951f240165 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3361,6 +3361,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -3449,6 +3450,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.4", diff --git a/tools/internal/contract-state-importer/Makefile b/tools/internal/contract-state-importer/Makefile new file mode 100644 index 0000000000..8e375fdf6a --- /dev/null +++ b/tools/internal/contract-state-importer/Makefile @@ -0,0 +1,2 @@ +build-importer-contract: + $(MAKE) -C importer-contract build \ No newline at end of file diff --git a/tools/internal/contract-state-importer/importer-cli/Cargo.toml b/tools/internal/contract-state-importer/importer-cli/Cargo.toml new file mode 100644 index 0000000000..45a2302209 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "importer-cli" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +bip39 = { workspace = true } +clap = { workspace = true, features = ["derive"] } +dirs = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } +tracing = { workspace = true } + +importer-contract = { path = "../importer-contract" } +nym-validator-client = { path = "../../../../common/client-libs/validator-client" } +nym-bin-common = { path = "../../../../common/bin-common", features = ["basic_tracing"] } +nym-network-defaults = { path = "../../../../common/network-defaults" } diff --git a/tools/internal/contract-state-importer/importer-cli/src/main.rs b/tools/internal/contract-state-importer/importer-cli/src/main.rs new file mode 100644 index 0000000000..842217363c --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/main.rs @@ -0,0 +1,477 @@ +// 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 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}; + +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, + } + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + setup_env(cli.config_env_file.as_ref()); + + setup_tracing_logger(); + cli.execute().await +} diff --git a/tools/internal/contract-state-importer/importer-contract/Cargo.toml b/tools/internal/contract-state-importer/importer-contract/Cargo.toml new file mode 100644 index 0000000000..7952b197c2 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "importer-contract" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +readme.workspace = true + + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +base85rs = "0.1.3" +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +cosmwasm-schema = { workspace = true } + +[features] +default = ["library"] +library = [] \ No newline at end of file diff --git a/tools/internal/contract-state-importer/importer-contract/Makefile b/tools/internal/contract-state-importer/importer-contract/Makefile new file mode 100644 index 0000000000..fdc2752a81 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/Makefile @@ -0,0 +1,5 @@ +all: build + +build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown --no-default-features + wasm-opt --signext-lowering -O ../../../../target/wasm32-unknown-unknown/release/importer_contract.wasm -o ../../../../target/wasm32-unknown-unknown/release/importer_contract.wasm \ No newline at end of file diff --git a/tools/internal/contract-state-importer/importer-contract/src/contract.rs b/tools/internal/contract-state-importer/importer-contract/src/contract.rs new file mode 100644 index 0000000000..c47a31f75b --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/src/contract.rs @@ -0,0 +1,45 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ExecuteMsg; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError}; + +#[cw_serde] +pub struct EmptyMessage {} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn instantiate( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn execute( + deps: DepsMut<'_>, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + for (key, value) in msg.pairs { + let key = base85rs::decode(&key).unwrap(); + let value = base85rs::decode(&value).unwrap(); + deps.storage.set(&key, &value); + } + + Ok(Default::default()) +} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn query(_: Deps<'_>, _: Env, _: EmptyMessage) -> Result { + Ok(Default::default()) +} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: EmptyMessage) -> Result { + Ok(Default::default()) +} diff --git a/tools/internal/contract-state-importer/importer-contract/src/lib.rs b/tools/internal/contract-state-importer/importer-contract/src/lib.rs new file mode 100644 index 0000000000..d1ca431ab0 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/src/lib.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod contract; +pub mod msg; + +pub use base85rs; +pub use msg::ExecuteMsg; diff --git a/tools/internal/contract-state-importer/importer-contract/src/msg.rs b/tools/internal/contract-state-importer/importer-contract/src/msg.rs new file mode 100644 index 0000000000..bbdce88fcc --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/src/msg.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct ExecuteMsg { + pub pairs: Vec<(String, String)>, +} + +impl From, Vec)>> for ExecuteMsg { + fn from(raw: Vec<(Vec, Vec)>) -> Self { + ExecuteMsg { + pairs: raw + .into_iter() + .map(|(k, v)| (base85rs::encode(&k), base85rs::encode(&v))) + .collect(), + } + } +} diff --git a/tools/nym-cli/src/validator/cosmwasm/mod.rs b/tools/nym-cli/src/validator/cosmwasm/mod.rs index 28f197eedb..d078f498ab 100644 --- a/tools/nym-cli/src/validator/cosmwasm/mod.rs +++ b/tools/nym-cli/src/validator/cosmwasm/mod.rs @@ -1,4 +1,4 @@ -use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; pub(crate) mod generators; @@ -9,14 +9,14 @@ pub(crate) async fn execute( network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match cosmwasm.command { - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Upload(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Upload(args) => { nym_cli_commands::validator::cosmwasm::upload_contract::upload( args, create_signing_client(global_args, network_details)?, ) .await } - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Init(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Init(args) => { nym_cli_commands::validator::cosmwasm::init_contract::init( args, create_signing_client(global_args, network_details)?, @@ -25,24 +25,30 @@ pub(crate) async fn execute( .await } - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::GenerateInitMessage( - generator, - )) => generators::execute(generator).await?, - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Migrate(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::GenerateInitMessage(generator) => { + generators::execute(generator).await? + } + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Migrate(args) => { nym_cli_commands::validator::cosmwasm::migrate_contract::migrate( args, create_signing_client(global_args, network_details)?, ) .await } - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Execute(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Execute(args) => { nym_cli_commands::validator::cosmwasm::execute_contract::execute( args, create_signing_client(global_args, network_details)?, ) .await } - _ => unreachable!(), + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::RawContractState(args) => { + nym_cli_commands::validator::cosmwasm::raw_contract_state::execute( + args, + create_query_client(network_details)?, + ) + .await? + } } Ok(()) }