split the existing tools into separate modules

This commit is contained in:
Jędrzej Stuczyński
2024-10-11 16:27:49 +01:00
parent 27ac34522c
commit 67462a9f47
9 changed files with 547 additions and 463 deletions
@@ -0,0 +1,4 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use clap::ArgGroup;
@@ -0,0 +1,56 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<PathBuf>,
/// 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<u64>,
/// 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<PathBuf>,
#[clap(long)]
pub migrate_msg: Option<serde_json::Value>,
}
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(())
}
@@ -0,0 +1,78 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<String> = 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<PathBuf>,
#[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,
}
}
}
@@ -0,0 +1,82 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<PathBuf>,
}
pub async fn create_importer_contract(
explicit_contract_path: Option<PathBuf>,
client: &DirectSigningHttpRpcNyxdClient,
) -> anyhow::Result<AccountId> {
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, "<empty>", 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(),
"<empty>",
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(())
}
@@ -0,0 +1,126 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<AccountId>,
/// 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<Model>) -> ExecuteMsg {
let pairs = data
.into_iter()
.map(|kv| (kv.key, kv.value))
.collect::<Vec<_>>();
pairs.into()
}
fn split_into_importable_execute_msgs(
kv_pairs: Vec<Model>,
approximate_max_chunk: usize,
) -> Vec<ExecuteMsg> {
let mut chunks: Vec<ExecuteMsg> = 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<AccountId>,
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<Model> = 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(())
}
@@ -0,0 +1,104 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<AccountId>,
/// 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<u64>,
/// 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<PathBuf>,
/// 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<serde_json::Value>,
}
pub async fn swap_contract(
target_code_id: Option<u64>,
target_contract_path: Option<PathBuf>,
explicit_importer_address: Option<AccountId>,
migrate_msg: Option<serde_json::Value>,
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, "<empty>", 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(())
}
@@ -0,0 +1,51 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<PathBuf>) -> anyhow::Result<PathBuf> {
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<AccountId>) -> anyhow::Result<AccountId> {
if let Some(explicit) = explicit {
return Ok(explicit);
}
let state = CachedState::load()?;
Ok(state.importer_address)
}
@@ -1,471 +1,14 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<String> = 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<PathBuf>,
#[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<Self> {
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<PathBuf>) -> anyhow::Result<PathBuf> {
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<AccountId>) -> anyhow::Result<AccountId> {
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<PathBuf>,
}
#[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<AccountId>,
/// 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<AccountId>,
/// 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<u64>,
/// 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<PathBuf>,
/// 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<serde_json::Value>,
}
#[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<PathBuf>,
/// 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<u64>,
/// 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<PathBuf>,
#[clap(long)]
pub migrate_msg: Option<serde_json::Value>,
}
#[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<PathBuf>,
client: &DirectSigningHttpRpcNyxdClient,
) -> anyhow::Result<AccountId> {
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, "<empty>", 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(),
"<empty>",
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<Model>) -> ExecuteMsg {
let pairs = data
.into_iter()
.map(|kv| (kv.key, kv.value))
.collect::<Vec<_>>();
pairs.into()
}
fn split_into_importable_execute_msgs(
kv_pairs: Vec<Model>,
approximate_max_chunk: usize,
) -> Vec<ExecuteMsg> {
let mut chunks: Vec<ExecuteMsg> = 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<AccountId>,
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<Model> = 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<u64>,
target_contract_path: Option<PathBuf>,
explicit_importer_address: Option<AccountId>,
migrate_msg: Option<serde_json::Value>,
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, "<empty>", 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<()> {
@@ -0,0 +1,40 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<Self> {
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")
}