diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 31dc66a2..c1f12bae 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -20,7 +20,7 @@ permissions: jobs: contracts: name: Contracts - uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@v3.3.1 + uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@79d7ac76e34b3208fbe07559ac276d0ea48be4da with: rust-toolchain: stable coverage-args: --ignore-filename-regex='/.cargo/git' --output ./coverage.md diff --git a/Cargo.lock b/Cargo.lock index 49e0494a..535d555c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,6 +680,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "forge-rust-interact" +version = "0.0.0" +dependencies = [ + "clap", + "multiversx-sc", + "multiversx-sc-snippets", + "proxies", + "serde", + "sovereign-forge", + "toml", +] + [[package]] name = "form_urlencoded" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 4dda3dc6..60134fc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,6 @@ members = [ "testing-sc", "testing-sc/meta", "sovereign-forge", + "sovereign-forge/interactor", "sovereign-forge/meta", ] diff --git a/sovereign-forge/interactor/.gitignore b/sovereign-forge/interactor/.gitignore new file mode 100644 index 00000000..4f9be44d --- /dev/null +++ b/sovereign-forge/interactor/.gitignore @@ -0,0 +1,5 @@ +# Pem files are used for interactions, but shouldn't be committed +*.pem + +# State files are used for interactions, but shouldn't be committed +state.toml diff --git a/sovereign-forge/interactor/Cargo.toml b/sovereign-forge/interactor/Cargo.toml new file mode 100644 index 00000000..71b1d326 --- /dev/null +++ b/sovereign-forge/interactor/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "forge-rust-interact" +version = "0.0.0" +authors = ["you"] +edition = "2021" +publish = false + +[[bin]] +name = "forge-rust-interact" +path = "src/interactor_main.rs" + +[lib] +path = "src/interact.rs" + +[dependencies.sovereign-forge] +path = ".." + +[dependencies.multiversx-sc-snippets] +version = "0.54.5" + +[dependencies.multiversx-sc] +version = "0.54.5" + +[dependencies.proxies] +path = "../../common/proxies" + +[dependencies] +clap = { version = "4.4.7", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8.6" + +[features] +chain-simulator-tests = [] + diff --git a/sovereign-forge/interactor/config.toml b/sovereign-forge/interactor/config.toml new file mode 100644 index 00000000..1a67ad8d --- /dev/null +++ b/sovereign-forge/interactor/config.toml @@ -0,0 +1,7 @@ + +chain_type = 'simulator' +gateway_uri = 'http://localhost:8085' +# +# chain_type = 'real' +# gateway_uri = 'https://devnet-gateway.multiversx.com' + diff --git a/sovereign-forge/interactor/src/config.rs b/sovereign-forge/interactor/src/config.rs new file mode 100644 index 00000000..2d072b4b --- /dev/null +++ b/sovereign-forge/interactor/src/config.rs @@ -0,0 +1,51 @@ +#![allow(unused)] + +use serde::Deserialize; +use std::io::Read; + +/// Config file +const CONFIG_FILE: &str = "config.toml"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ChainType { + Real, + Simulator, +} + +/// Contract Interact configuration +#[derive(Debug, Deserialize)] +pub struct Config { + pub gateway_uri: String, + pub chain_type: ChainType, +} + +impl Config { + // Deserializes config from file + pub fn new() -> Self { + let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + toml::from_str(&content).unwrap() + } + + pub fn chain_simulator_config() -> Self { + Config { + gateway_uri: "http://localhost:8085".to_owned(), + chain_type: ChainType::Simulator, + } + } + + // Returns the gateway URI + pub fn gateway_uri(&self) -> &str { + &self.gateway_uri + } + + // Returns if chain type is chain simulator + pub fn use_chain_simulator(&self) -> bool { + match self.chain_type { + ChainType::Real => false, + ChainType::Simulator => true, + } + } +} diff --git a/sovereign-forge/interactor/src/interact.rs b/sovereign-forge/interactor/src/interact.rs new file mode 100644 index 00000000..09eced50 --- /dev/null +++ b/sovereign-forge/interactor/src/interact.rs @@ -0,0 +1,487 @@ +#![allow(non_snake_case)] + +mod config; + +use config::Config; +use multiversx_sc_snippets::{imports::*, sdk::bech32}; +use proxies::{ + chain_config_proxy::ChainConfigContractProxy, chain_factory_proxy::ChainFactoryContractProxy, + esdt_safe_proxy::EsdtSafeProxy, header_verifier_proxy::HeaderverifierProxy, + sovereign_forge_proxy::SovereignForgeProxy, +}; +use serde::{Deserialize, Serialize}; +use std::{ + io::{Read, Write}, + path::Path, +}; + +const STATE_FILE: &str = "state.toml"; +const CHAIN_CONFIG_CODE_PATH: &str = "../../chain-config/output/chain-config.mxsc.json"; +const CHAIN_FACTORY_CODE_PATH: &str = "../../chain-factory/output/chain-factory.mxsc.json"; +const HEADER_VERIFIER_CODE_PATH: &str = "../../header-verifier/output/header-verifier.mxsc.json"; +const ESDT_SAFE_CODE_PATH: &str = "../../esdt-safe/output/esdt-safe.mxsc.json"; + +pub async fn sovereign_forge_cli() { + env_logger::init(); + + let mut args = std::env::args(); + let _ = args.next(); + let cmd = args.next().expect("at least one argument required"); + let mut interact = ContractInteract::new().await; + match cmd.as_str() { + "deploy" => interact.deploy().await, + "upgrade" => interact.upgrade().await, + "completeSetupPhase" => interact.complete_setup_phase().await, + "deployPhaseOne" => interact.deploy_phase_one().await, + "deployPhaseTwo" => interact.deploy_phase_two().await, + "deployPhaseThree" => interact.deploy_phase_three().await, + "getChainFactoryAddress" => interact.chain_factories().await, + "getTokenHandlerAddress" => interact.token_handlers().await, + "getDeployCost" => interact.deploy_cost().await, + "getAllChainIds" => interact.chain_ids().await, + _ => panic!("unknown command: {}", &cmd), + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + contract_address: Option, + config_address: Option, + factory_address: Option, + header_verifier_address: Option, + esdt_safe_address: Option, +} + +impl State { + // Deserializes state from file + pub fn load_state() -> Self { + if Path::new(STATE_FILE).exists() { + let mut file = std::fs::File::open(STATE_FILE).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + toml::from_str(&content).unwrap() + } else { + Self::default() + } + } + + /// Sets the contract address + pub fn set_address(&mut self, address: Bech32Address) { + self.contract_address = Some(address); + } + + /// Sets the contract address + pub fn set_config_template(&mut self, address: Bech32Address) { + self.config_address = Some(address); + } + + /// Sets the contract address + pub fn set_factory_template(&mut self, address: Bech32Address) { + self.factory_address = Some(address); + } + + /// Sets the contract address + pub fn set_header_verifier_address(&mut self, address: Bech32Address) { + self.header_verifier_address = Some(address); + } + + pub fn set_esdt_safe_address(&mut self, address: Bech32Address) { + self.esdt_safe_address = Some(address); + } + + /// Returns the contract address + pub fn current_address(&self) -> &Bech32Address { + self.contract_address + .as_ref() + .expect("no known contract, deploy first") + } +} + +impl Drop for State { + // Serializes state to file + fn drop(&mut self) { + let mut file = std::fs::File::create(STATE_FILE).unwrap(); + file.write_all(toml::to_string(self).unwrap().as_bytes()) + .unwrap(); + } +} + +pub struct ContractInteract { + interactor: Interactor, + wallet_address: Address, + contract_code: BytesValue, + state: State, +} + +impl ContractInteract { + pub async fn new() -> Self { + let config = Config::new(); + let mut interactor = Interactor::new(config.gateway_uri()) + .await + .use_chain_simulator(config.use_chain_simulator()); + + interactor.set_current_dir_from_workspace("sovereign_forge/interactor"); + let wallet_address = interactor.register_wallet(test_wallets::alice()).await; + + // Useful in the chain simulator setting + // generate blocks until ESDTSystemSCAddress is enabled + interactor.generate_blocks_until_epoch(1).await.unwrap(); + + let contract_code = BytesValue::interpret_from( + "mxsc:../output/sovereign-forge.mxsc.json", + &InterpreterContext::default(), + ); + + ContractInteract { + interactor, + wallet_address, + contract_code, + state: State::load_state(), + } + } + + pub async fn deploy(&mut self) { + let deploy_cost = BigUint::::from(100u128); + + let new_address = self + .interactor + .tx() + .from(&self.wallet_address) + .gas(50_000_000u64) + .typed(SovereignForgeProxy) + .init(deploy_cost) + .code(&self.contract_code) + .returns(ReturnsNewAddress) + .run() + .await; + + let new_address_bech32 = bech32::encode(&new_address); + self.state.set_address(Bech32Address::from_bech32_string( + new_address_bech32.clone(), + )); + + println!("new Forge address: {new_address_bech32}"); + } + + pub async fn deploy_chain_factory(&mut self) { + let header_verifier_managed_address = + self.convert_address_to_managed(self.state.header_verifier_address.clone()); + let forge_managed_address = + self.convert_address_to_managed(self.state.contract_address.clone()); + let config_managed_address = + self.convert_address_to_managed(self.state.config_address.clone()); + let esdt_safe_managed_address = + self.convert_address_to_managed(self.state.esdt_safe_address.clone()); + + let new_address = self + .interactor + .tx() + .from(&self.wallet_address) + .gas(50_000_000u64) + .typed(ChainFactoryContractProxy) + .init( + forge_managed_address.clone(), + config_managed_address, + header_verifier_managed_address, + esdt_safe_managed_address, + forge_managed_address, // USE ACTUAL FEE-MARKET TEMPLATE + ) + .code(MxscPath::new(CHAIN_FACTORY_CODE_PATH)) + .returns(ReturnsNewAddress) + .run() + .await; + + let new_address_bech32 = bech32::encode(&new_address); + self.state + .set_factory_template(Bech32Address::from_bech32_string( + new_address_bech32.clone(), + )); + + println!("new Chain-Factory address: {new_address_bech32}"); + } + + pub fn convert_address_to_managed( + &mut self, + address: Option, + ) -> ManagedAddress { + let address_bech32 = address.as_ref().unwrap(); + + ManagedAddress::from(address_bech32.to_address()) + } + + pub async fn deploy_chain_config_template(&mut self) { + let new_address = self + .interactor + .tx() + .from(&self.wallet_address) + .gas(50_000_000u64) + .typed(ChainConfigContractProxy) + .init( + 1u64, + 2u64, + BigUint::from(100u64), + &self.wallet_address, + MultiValueEncoded::new(), + ) + .returns(ReturnsNewAddress) + .code(MxscPath::new(CHAIN_CONFIG_CODE_PATH)) + .run() + .await; + + let new_address_bech32 = bech32::encode(&new_address); + self.state + .set_config_template(Bech32Address::from_bech32_string( + new_address_bech32.clone(), + )); + + println!("new Chain-Config address: {new_address_bech32}"); + } + + pub async fn deploy_header_verifier_template(&mut self) { + let new_address = self + .interactor + .tx() + .from(&self.wallet_address) + .gas(50_000_000u64) + .typed(HeaderverifierProxy) + .init(MultiValueEncoded::new()) + .returns(ReturnsNewAddress) + .code(MxscPath::new(HEADER_VERIFIER_CODE_PATH)) + .run() + .await; + + let new_address_bech32 = bech32::encode(&new_address); + self.state + .set_header_verifier_address(Bech32Address::from_bech32_string( + new_address_bech32.clone(), + )); + + println!("new Header-Verifier address: {new_address_bech32}"); + } + + pub async fn deploy_esdt_safe_template(&mut self) { + let new_address = self + .interactor + .tx() + .from(&self.wallet_address) + .gas(80_000_000u64) + .typed(EsdtSafeProxy) + .init(false) + .returns(ReturnsNewAddress) + .code(MxscPath::new(ESDT_SAFE_CODE_PATH)) + .run() + .await; + + let new_address_bech32 = bech32::encode(&new_address); + self.state + .set_esdt_safe_address(Bech32Address::from_bech32_string( + new_address_bech32.clone(), + )); + + println!("new Header-Verifier address: {new_address_bech32}"); + } + pub async fn upgrade(&mut self) { + let response = self + .interactor + .tx() + .to(self.state.current_address()) + .from(&self.wallet_address) + .gas(50_000_000u64) + .typed(SovereignForgeProxy) + .upgrade() + .code(&self.contract_code) + .code_metadata(CodeMetadata::UPGRADEABLE) + .returns(ReturnsNewAddress) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn register_token_handler(&mut self, shard_id: u32) { + let bech32 = &self.state.contract_address.as_ref().unwrap(); + let address = bech32.to_address(); + let token_handler_address = ManagedAddress::from(address); + + let response = self + .interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_address()) + .gas(30_000_000u64) + .typed(SovereignForgeProxy) + .register_token_handler(shard_id, token_handler_address) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn register_chain_factory(&mut self, shard_id: u32) { + let bech32 = &self.state.factory_address.as_ref().unwrap(); + let address = bech32.to_address(); + let chain_factory_address = ManagedAddress::from(address); + + let response = self + .interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_address()) + .gas(30_000_000u64) + .typed(SovereignForgeProxy) + .register_chain_factory(shard_id, chain_factory_address) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn complete_setup_phase(&mut self) { + let response = self + .interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_address()) + .gas(30_000_000u64) + .typed(SovereignForgeProxy) + .complete_setup_phase() + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn deploy_phase_one(&mut self) { + let egld_amount = BigUint::::from(100u128); + + let min_validators = 1u64; + let max_validators = 3u64; + let min_stake = BigUint::::from(0u128); + let additional_stake_required = MultiValueVec::from(vec![MultiValue2::< + TokenIdentifier, + BigUint, + >::from(( + TokenIdentifier::from_esdt_bytes(&b""[..]), + BigUint::::from(0u128), + ))]); + + let response = self + .interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_address()) + .gas(100_000_000u64) + .typed(SovereignForgeProxy) + .deploy_phase_one( + min_validators, + max_validators, + min_stake, + additional_stake_required, + ) + .egld(egld_amount) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn deploy_phase_two(&mut self) { + let bls_keys = MultiValueVec::from(vec![ManagedBuffer::new_from_bytes(&b""[..])]); + + let response = self + .interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_address()) + .gas(30_000_000u64) + .typed(SovereignForgeProxy) + .deploy_phase_two(bls_keys) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn deploy_phase_three(&mut self) { + let is_sovereign_chain = false; + + let response = self + .interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_address()) + .gas(80_000_000u64) + .typed(SovereignForgeProxy) + .deploy_phase_three(is_sovereign_chain) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn chain_factories(&mut self) { + let shard_id = 0u32; + + let result_value = self + .interactor + .query() + .to(self.state.current_address()) + .typed(SovereignForgeProxy) + .chain_factories(shard_id) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {result_value:?}"); + } + + pub async fn token_handlers(&mut self) { + let shard_id = 0u32; + + let result_value = self + .interactor + .query() + .to(self.state.current_address()) + .typed(SovereignForgeProxy) + .token_handlers(shard_id) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {result_value:?}"); + } + + pub async fn deploy_cost(&mut self) { + let result_value = self + .interactor + .query() + .to(self.state.current_address()) + .typed(SovereignForgeProxy) + .deploy_cost() + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {result_value:?}"); + } + + pub async fn chain_ids(&mut self) { + let result_value = self + .interactor + .query() + .to(self.state.current_address()) + .typed(SovereignForgeProxy) + .chain_ids() + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {result_value:?}"); + } +} diff --git a/sovereign-forge/interactor/src/interactor_main.rs b/sovereign-forge/interactor/src/interactor_main.rs new file mode 100644 index 00000000..b4c06c2f --- /dev/null +++ b/sovereign-forge/interactor/src/interactor_main.rs @@ -0,0 +1,7 @@ +use forge_rust_interact::sovereign_forge_cli; +use multiversx_sc_snippets::imports::*; + +#[tokio::main] +async fn main() { + sovereign_forge_cli().await; +} diff --git a/sovereign-forge/interactor/state.toml b/sovereign-forge/interactor/state.toml new file mode 100644 index 00000000..de9f43bb --- /dev/null +++ b/sovereign-forge/interactor/state.toml @@ -0,0 +1,5 @@ +contract_address = "erd1qqqqqqqqqqqqqpgqt54uz2y2frvy7fqet479a6efarnyw3yud8ssj6kyuz" +config_address = "erd1qqqqqqqqqqqqqpgqd3sm30pzrsqs2y7ct6368tlt94d6xvl6d8ssqhckfm" +factory_address = "erd1qqqqqqqqqqqqqpgq9xqda668zt76c8tvfqe9t39g7pz0j9t2d8sss3w65z" +header_verifier_address = "erd1qqqqqqqqqqqqqpgqlmtayq49qt94dj7gghagd7ap3d3htj76d8sszr0t7z" +esdt_safe_address = "erd1qqqqqqqqqqqqqpgqvxvmyp2fgr6qekksx8n6t8netkpweh28d8ssw9gwh8" diff --git a/sovereign-forge/interactor/tests/interact_cs_tests.rs b/sovereign-forge/interactor/tests/interact_cs_tests.rs new file mode 100644 index 00000000..f035b97d --- /dev/null +++ b/sovereign-forge/interactor/tests/interact_cs_tests.rs @@ -0,0 +1,27 @@ +use forge_rust_interact::ContractInteract; +use multiversx_sc_snippets::imports::*; + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn deploy_test_sovereign_forge_cs() { + let mut interactor = ContractInteract::new().await; + interactor.deploy().await; + + interactor.deploy_header_verifier_template().await; + interactor.deploy_chain_config_template().await; + interactor.deploy_esdt_safe_template().await; + interactor.deploy_chain_factory().await; + + interactor.register_token_handler(1).await; + interactor.register_token_handler(2).await; + interactor.register_token_handler(3).await; + interactor.register_chain_factory(1).await; + interactor.register_chain_factory(2).await; + interactor.register_chain_factory(3).await; + + interactor.complete_setup_phase().await; + + interactor.deploy_phase_one().await; + interactor.deploy_phase_two().await; + interactor.deploy_phase_three().await; +} diff --git a/sovereign-forge/interactor/tests/interact_tests.rs b/sovereign-forge/interactor/tests/interact_tests.rs new file mode 100644 index 00000000..3d755c41 --- /dev/null +++ b/sovereign-forge/interactor/tests/interact_tests.rs @@ -0,0 +1,26 @@ +use forge_rust_interact::ContractInteract; +use multiversx_sc_snippets::imports::tokio; + +#[tokio::test] +#[ignore = "run on demand, relies on real blockchain state"] +async fn deploy_test_sovereign_forge() { + let mut interactor = ContractInteract::new().await; + interactor.deploy().await; + + interactor.deploy_chain_factory().await; + interactor.deploy_chain_config_template().await; + interactor.deploy_header_verifier_template().await; + + interactor.register_token_handler(1).await; + interactor.register_token_handler(2).await; + interactor.register_token_handler(3).await; + interactor.register_chain_factory(1).await; + interactor.register_chain_factory(2).await; + interactor.register_chain_factory(3).await; + + interactor.complete_setup_phase().await; + + interactor.deploy_phase_one().await; + interactor.deploy_phase_two().await; + interactor.deploy_phase_three().await; +}