use core::str::FromStr;
use eyre::eyre;
use hdpath::StandardHDPath;
use serde_json as json;
use std::fs;
use std::path::PathBuf;
use std::str;
use std::time::Duration;
use toml;
use tracing::debug;
use ibc_relayer::keyring::{Secp256k1KeyPair, SigningKeyPair};
use crate::chain::cli::bootstrap::{
add_genesis_account, add_genesis_validator, add_wallet, collect_gen_txs, initialize,
start_chain,
};
use crate::chain::cli::provider::{
copy_validator_key_pair, query_consumer_genesis, query_consumer_proposal,
replace_genesis_state, submit_consumer_chain_proposal,
};
use crate::chain::driver::ChainDriver;
use crate::chain::exec::simple_exec;
use crate::error::{handle_generic_error, Error};
use crate::ibc::token::Token;
use crate::prelude::assert_eventually_succeed;
use crate::types::process::ChildProcess;
use crate::types::wallet::{Wallet, WalletAddress, WalletId};
pub trait ChainBootstrapMethodsExt {
fn read_file(&self, file_path: &str) -> Result<String, Error>;
fn write_file(&self, file_path: &str, content: &str) -> Result<(), Error>;
fn update_chain_config(
&self,
file: &str,
cont: impl FnOnce(&mut toml::Value) -> Result<(), Error>,
) -> Result<(), Error>;
fn initialize(&self) -> Result<(), Error>;
fn update_genesis_file(
&self,
file: &str,
cont: impl FnOnce(&mut serde_json::Value) -> Result<(), Error>,
) -> Result<(), Error>;
fn add_wallet(&self, wallet_id: &str) -> Result<Wallet, Error>;
fn add_genesis_account(&self, wallet: &WalletAddress, amounts: &[&Token]) -> Result<(), Error>;
fn add_genesis_validator(&self, wallet_id: &WalletId, token: &Token) -> Result<(), Error>;
fn collect_gen_txs(&self) -> Result<(), Error>;
fn start(&self) -> Result<ChildProcess, Error>;
fn submit_consumer_chain_proposal(
&self,
consumer_chain_id: &str,
spawn_time: &str,
) -> Result<(), Error>;
fn assert_consumer_chain_proposal_submitted(
&self,
chain_id: &str,
command_path: &str,
home_path: &str,
rpc_listen_address: &str,
) -> Result<(), Error>;
fn assert_consumer_chain_proposal_passed(
&self,
chain_id: &str,
command_path: &str,
home_path: &str,
rpc_listen_address: &str,
) -> Result<(), Error>;
fn query_consumer_genesis(
&self,
consumer_chain_driver: &ChainDriver,
consumer_chain_id: &str,
) -> Result<(), Error>;
fn replace_genesis_state(&self) -> Result<(), Error>;
fn copy_validator_key_pair(&self, provider_chain_driver: &ChainDriver) -> Result<(), Error>;
}
impl ChainBootstrapMethodsExt for ChainDriver {
fn read_file(&self, file_path: &str) -> Result<String, Error> {
let full_path = PathBuf::from(&self.home_path).join(file_path);
let res = fs::read_to_string(full_path)?;
Ok(res)
}
fn write_file(&self, file_path: &str, content: &str) -> Result<(), Error> {
let full_path = PathBuf::from(&self.home_path).join(file_path);
let full_path_str = format!("{}", full_path.display());
fs::write(full_path, content)?;
debug!("created new file {:?}", full_path_str);
Ok(())
}
fn update_chain_config(
&self,
file: &str,
cont: impl FnOnce(&mut toml::Value) -> Result<(), Error>,
) -> Result<(), Error> {
let config_path = format!("config/{file}");
let config1 = self.read_file(&config_path)?;
let mut config2 = toml::from_str(&config1).map_err(handle_generic_error)?;
cont(&mut config2)?;
let config3 = toml::to_string_pretty(&config2).map_err(handle_generic_error)?;
self.write_file(&config_path, &config3)?;
Ok(())
}
fn initialize(&self) -> Result<(), Error> {
initialize(self.chain_id.as_str(), &self.command_path, &self.home_path)
}
fn update_genesis_file(
&self,
file: &str,
cont: impl FnOnce(&mut serde_json::Value) -> Result<(), Error>,
) -> Result<(), Error> {
let config1 = self.read_file(&format!("config/{file}"))?;
let mut config2 = serde_json::from_str(&config1).map_err(handle_generic_error)?;
cont(&mut config2)?;
let config3 = serde_json::to_string_pretty(&config2).map_err(handle_generic_error)?;
self.write_file("config/genesis.json", &config3)?;
Ok(())
}
fn add_wallet(&self, wallet_id: &str) -> Result<Wallet, Error> {
let seed_content = add_wallet(
self.chain_id.as_str(),
&self.command_path,
&self.home_path,
wallet_id,
)?;
let json_val: json::Value = json::from_str(&seed_content).map_err(handle_generic_error)?;
let wallet_address = json_val
.get("address")
.ok_or_else(|| eyre!("expect address string field to be present in json result"))?
.as_str()
.ok_or_else(|| eyre!("expect address string field to be present in json result"))?
.to_string();
let seed_path = format!("{wallet_id}-seed.json");
self.write_file(&seed_path, &seed_content)?;
let hd_path = StandardHDPath::from_str(self.chain_type.hd_path())
.map_err(|e| eyre!("failed to create StandardHDPath: {:?}", e))?;
let key = Secp256k1KeyPair::from_seed_file(&seed_content, &hd_path)
.map_err(handle_generic_error)?;
Ok(Wallet::new(wallet_id.to_string(), wallet_address, key))
}
fn add_genesis_account(&self, wallet: &WalletAddress, amounts: &[&Token]) -> Result<(), Error> {
let amounts_str = amounts.iter().map(|t| t.to_string()).collect::<Vec<_>>();
add_genesis_account(
self.chain_id.as_str(),
&self.command_path,
&self.home_path,
&wallet.0,
&amounts_str,
)
}
fn add_genesis_validator(&self, wallet_id: &WalletId, token: &Token) -> Result<(), Error> {
add_genesis_validator(
self.chain_id.as_str(),
&self.command_path,
&self.home_path,
&wallet_id.0,
&token.to_string(),
)
}
fn collect_gen_txs(&self) -> Result<(), Error> {
collect_gen_txs(self.chain_id.as_str(), &self.command_path, &self.home_path)
}
fn start(&self) -> Result<ChildProcess, Error> {
let extra_start_args = self.chain_type.extra_start_args();
start_chain(
&self.command_path,
&self.home_path,
&self.rpc_listen_address(),
&self.grpc_listen_address(),
&extra_start_args
.iter()
.map(|s| s.as_ref())
.collect::<Vec<_>>(),
)
}
fn submit_consumer_chain_proposal(
&self,
consumer_chain_id: &str,
_spawn_time: &str,
) -> Result<(), Error> {
let res = simple_exec(
self.chain_id.as_str(),
"jq",
&[
"-r",
".genesis_time",
&format!("{}/config/genesis.json", self.home_path),
],
)?;
let mut spawn_time = res.stdout;
spawn_time.pop();
let raw_proposal = r#"
{
"title": "Create consumer chain",
"description": "First consumer chain",
"chain_id": "{consumer_chain_id}",
"initial_height": {
"revision_number": 1,
"revision_height": 1
},
"genesis_hash": "Z2VuX2hhc2g=",
"binary_hash": "YmluX2hhc2g=",
"spawn_time": "{spawn_time}",
"blocks_per_distribution_transmission": 10,
"consumer_redistribution_fraction": "0.75",
"distribution_transmission_channel": "",
"historical_entries": 10000,
"transfer_timeout_period": 100000000000,
"ccv_timeout_period": 100000000000,
"unbonding_period": 100000000000,
"deposit": "10000001stake"
}"#;
let proposal = raw_proposal.replace("{consumer_chain_id}", consumer_chain_id);
let proposal = proposal.replace("{spawn_time}", &spawn_time);
self.write_file("consumer_proposal.json", &proposal)?;
submit_consumer_chain_proposal(
self.chain_id.as_str(),
&self.command_path,
&self.home_path,
&self.rpc_listen_address(),
)
}
fn assert_consumer_chain_proposal_submitted(
&self,
chain_id: &str,
command_path: &str,
home_path: &str,
rpc_listen_address: &str,
) -> Result<(), Error> {
assert_eventually_succeed(
"consumer chain proposal submitted",
10,
Duration::from_secs(1),
|| {
match query_consumer_proposal(chain_id, command_path, home_path, rpc_listen_address) {
Ok(exec_output) => {
let json_res = json::from_str::<json::Value>(&exec_output.stdout).map_err(handle_generic_error)?;
let proposal_status = json_res.get("status")
.ok_or_else(|| eyre!("expected `status` field"))?
.as_str()
.ok_or_else(|| eyre!("expected string field"))?;
if proposal_status == "PROPOSAL_STATUS_VOTING_PERIOD" {
Ok(())
} else {
Err(Error::generic(eyre!("consumer chain proposal is not in voting period. Proposal status: {proposal_status}")))
}
},
Err(e) => Err(Error::generic(eyre!("Error querying the consumer chain proposal. Potential issues could be due to not using enough gas or the proposal submitted is invalid. Error: {e}"))),
}
},
)?;
Ok(())
}
fn assert_consumer_chain_proposal_passed(
&self,
chain_id: &str,
command_path: &str,
home_path: &str,
rpc_listen_address: &str,
) -> Result<(), Error> {
assert_eventually_succeed(
"consumer chain proposal passed",
10,
Duration::from_secs(5),
|| {
match query_consumer_proposal(chain_id, command_path, home_path, rpc_listen_address) {
Ok(exec_output) => {
let json_res = json::from_str::<json::Value>(&exec_output.stdout).map_err(handle_generic_error)?;
let proposal_status = json_res.get("status")
.ok_or_else(|| eyre!("expected `status` field"))?
.as_str()
.ok_or_else(|| eyre!("expected string field"))?;
if proposal_status == "PROPOSAL_STATUS_PASSED" {
Ok(())
} else {
Err(Error::generic(eyre!("consumer chain proposal has not passed. Proposal status: {proposal_status}")))
}
},
Err(e) => Err(Error::generic(eyre!("Error querying the consumer chain proposal. Potential issues could be due to not using enough gas or the proposal submitted is invalid. Error: {e}"))),
}
},
)?;
Ok(())
}
fn query_consumer_genesis(
&self,
consumer_chain_driver: &ChainDriver,
consumer_chain_id: &str,
) -> Result<(), Error> {
let consumer_genesis = query_consumer_genesis(
self.chain_id.as_str(),
&self.command_path,
&self.home_path,
&self.rpc_listen_address(),
consumer_chain_id,
&consumer_chain_driver.command_path,
)?;
consumer_chain_driver.write_file("config/consumer_genesis.json", &consumer_genesis)?;
Ok(())
}
fn replace_genesis_state(&self) -> Result<(), Error> {
let genesis_output = replace_genesis_state(self.chain_id.as_str(), &self.home_path)?;
self.write_file("config/genesis.json", &genesis_output)?;
Ok(())
}
fn copy_validator_key_pair(&self, provider_chain_driver: &ChainDriver) -> Result<(), Error> {
copy_validator_key_pair(
self.chain_id.as_str(),
&provider_chain_driver.home_path,
&self.home_path,
)?;
Ok(())
}
}