Merge pull request #4954 from nymtech/feature/contract-state-tools
Feature/contract state tools
This commit is contained in:
Generated
+36
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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" }
|
||||
|
||||
+38
-6
@@ -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::<Result<_, _>>()?)
|
||||
}
|
||||
|
||||
async fn query_all_contract_state(&self, address: &AccountId) -> Result<Vec<Model>, 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,
|
||||
|
||||
@@ -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<u8>,
|
||||
|
||||
#[serde(with = "nym_serde_helpers::base64")]
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
// follow the cosmwasm serialisation format, i.e. hex for key and base64 for value
|
||||
|
||||
impl From<cosmrs::proto::cosmwasm::wasm::v1::Model> for Model {
|
||||
fn from(model: cosmrs::proto::cosmwasm::wasm::v1::Model) -> Self {
|
||||
Model {
|
||||
key: model.key,
|
||||
value: model.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EmptyMsg {}
|
||||
|
||||
|
||||
@@ -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<CosmwasmCommands>,
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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(())
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -32,6 +32,20 @@ pub mod bs58 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hex")]
|
||||
pub mod hex {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&::hex::encode(bytes))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, 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;
|
||||
|
||||
Generated
+2
@@ -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",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
build-importer-contract:
|
||||
$(MAKE) -C importer-contract build
|
||||
@@ -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" }
|
||||
@@ -0,0 +1,477 @@
|
||||
// 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 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<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
@@ -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 = []
|
||||
@@ -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
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<Response, StdError> {
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)]
|
||||
pub fn execute(
|
||||
deps: DepsMut<'_>,
|
||||
_env: Env,
|
||||
_info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, StdError> {
|
||||
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<QueryResponse, StdError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)]
|
||||
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: EmptyMessage) -> Result<Response, StdError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod contract;
|
||||
pub mod msg;
|
||||
|
||||
pub use base85rs;
|
||||
pub use msg::ExecuteMsg;
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<(Vec<u8>, Vec<u8>)>> for ExecuteMsg {
|
||||
fn from(raw: Vec<(Vec<u8>, Vec<u8>)>) -> Self {
|
||||
ExecuteMsg {
|
||||
pairs: raw
|
||||
.into_iter()
|
||||
.map(|(k, v)| (base85rs::encode(&k), base85rs::encode(&v)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user